Skip to content
This repository was archived by the owner on Feb 17, 2021. It is now read-only.

Commit 2343c3f

Browse files
committed
HUED-8605 Test & Fix Potential truncation issue in LabelLayout
- Added line break mode support in LabelLayout to calculate correct size based on line break mode.
1 parent dc2ff66 commit 2343c3f

File tree

6 files changed

+127
-17
lines changed

6 files changed

+127
-17
lines changed

Sources/Internal/NSAttributedStringExtension.swift

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@ extension NSAttributedString {
1212

1313
/// Returns a new NSAttributedString with a given font and the same attributes.
1414
func with(font: UIFont) -> NSAttributedString {
15-
let fontAttribute = [NSAttributedStringKey.font: font]
16-
let attributedTextWithFont = NSMutableAttributedString(string: string, attributes: fontAttribute)
15+
return with(additionalAttributes: [NSAttributedStringKey.font: font])
16+
}
17+
18+
/// Returns a new NSAttributedString with previous as well as additional attributes.
19+
func with(additionalAttributes: [NSAttributedStringKey : Any]?) -> NSAttributedString {
20+
let attributedTextWithAdditionalAttributes = NSMutableAttributedString(string: string, attributes: additionalAttributes)
1721
let fullRange = NSMakeRange(0, (string as NSString).length)
18-
attributedTextWithFont.beginEditing()
22+
attributedTextWithAdditionalAttributes.beginEditing()
1923
self.enumerateAttributes(in: fullRange, options: .longestEffectiveRangeNotRequired, using: { (attributes, range, _) in
20-
attributedTextWithFont.addAttributes(attributes, range: range)
24+
attributedTextWithAdditionalAttributes.addAttributes(attributes, range: range)
2125
})
22-
attributedTextWithFont.endEditing()
23-
24-
return attributedTextWithFont
26+
attributedTextWithAdditionalAttributes.endEditing()
27+
return attributedTextWithAdditionalAttributes
2528
}
26-
2729
}

