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

Commit 089149f

Browse files
committed
HUED-8605 Test & Fix Potential truncation issue in LabelLayout
- Added LabelLayout unit test to verify that size calculation logic considers line break mode while calculating LabelLayout size. - Added line break mode support in LabelLayout to calculate correct size based on line break mode.
1 parent dc2ff66 commit 089149f

File tree

7 files changed

+155
-19
lines changed

7 files changed

+155
-19
lines changed

LayoutKitTests/LabelLayoutTests.swift

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ import LayoutKit
1111

1212
class LabelLayoutTests: XCTestCase {
1313

14+
// For the defined `sampleText` and `labelLayoutMaxWidth` combination, `LabelLayout` requires 2 line of
15+
// text for char-wrapping and 3 lines for word-wrapping/truncating-tail.
16+
// So don't change this combination as the following tests are based on these values:
17+
// - testSizeCalculationWithDifferentLineBreakMode
18+
// - testAttributedTextSizeCalculationWithDifferentLineBreakMode
19+
private static let sampleText = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean comm"
20+
private static let labelLayoutMaxWidth = 305
21+
1422
func testNeedsView() {
1523
let l = LabelLayout(text: "hi").arrangement().makeViews()
1624
XCTAssertNotNil(l as? UILabel)
@@ -200,10 +208,78 @@ class LabelLayoutTests: XCTestCase {
200208
let maxSize = CGSize(width: 17, height: .max)
201209
XCTAssertEqual(layout.measurement(within: maxSize).size, label.sizeThatFits(maxSize))
202210
}
211+
212+
func testSizeCalculationWithDifferentLineBreakMode() {
213+
// Use different line break mode for `LabelLayout` and dummy label.
214+
// So in this case, size calculation should not match with dummy label's size calculation.
215+
let label = UILabel(text: LabelLayoutTests.sampleText, numberOfLines: 0, lineBreakMode: .byCharWrapping)
216+
let layout = LabelLayout(text: LabelLayoutTests.sampleText)
217+
let maxSize = CGSize(width: LabelLayoutTests.labelLayoutMaxWidth, height: .max)
218+
XCTAssertNotEqual(layout.measurement(within: maxSize).size, label.sizeThatFits(maxSize))
219+
}
220+
221+
func testAttributedTextSizeCalculationWithDifferentLineBreakMode() {
222+
// Use different line break mode for `LabelLayout` and dummy label.
223+
// So in this case, size calculation should not match with dummy label's size calculation.
224+
let attributedText = NSAttributedString(string: LabelLayoutTests.sampleText)
225+
let label = UILabel(attributedText: attributedText, numberOfLines: 0, lineBreakMode: .byCharWrapping)
226+
let layout = LabelLayout(attributedText: attributedText)
227+
let maxSize = CGSize(width: LabelLayoutTests.labelLayoutMaxWidth, height: .max)
228+
XCTAssertNotEqual(layout.measurement(within: maxSize).size, label.sizeThatFits(maxSize))
229+
}
230+
231+
func testTextSizeCalculationWithSameLineBreakMode() {
232+
// Use same line break mode for `LabelLayout` and dummy label and then match LabelLayout's size with dummy label's size calculation.
233+
let lineBreakingModes: [NSLineBreakMode] = [
234+
.byWordWrapping,
235+
.byCharWrapping,
236+
.byClipping,
237+
.byTruncatingHead,
238+
.byTruncatingTail,
239+
.byTruncatingMiddle
240+
]
241+
242+
lineBreakingModes.forEach { (lineBreakMode) in
243+
verifyTextSizeCalculation(with: LabelLayoutTests.sampleText, lineBreakMode: lineBreakMode)
244+
}
245+
}
246+
247+
func testAttributedTextSizeCalculationWithSameLineBreakMode() {
248+
// Use same line break mode for `LabelLayout` and dummy label and then match LabelLayout's size with dummy label's size calculation.
249+
let lineBreakingModes: [NSLineBreakMode] = [
250+
.byWordWrapping,
251+
.byCharWrapping,
252+
.byClipping,
253+
.byTruncatingHead,
254+
.byTruncatingTail,
255+
.byTruncatingMiddle
256+
]
257+
258+
let attributedText = NSAttributedString(string: LabelLayoutTests.sampleText)
259+
lineBreakingModes.forEach { (lineBreakMode) in
260+
verifyAttributedTextSizeCalculation(with: attributedText, lineBreakMode: lineBreakMode)
261+
}
262+
}
263+
264+
// MARK: Private Helpers
265+
266+
private func verifyTextSizeCalculation(with text: String, lineBreakMode: NSLineBreakMode) {
267+
let label = UILabel(text: text, numberOfLines: 0, lineBreakMode: lineBreakMode)
268+
let layout = LabelLayout(text: text, lineBreakMode: lineBreakMode)
269+
let maxSize = CGSize(width: LabelLayoutTests.labelLayoutMaxWidth, height: .max)
270+
XCTAssertEqual(layout.measurement(within: maxSize).size, label.sizeThatFits(maxSize))
271+
}
272+
273+
private func verifyAttributedTextSizeCalculation(with attributedText: NSAttributedString, lineBreakMode: NSLineBreakMode) {
274+
let label = UILabel(attributedText: attributedText, numberOfLines: 0, lineBreakMode: lineBreakMode)
275+
let layout = LabelLayout(attributedText: attributedText, lineBreakMode: lineBreakMode)
276+
let maxSize = CGSize(width: LabelLayoutTests.labelLayoutMaxWidth, height: .max)
277+
XCTAssertEqual(layout.measurement(within: maxSize).size, label.sizeThatFits(maxSize))
278+
}
203279
}
204280

205281
extension UILabel {
206-
convenience init(text: String, font: UIFont? = nil, numberOfLines: Int? = nil) {
282+
convenience init(text: String, font: UIFont? = nil, numberOfLines: Int? = nil, lineBreakMode: NSLineBreakMode? = nil) {
207283
self.init()
208284
self.text = text
209285
if let font = font {
@@ -212,16 +288,23 @@ extension UILabel {
212288
if let numberOfLines = numberOfLines {
213289
self.numberOfLines = numberOfLines
214290
}
291+
if let lineBreakMode = lineBreakMode {
292+
self.lineBreakMode = lineBreakMode
293+
}
215294
}
216295

217-
convenience init(attributedText: NSAttributedString, font: UIFont? = nil, numberOfLines: Int? = nil) {
296+
convenience init(attributedText: NSAttributedString, font: UIFont? = nil, numberOfLines: Int? = nil, lineBreakMode: NSLineBreakMode? = nil) {
218297
self.init()
219298
if let font = font {
220299
self.font = font
221300
}
222301
if let numberOfLines = numberOfLines {
223302
self.numberOfLines = numberOfLines
224303
}
304+
if let lineBreakMode = lineBreakMode {
305+
self.lineBreakMode = lineBreakMode
306+
}
307+
225308
// Want to set attributed text AFTER font it set, otherwise the font seems to take precedence.
226309
self.attributedText = attributedText
227310
}

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: 12 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

@@ -31,12 +32,14 @@ @implementation LOKLabelLayoutBuilder
3132
- (instancetype)initWithString:(NSString *)string {
3233
self = [super init];
3334
_privateString = string;
35+
_privateLineBreakMode = NSLineBreakByTruncatingTail;
3436
return self;
3537
}
3638

3739
- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString {
3840
self = [super init];
3941
_privateAttributedString = attributedString;
42+
_privateLineBreakMode = NSLineBreakByTruncatingTail;
4043
return self;
4144
}
4245

@@ -53,6 +56,7 @@ - (nonnull LOKLabelLayout *)layout {
5356
if (self.privateAttributedString) {
5457
return [[LOKLabelLayout alloc] initWithAttributedString:self.privateAttributedString
5558
font:self.privateFont
59+
lineBreakMode:self.privateLineBreakMode
5660
lineHeight:self.privateLineHeight
5761
numberOfLines:self.privateNumberOfLines
5862
alignment:self.privateAlignment
@@ -63,6 +67,7 @@ - (nonnull LOKLabelLayout *)layout {
6367
} else {
6468
return [[LOKLabelLayout alloc] initWithString:self.privateString
6569
font:self.privateFont
70+
lineBreakMode:self.privateLineBreakMode
6671
lineHeight:self.privateLineHeight
6772
numberOfLines:self.privateNumberOfLines
6873
alignment:self.privateAlignment
@@ -87,6 +92,13 @@ - (nonnull LOKLabelLayout *)layout {
8792
};
8893
}
8994

95+
- (LOKLabelLayoutBuilder * _Nonnull (^)(NSLineBreakMode))lineBreakMode {
96+
return ^LOKLabelLayoutBuilder *(NSLineBreakMode lineBreakMode){
97+
self.privateLineBreakMode = lineBreakMode;
98+
return self;
99+
};
100+
}
101+
90102
- (LOKLabelLayoutBuilder * _Nonnull (^)(CGFloat))lineHeight {
91103
return ^LOKLabelLayoutBuilder *(CGFloat lineHeight){
92104
self.privateLineHeight = lineHeight;

Sources/ObjCSupport/LOKLabelLayout.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ 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

2122
@objc public init(attributedString: NSAttributedString,
2223
font: UIFont?,
24+
lineBreakMode: NSLineBreakMode,
2325
lineHeight: CGFloat,
2426
numberOfLines: Int,
2527
alignment: LOKAlignment?,
@@ -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 = lineBreakMode
3235
self.lineHeight = lineHeight
3336
self.numberOfLines = numberOfLines
3437
self.alignment = alignment ?? .topLeading
@@ -40,6 +43,7 @@ import UIKit
4043
font: self.font,
4144
lineHeight: lineHeight > 0 && lineHeight.isFinite ? lineHeight : Optional<CGFloat>.none,
4245
numberOfLines: self.numberOfLines,
46+
lineBreakMode: self.lineBreakMode,
4347
alignment: self.alignment.alignment,
4448
flexibility: flexibility?.flexibility ?? .flexible,
4549
viewReuseId: viewReuseId,
@@ -51,6 +55,7 @@ import UIKit
5155

5256
@objc public init(string: String,
5357
font: UIFont?,
58+
lineBreakMode: NSLineBreakMode,
5459
lineHeight: CGFloat,
5560
numberOfLines: Int,
5661
alignment: LOKAlignment?,
@@ -60,6 +65,7 @@ import UIKit
6065
configure: ((UILabel) -> Void)?) {
6166
self.string = string
6267
self.font = font ?? UIFont.systemFont(ofSize: UIFont.systemFontSize)
68+
self.lineBreakMode = lineBreakMode
6369
self.lineHeight = lineHeight
6470
self.numberOfLines = numberOfLines
6571
self.alignment = alignment ?? .topLeading
@@ -71,6 +77,7 @@ import UIKit
7177
font: self.font,
7278
lineHeight: lineHeight > 0 && lineHeight.isFinite ? lineHeight : Optional<CGFloat>.none,
7379
numberOfLines: self.numberOfLines,
80+
lineBreakMode: self.lineBreakMode,
7481
alignment: self.alignment.alignment,
7582
flexibility: flexibility?.flexibility ?? .flexible,
7683
viewReuseId: viewReuseId,

0 commit comments

Comments
 (0)