Skip to content

Commit ccec4e1

Browse files
committed
0.8.0
1 parent 9793eb6 commit ccec4e1

11 files changed

Lines changed: 162 additions & 88 deletions

File tree

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# UILabel Typography Extensions 📐
2-
Set line height, letter spacing (and more) on `UILabel`.
2+
Set line height, letter spacing (and more) directly on `UILabel`.
33

44
```Swift
55
let label = UILabel()
@@ -15,7 +15,9 @@ label.strikethrough = .patternDash
1515

1616
A single extension on `UILabel` that adds some typographic property (see above) using [**Objective-C Runtime**] (for stored properties on a Swift extension), [`NSAttributedString`] (for manage typographic properties), and [`NSKeyValueObservation`] (to be able to use the regular `text` property to manage text content).
1717

18-
> 🚧 **Work in progress**. A complemetary blog post (with a corresponding Figma file), and more preview/testing is underway. Also I have high hopes to add leading and trailing image feature as well, so stay tuned.
18+
See the complementary article at [**UILabel line height, letter spacing and more** UILabel typography extensions].
19+
20+
> 🚧 **Work in progress**. I have high hopes to add leading and trailing image feature, as well as upport for `UIButton`.
1921
2022
## License
2123

@@ -24,3 +26,4 @@ A single extension on `UILabel` that adds some typographic property (see above)
2426
[**Objective-C Runtime**]: https://developer.apple.com/documentation/objectivec/objective-c_runtime
2527
[`NSAttributedString`]: https://developer.apple.com/documentation/foundation/nsattributedstring
2628
[`NSKeyValueObservation`]: https://developer.apple.com/documentation/foundation/nskeyvalueobservation
29+
[**UILabel line height, letter spacing and more** UILabel typography extensions]: http://blog.eppz.eu/uilabel-line-height-letter-spacing-and-more-uilabel-typography-extensions/

UILabel_Typography_Extensions.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
55F7E34125A0A91E00E7F48B /* LoremIpsumViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F7E34025A0A91E00E7F48B /* LoremIpsumViewController.swift */; };
2525
55F7E34425A0AA5E00E7F48B /* GlyphViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F7E34325A0AA5E00E7F48B /* GlyphViewController.swift */; };
2626
55F7E34825A0AD7F00E7F48B /* AttributesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F7E34725A0AD7F00E7F48B /* AttributesViewController.swift */; };
27-
55F7E34B25A0ADFA00E7F48B /* UILabel+Empty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F7E34A25A0ADFA00E7F48B /* UILabel+Empty.swift */; };
27+
55F7E34B25A0ADFA00E7F48B /* UILabel+Typograpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F7E34A25A0ADFA00E7F48B /* UILabel+Typograpy.swift */; };
2828
55F7F0A2259A6F03001BEF90 /* Typography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F7F0A1259A6F03001BEF90 /* Typography.swift */; };
2929
/* End PBXBuildFile section */
3030

@@ -49,7 +49,7 @@
4949
55F7E34025A0A91E00E7F48B /* LoremIpsumViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoremIpsumViewController.swift; sourceTree = "<group>"; };
5050
55F7E34325A0AA5E00E7F48B /* GlyphViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlyphViewController.swift; sourceTree = "<group>"; };
5151
55F7E34725A0AD7F00E7F48B /* AttributesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributesViewController.swift; sourceTree = "<group>"; };
52-
55F7E34A25A0ADFA00E7F48B /* UILabel+Empty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Empty.swift"; sourceTree = "<group>"; };
52+
55F7E34A25A0ADFA00E7F48B /* UILabel+Typograpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Typograpy.swift"; sourceTree = "<group>"; };
5353
55F7E34F25A0B32D00E7F48B /* NSAttributedString+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Extensions.swift"; sourceTree = "<group>"; };
5454
55F7F0A1259A6F03001BEF90 /* Typography.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typography.swift; sourceTree = "<group>"; };
5555
/* End PBXFileReference section */
@@ -69,7 +69,7 @@
6969
isa = PBXGroup;
7070
children = (
7171
55F7F0A1259A6F03001BEF90 /* Typography.swift */,
72-
55F7E34A25A0ADFA00E7F48B /* UILabel+Empty.swift */,
72+
55F7E34A25A0ADFA00E7F48B /* UILabel+Typograpy.swift */,
7373
55F7E33125A03D8300E7F48B /* UILabel+Observer.swift */,
7474
55F7E33825A03FE500E7F48B /* UILabel+Grid.swift */,
7575
559795E425A259E400A757A3 /* UIColor+Styles.swift */,
@@ -205,7 +205,7 @@
205205
isa = PBXSourcesBuildPhase;
206206
buildActionMask = 2147483647;
207207
files = (
208-
55F7E34B25A0ADFA00E7F48B /* UILabel+Empty.swift in Sources */,
208+
55F7E34B25A0ADFA00E7F48B /* UILabel+Typograpy.swift in Sources */,
209209
55653BE625A36E0300678FD9 /* NSAttributedString+Extensions.swift in Sources */,
210210
55F236A225912BBE0007BC69 /* AppDelegate.swift in Sources */,
211211
55F7F0A2259A6F03001BEF90 /* Typography.swift in Sources */,

UILabel_Typography_Extensions/AttributesViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class AttributesViewController: UIViewController {
7070
.with {
7171
$0.text = "Line Height / Recolored"
7272
$0.textColor = .systemGreen
73+
$0.backgroundColor = .gray
7374
$0.layer.compositingFilter = "multiplyBlendMode"
7475
$0.showGrid = true
7576
}

UILabel_Typography_Extensions/Typography/Drafts/UIFont+Extensions.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,15 @@ extension UIFont {
6363
}
6464
}
6565

66-
extension Double {
66+
fileprivate extension Double {
6767

6868
var decimalPlaces: Int {
6969
let decimals = String(self).split(separator: ".")[1]
7070
return decimals == "0" ? 0 : decimals.count
7171
}
7272
}
7373

74-
extension CGFloat {
74+
fileprivate extension CGFloat {
7575

7676
var decimalPlaces: Int {
7777
Double(self).decimalPlaces

UILabel_Typography_Extensions/Typography/NSAttributedString+Extensions.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,26 @@
22
// NSAttributedString+Extensions.swift
33
// UILabel_Typography_Extensions
44
//
5-
// Created by Geri Borbás on 02/01/2021.
5+
// Copyright © 2020. Geri Borbás. All rights reserved.
6+
// https://twitter.com/Geri_Borbas
7+
//
8+
// Permission is hereby granted, free of charge, to any person obtaining a copy
9+
// of this software and associated documentation files (the "Software"), to deal
10+
// in the Software without restriction, including without limitation the rights
11+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
// copies of the Software, and to permit persons to whom the Software is
13+
// furnished to do so, subject to the following conditions:
14+
//
15+
// The above copyright notice and this permission notice shall be included in all
16+
// copies or substantial portions of the Software.
17+
//
18+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
// SOFTWARE.
625
//
726

827
import Foundation

UILabel_Typography_Extensions/Typography/Typography.swift

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,28 +28,50 @@
2828
import UIKit
2929

3030

31-
public protocol TypographyExtensions {
31+
public protocol TypographyExtensions: UILabel {
3232

33-
/// The line height for the reciever (setter lays out and sets `attributedText`).
33+
/// Set the line height (points) on the underlying `NSAttributedString` (with
34+
/// vertical centering). The provided `lineHeight` value is set to the
35+
/// `minimumLineHeight` and `maximumLineHeight` property of the `paragraphStyle`
36+
/// attribute.
37+
///
38+
/// If the label has no `attributedText` populated yet, a new one will be created
39+
/// with the corresponding attributes (otherwise the existing `attributedText`
40+
/// will be mutated). In order to preserve consistent `baselineOffset` behavior,
41+
/// the `attributedText` property gets updated whenever the `text` property of
42+
/// the label changes.
3443
var lineHeight: CGFloat? { get set }
3544

36-
/// The letter spacing (kerning) for the reciever (setter lays out and sets `attributedText`).
45+
/// Set the letter spacing (kerning) on the underlying `NSAttributedString`.
46+
///
47+
/// If the label has no `attributedText` populated yet, a new one will be created
48+
/// with the corresponding attributes (otherwise the existing `attributedText`
49+
/// will be mutated).
3750
var letterSpacing: CGFloat? { get set }
3851

39-
/// The underline style for the reciever (setter lays out and sets `attributedText`).
52+
/// Set the underline style on the underlying `NSAttributedString`.
53+
///
54+
/// If the label has no `attributedText` populated yet, a new one will be created
55+
/// with the corresponding attributes (otherwise the existing `attributedText`
56+
/// will be mutated).
4057
var underline: NSUnderlineStyle? { get set }
4158

42-
/// The strikethrough style for the reciever (setter lays out and sets `attributedText`).
59+
/// Set the strikethrough style on the underlying `NSAttributedString`.
60+
///
61+
/// If the label has no `attributedText` populated yet, a new one will be created
62+
/// with the corresponding attributes (otherwise the existing `attributedText`
63+
/// will be mutated).
4364
var strikethrough: NSUnderlineStyle? { get set }
4465

66+
/// If this value is set to `true`, the label will add a sublayer containing
67+
/// a typographic grid (updated on `layoutSubviews`).
68+
var showGrid: Bool { get set }
69+
4570
/// The leading image for the reciever (setter lays out and sets `attributedText`).
4671
var leadingImage: Typography.Image? { get set }
4772

4873
/// The trailing image for the reciever (setter lays out and sets `attributedText`).
4974
var trailingImage: Typography.Image? { get set }
50-
51-
/// Show typographic grid.
52-
var showGrid: Bool { get set }
5375
}
5476

5577

UILabel_Typography_Extensions/Typography/UIColor+Styles.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,26 @@
22
// UIColor+Styles.swift
33
// UILabel_Typography_Extensions
44
//
5-
// Created by Geri Borbás on 03/01/2021.
5+
// Copyright © 2020. Geri Borbás. All rights reserved.
6+
// https://twitter.com/Geri_Borbas
7+
//
8+
// Permission is hereby granted, free of charge, to any person obtaining a copy
9+
// of this software and associated documentation files (the "Software"), to deal
10+
// in the Software without restriction, including without limitation the rights
11+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
// copies of the Software, and to permit persons to whom the Software is
13+
// furnished to do so, subject to the following conditions:
14+
//
15+
// The above copyright notice and this permission notice shall be included in all
16+
// copies or substantial portions of the Software.
17+
//
18+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
// SOFTWARE.
625
//
726

827
import UIKit

UILabel_Typography_Extensions/Typography/UILabel+Grid.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,7 @@ extension UILabel {
151151

152152

153153
fileprivate extension UILabel {
154-
155-
var paragraphStyle: NSParagraphStyle? {
156-
attributedText?.attribute(NSAttributedString.Key.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle
157-
}
158-
154+
159155
var baselineOffset: CGFloat {
160156
guard let lineHeight = paragraphStyle?.maximumLineHeight,
161157
lineHeight != 0.0 else {

UILabel_Typography_Extensions/Typography/UILabel+Observer.swift

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,30 +29,30 @@ import UIKit
2929

3030
extension UILabel {
3131

32+
fileprivate typealias TextObserver = Observer<UILabel, String?>
33+
3234
fileprivate struct Keys {
3335
static var observer: UInt8 = 0
3436
}
3537

36-
var observer: Observer? {
38+
fileprivate var observer: TextObserver? {
3739
get {
38-
objc_getAssociatedObject(self, &Keys.observer) as? Observer
40+
objc_getAssociatedObject(self, &Keys.observer) as? TextObserver
3941
}
4042
set {
4143
objc_setAssociatedObject(self, &Keys.observer, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
4244
}
4345
}
44-
46+
4547
func observeIfNeeded() {
4648
guard observer == nil else {
4749
return
4850
}
4951

50-
observer = Observer(
52+
observer = TextObserver(
5153
for: self,
52-
onTextChange: { [weak self] text in
53-
54-
/// This is needed every time after the `text` property updated
55-
/// in order to render a consistent baseline offset.
54+
keyPath: \.text,
55+
onChange: { [weak self] _ in
5656
if let attributedText = self?.attributedText {
5757
self?.attributedText = attributedText
5858
}
@@ -62,24 +62,24 @@ extension UILabel {
6262
}
6363

6464

65-
class Observer: NSObject {
65+
class Observer<ObjectType: NSObject, ValueType>: NSObject {
6666

67-
typealias TextChangeAction = (_ text: String?) -> Void
68-
let onTextChange: TextChangeAction
67+
typealias OnChangeAction = (_ value: ValueType?) -> Void
68+
let onChange: OnChangeAction
6969
private var observer: NSKeyValueObservation?
7070

71-
init(for label: UILabel, onTextChange: @escaping TextChangeAction) {
72-
self.onTextChange = onTextChange
71+
init(for object: ObjectType, keyPath: KeyPath<ObjectType, ValueType>, onChange: @escaping OnChangeAction) {
72+
self.onChange = onChange
7373
super.init()
74-
observe(label)
74+
observe(object, keyPath: keyPath)
7575
}
7676

77-
func observe(_ label: UILabel) {
78-
observer = label.observe(
79-
\.text,
77+
func observe(_ object: ObjectType, keyPath: KeyPath<ObjectType, ValueType>) {
78+
observer = object.observe(
79+
keyPath,
8080
options: [.new, .old],
8181
changeHandler: { [weak self] _, change in
82-
self?.onTextChange(change.newValue ?? nil)
82+
self?.onChange(change.newValue ?? nil)
8383
}
8484
)
8585
}

0 commit comments

Comments
 (0)