Sources/Layouts/LabelLayout.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ open class LabelLayout<Label: UILabel>: BaseLayout<Label>, ConfigurableLayout {
1717
open let font: UIFont
1818
open let numberOfLines: Int
1919
open let lineHeight: CGFloat
20+
open let lineBreakMode: NSLineBreakMode
2021

2122
public init(text: Text,
2223
font: UIFont = LabelLayoutDefaults.defaultFont,
2324
lineHeight: CGFloat? = nil,
2425
numberOfLines: Int = LabelLayoutDefaults.defaultNumberOfLines,
26+
lineBreakMode: NSLineBreakMode = LabelLayoutDefaults.defaultLineBreakMode,
2527
alignment: Alignment = LabelLayoutDefaults.defaultAlignment,
2628
flexibility: Flexibility = LabelLayoutDefaults.defaultFlexibility,
2729
viewReuseId: String? = nil,
@@ -31,13 +33,15 @@ open class LabelLayout<Label: UILabel>: BaseLayout<Label>, ConfigurableLayout {
3133
self.numberOfLines = numberOfLines
3234
self.font = font
3335
self.lineHeight = lineHeight ?? font.lineHeight
36+
self.lineBreakMode = lineBreakMode
3437
super.init(alignment: alignment, flexibility: flexibility, viewReuseId: viewReuseId, config: config)
3538
}
3639

3740
init(attributedString: NSAttributedString,
3841
font: UIFont = LabelLayoutDefaults.defaultFont,
3942
lineHeight: CGFloat? = nil,
4043
numberOfLines: Int = LabelLayoutDefaults.defaultNumberOfLines,
44+
lineBreakMode: NSLineBreakMode = LabelLayoutDefaults.defaultLineBreakMode,
4145
alignment: Alignment = LabelLayoutDefaults.defaultAlignment,
4246
flexibility: Flexibility = LabelLayoutDefaults.defaultFlexibility,
4347
viewReuseId: String? = nil,
@@ -48,13 +52,15 @@ open class LabelLayout<Label: UILabel>: BaseLayout<Label>, ConfigurableLayout {
4852
self.numberOfLines = numberOfLines
4953
self.font = font
5054
self.lineHeight = lineHeight ?? font.lineHeight
55+
self.lineBreakMode = lineBreakMode
5156
super.init(alignment: alignment, flexibility: flexibility, viewReuseId: viewReuseId, viewClass: viewClass ?? Label.self, config: config)
5257
}
5358

5459
init(string: String,
5560
font: UIFont = LabelLayoutDefaults.defaultFont,
5661
lineHeight: CGFloat? = nil,
5762
numberOfLines: Int = LabelLayoutDefaults.defaultNumberOfLines,
63+
lineBreakMode: NSLineBreakMode = LabelLayoutDefaults.defaultLineBreakMode,
5864
alignment: Alignment = LabelLayoutDefaults.defaultAlignment,
5965
flexibility: Flexibility = LabelLayoutDefaults.defaultFlexibility,
6066
viewReuseId: String? = nil,
@@ -65,6 +71,7 @@ open class LabelLayout<Label: UILabel>: BaseLayout<Label>, ConfigurableLayout {
6571
self.numberOfLines = numberOfLines
6672
self.font = font
6773
self.lineHeight = lineHeight ?? font.lineHeight
74+
self.lineBreakMode = lineBreakMode
6875
super.init(alignment: alignment, flexibility: flexibility, viewReuseId: viewReuseId, viewClass: viewClass ?? Label.self, config: config)
6976
}
7077

@@ -74,6 +81,7 @@ open class LabelLayout<Label: UILabel>: BaseLayout<Label>, ConfigurableLayout {
7481
font: UIFont = LabelLayoutDefaults.defaultFont,
7582
lineHeight: CGFloat? = nil,
7683
numberOfLines: Int = LabelLayoutDefaults.defaultNumberOfLines,
84+
lineBreakMode: NSLineBreakMode = LabelLayoutDefaults.defaultLineBreakMode,
7785
alignment: Alignment = LabelLayoutDefaults.defaultAlignment,
7886
flexibility: Flexibility = LabelLayoutDefaults.defaultFlexibility,
7987
viewReuseId: String? = nil,
@@ -83,6 +91,7 @@ open class LabelLayout<Label: UILabel>: BaseLayout<Label>, ConfigurableLayout {
8391
font: font,
8492
lineHeight: lineHeight,
8593
numberOfLines: numberOfLines,
94+
lineBreakMode: lineBreakMode,
8695
alignment: alignment,
8796
flexibility: flexibility,
8897
viewReuseId: viewReuseId,
@@ -93,6 +102,7 @@ open class LabelLayout<Label: UILabel>: BaseLayout<Label>, ConfigurableLayout {
93102
font: UIFont = LabelLayoutDefaults.defaultFont,
94103
lineHeight: CGFloat? = nil,
95104
numberOfLines: Int = LabelLayoutDefaults.defaultNumberOfLines,
105+
lineBreakMode: NSLineBreakMode = LabelLayoutDefaults.defaultLineBreakMode,
96106
alignment: Alignment = LabelLayoutDefaults.defaultAlignment,
97107
flexibility: Flexibility = LabelLayoutDefaults.defaultFlexibility,
98108
viewReuseId: String? = nil,
@@ -102,6 +112,7 @@ open class LabelLayout<Label: UILabel>: BaseLayout<Label>, ConfigurableLayout {
102112
font: font,
103113
lineHeight: lineHeight,
104114
numberOfLines: numberOfLines,
115+
lineBreakMode: lineBreakMode,
105116
alignment: alignment,
106117
flexibility: flexibility,
107118
viewReuseId: viewReuseId,
@@ -116,7 +127,7 @@ open class LabelLayout<Label: UILabel>: BaseLayout<Label>, ConfigurableLayout {
116127
}
117128

118129
private func textSize(within maxSize: CGSize) -> CGSize {
119-
var size = text.textSize(within: maxSize, font: font)
130+
var size = text.textSize(within: maxSize, font: font, lineBreakMode: lineBreakMode)
120131
if numberOfLines > 0 {
121132
let maxHeight = (CGFloat(numberOfLines) * lineHeight).roundedUpToFractionalPoint
122133
if size.height > maxHeight {
@@ -134,6 +145,7 @@ open class LabelLayout<Label: UILabel>: BaseLayout<Label>, ConfigurableLayout {
134145
open override func configure(view label: Label) {
135146
config?(label)
136147
label.numberOfLines = numberOfLines
148+
label.lineBreakMode = lineBreakMode
137149
label.font = font
138150
switch text {
139151
case .unattributed(let text):
@@ -155,6 +167,7 @@ public class LabelLayoutDefaults {
155167
public static let defaultNumberOfLines = 0
156168
public static let defaultFont = UILabel().font ?? UIFont.systemFont(ofSize: 17)
157169
public static let defaultAlignment = Alignment.topLeading
170+
public static let defaultLineBreakMode = NSLineBreakMode.byTruncatingTail
158171
public static let defaultFlexibility = Flexibility.flexible
159172
}
160173

Sources/ObjCSupport/Builders/LOKLabelLayoutBuilder.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
@property (nonatomic, nonnull, readonly) LOKLabelLayoutBuilder * _Nonnull(^font)(UIFont * _Nullable);
2424
@property (nonatomic, nonnull, readonly) LOKLabelLayoutBuilder * _Nonnull(^numberOfLines)(NSInteger);
25+
@property (nonatomic, nonnull, readonly) LOKLabelLayoutBuilder * _Nonnull(^lineBreakMode)(NSLineBreakMode);
2526
@property (nonatomic, nonnull, readonly) LOKLabelLayoutBuilder * _Nonnull(^lineHeight)(CGFloat);
2627

2728
@property (nonatomic, nonnull, readonly) LOKLabelLayoutBuilder * _Nonnull(^alignment)(LOKAlignment * _Nullable);

Sources/ObjCSupport/Builders/LOKLabelLayoutBuilder.m

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ @interface LOKLabelLayoutBuilder ()
2121
@property (nonatomic, nullable) NSAttributedString *privateAttributedString;
2222
@property (nonatomic, nullable) UIFont *privateFont;
2323
@property (nonatomic) NSInteger privateNumberOfLines;
24+
@property (nonatomic) NSLineBreakMode privateLineBreakMode;
2425
@property (nonatomic) CGFloat privateLineHeight;
2526
@property (nonatomic, nullable) void (^ privateConfigure)(UILabel * _Nonnull);
2627

@@ -87,6 +88,13 @@ - (nonnull LOKLabelLayout *)layout {
8788
};
8889
}
8990

91+
- (LOKLabelLayoutBuilder * _Nonnull (^)(NSLineBreakMode))lineBreakMode {
92+
return ^LOKLabelLayoutBuilder *(NSLineBreakMode lineBreakMode){
93+
self.privateLineBreakMode = lineBreakMode;
94+
return self;
95+
};
96+
}
97+
9098
- (LOKLabelLayoutBuilder * _Nonnull (^)(CGFloat))lineHeight {
9199
return ^LOKLabelLayoutBuilder *(CGFloat lineHeight){
92100
self.privateLineHeight = lineHeight;

Sources/ObjCSupport/LOKLabelLayout.swift

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ import UIKit
1313
@objc public let string: String?
1414
@objc public let lineHeight: CGFloat
1515
@objc public let font: UIFont
16+
@objc public let lineBreakMode: NSLineBreakMode?
1617
@objc public let numberOfLines: Int
1718
@objc public let alignment: LOKAlignment
1819
@objc public let viewClass: UILabel.Type
1920
@objc public let configure: ((UILabel) -> Void)?
2021

22+
// TODO: Remove this init once all consumers are switch to `init(attributedString:font:lineBreakMode:lineHeight:numberOfLines:alignment:flexibility:viewReuseId:viewClass:configure:)`
2123
@objc public init(attributedString: NSAttributedString,
2224
font: UIFont?,
2325
lineHeight: CGFloat,
@@ -29,6 +31,7 @@ import UIKit
2931
configure: ((UILabel) -> Void)?) {
3032
self.attributedString = attributedString
3133
self.font = font ?? UIFont.systemFont(ofSize: UIFont.systemFontSize)
34+
self.lineBreakMode = nil
3235
self.lineHeight = lineHeight
3336
self.numberOfLines = numberOfLines
3437
self.alignment = alignment ?? .topLeading
@@ -49,6 +52,7 @@ import UIKit
4952
super.init(layout: layout)
5053
}
5154

55+
// TODO: Remove this init once all consumers are switch to `init(string:font:lineBreakMode:lineHeight:numberOfLines:alignment:flexibility:viewReuseId:viewClass:configure:)`
5256
@objc public init(string: String,
5357
font: UIFont?,
5458
lineHeight: CGFloat,
@@ -60,6 +64,7 @@ import UIKit
6064
configure: ((UILabel) -> Void)?) {
6165
self.string = string
6266
self.font = font ?? UIFont.systemFont(ofSize: UIFont.systemFontSize)
67+
self.lineBreakMode = nil
6368
self.lineHeight = lineHeight
6469
self.numberOfLines = numberOfLines
6570
self.alignment = alignment ?? .topLeading
@@ -78,4 +83,71 @@ import UIKit
7883
config: self.configure)
7984
super.init(layout: layout)
8085
}
86+
87+
@objc public init(attributedString: NSAttributedString,
88+
font: UIFont?,
89+
lineBreakMode: NSlineBreakMode?,
90+
lineHeight: CGFloat,
91+
numberOfLines: Int,
92+
alignment: LOKAlignment?,
93+
flexibility: LOKFlexibility?,
94+
viewReuseId: String?,
95+
viewClass: UILabel.Type?,
96+
configure: ((UILabel) -> Void)?) {
97+
self.attributedString = attributedString
98+
self.font = font ?? UIFont.systemFont(ofSize: UIFont.systemFontSize)
99+
self.lineBreakMode = lineBreakMode
100+
self.lineHeight = lineHeight
101+
self.numberOfLines = numberOfLines
102+
self.alignment = alignment ?? .topLeading
103+
self.viewClass = viewClass ?? UILabel.self
104+
self.configure = configure
105+
string = nil
106+
let layout = LabelLayout<UILabel>(
107+
attributedString: attributedString,
108+
font: self.font,
109+
lineHeight: lineHeight > 0 && lineHeight.isFinite ? lineHeight : Optional<CGFloat>.none,
110+
numberOfLines: self.numberOfLines,
111+
lineBreakMode: self.lineBreakMode ?? .byTruncatingTail,
112+
alignment: self.alignment.alignment,
113+
flexibility: flexibility?.flexibility ?? .flexible,
114+
viewReuseId: viewReuseId,
115+
viewClass: self.viewClass,
116+
config: self.configure)
117+
118+
super.init(layout: layout)
119+
}
120+
121+
@objc public init(string: String,
122+
font: UIFont?,
123+
lineBreakMode: NSlineBreakMode?,
124+
lineHeight: CGFloat,
125+
numberOfLines: Int,
126+
alignment: LOKAlignment?,
127+
flexibility: LOKFlexibility?,
128+
viewReuseId: String?,
129+
viewClass: UILabel.Type?,
130+
configure: ((UILabel) -> Void)?) {
131+
self.string = string
132+
self.font = font ?? UIFont.systemFont(ofSize: UIFont.systemFontSize)
133+
self.lineBreakMode = lineBreakMode
134+
self.lineHeight = lineHeight
135+
self.numberOfLines = numberOfLines
136+
self.alignment = alignment ?? .topLeading
137+
self.viewClass = viewClass ?? UILabel.self
138+
self.configure = configure
139+
attributedString = nil
140+
let layout = LabelLayout<UILabel>(
141+
string: string,
142+
font: self.font,
143+
lineHeight: lineHeight > 0 && lineHeight.isFinite ? lineHeight : Optional<CGFloat>.none,
144+
numberOfLines: self.numberOfLines,
145+
lineBreakMode: self.lineBreakMode ?? .byTruncatingTail,
146+
alignment: self.alignment.alignment,
147+
flexibility: flexibility?.flexibility ?? .flexible,
148+
viewReuseId: viewReuseId,
149+
viewClass: self.viewClass,
150+
config: self.configure)
151+
super.init(layout: layout)
152+
}
81153
}

Sources/Text.swift

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,47 @@ public enum Text {
1313
case unattributed(String)
1414
case attributed(NSAttributedString)
1515

16-
/// Calculate the text size within `maxSize` by given `UIFont`
16+
/// Calculate the text size within `maxSize` by given `UIFont` and `NSLineBreakMode`
1717
func textSize(within maxSize: CGSize,
18-
font: UIFont) -> CGSize {
18+
font: UIFont,
19+
lineBreakMode: NSLineBreakMode = .byTruncatingTail) -> CGSize {
1920
let options: NSStringDrawingOptions = [
2021
.usesLineFragmentOrigin
2122
]
2223

24+
/**
25+
By default `boundingRect(with:options:attributes:)` seems to be using `lineBreakMode=byWordWrapping` to calculate size,
26+
so if UILabel/UITextView's mode is char wrapping, we get more height than required.
27+
So use `paragraphStyle` only in case when UILabel/UITextView's mode is `byCharWrapping` because if we use `paragraphStyle`
28+
with `byTruncatingTail`, `boundingRect(with:options:attributes:)` always give single line height.
29+
*/
30+
var attributes: [NSAttributedStringKey: Any] = [NSAttributedStringKey.font: font]
31+
if lineBreakMode == .byCharWrapping {
32+
let paragraphStyle = NSMutableParagraphStyle()
33+
paragraphStyle.lineBreakMode = lineBreakMode
34+
attributes[NSAttributedStringKey.paragraphStyle] = paragraphStyle
35+
}
36+
2337
let size: CGSize
2438
switch self {
2539
case .attributed(let attributedText):
2640
if attributedText.length == 0 {
2741
return .zero
2842
}
2943

30-
// UILabel/UITextView uses a default font if one is not specified in the attributed string.
44+
// UILabel/UITextView uses a default font and lineBreakMode if one is not specified in the attributed string.
3145
// boundingRect(with:options:attributes:) does not appear to have the same logic,
32-
// so we need to ensure that our attributed string has a default font.
33-
// We do this by creating a new attributed string with the default font and then
46+
// so we need to ensure that our attributed string has a default font and lineBreakMode.
47+
// We do this by creating a new attributed string with the default font & lineBreakMode and then
3448
// applying all of the attributes from the provided attributed string.
35-
let fontAppliedAttributeString = attributedText.with(font: font)
49+
let newAttributedString = attributedText.with(additionalAttributes: attributes)
3650

37-
size = fontAppliedAttributeString.boundingRect(with: maxSize, options: options, context: nil).size
51+
size = newAttributedString.boundingRect(with: maxSize, options: options, context: nil).size
3852
case .unattributed(let text):
3953
if text.isEmpty {
4054
return .zero
4155
}
42-
size = text.boundingRect(with: maxSize, options: options, attributes: [NSAttributedStringKey.font: font], context: nil).size
56+
size = text.boundingRect(with: maxSize, options: options, attributes: attributes, context: nil).size
4357
}
4458
// boundingRect(with:options:attributes:) returns size to a precision of hundredths of a point,
4559
// but UILabel only returns sizes with a point precision of 1/screenDensity.

0 commit comments

Comments
 (0)