Skip to content

Commit 758fa6f

Browse files
committed
Faster text sizing
1 parent 09f7862 commit 758fa6f

File tree

3 files changed

+65
-1
lines changed

3 files changed

+65
-1
lines changed

Sources/UIComponent/Components/View/Text.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@ public enum TextContent {
1515
case attributedString(NSAttributedString)
1616
}
1717

18+
/// A shared UILabel instance used for sizing text when `useSharedLabelForSizing` is true.
19+
private let layoutLabel = UILabel()
20+
1821
/// A `Text` component represents a piece of text with styling and layout information.
1922
/// It can be initialized with either a plain `String` or an `NSAttributedString`.
2023
/// It also supports the new swift `AttributedString` from iOS 15 for more complex styling.
2124
public struct Text: Component {
25+
/// A flag to determine if a shared UILabel should be used for sizing.
26+
static var useSharedLabelForSizing = true
27+
2228
/// Environment-injected font used when rendering plain strings.
2329
@Environment(\.font) var font
2430
/// Environment-injected text color used when rendering plain strings.
@@ -110,6 +116,25 @@ public struct Text: Component {
110116
case .attributedString(let string):
111117
attributedString = string
112118
}
119+
if Self.useSharedLabelForSizing, Thread.isMainThread {
120+
layoutLabel.numberOfLines = numberOfLines
121+
layoutLabel.lineBreakMode = lineBreakMode
122+
switch content {
123+
case .string(let string):
124+
layoutLabel.font = font
125+
layoutLabel.text = string
126+
case .attributedString(let string):
127+
layoutLabel.font = nil
128+
layoutLabel.attributedText = string
129+
}
130+
let size = layoutLabel.sizeThatFits(constraint.maxSize)
131+
return TextRenderNode(
132+
attributedString: attributedString,
133+
numberOfLines: numberOfLines,
134+
lineBreakMode: lineBreakMode,
135+
size: size.bound(to: constraint)
136+
)
137+
}
113138
if numberOfLines != 0 || isSwiftAttributedString {
114139
// Slower route
115140
//

Tests/UIComponentTests/LazyLayoutTest.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,45 @@ struct LazyLayoutTests {
9797
#expect(fixedSizeLayoutTime * 2 < rawLayoutTime)
9898
}
9999

100+
@Test func testPerfImprovment() throws {
101+
Text.useSharedLabelForSizing = false
102+
let oldLayoutTime = measureTime {
103+
VStack {
104+
for _ in 0..<10000 {
105+
Text(text1)
106+
}
107+
}
108+
}
109+
let oldLayoutTimeWithNumberOfLines = measureTime {
110+
VStack {
111+
for _ in 0..<10000 {
112+
Text(text1, numberOfLines: 1)
113+
}
114+
}
115+
}
116+
Text.useSharedLabelForSizing = true
117+
let newLayoutTime = measureTime {
118+
VStack {
119+
for _ in 0..<10000 {
120+
Text(text1)
121+
}
122+
}
123+
}
124+
let newLayoutTimeWithNumberOfLines = measureTime {
125+
VStack {
126+
for _ in 0..<10000 {
127+
Text(text1, numberOfLines: 1)
128+
}
129+
}
130+
}
131+
print("Layout 10000 text with old method used \(oldLayoutTime)s.")
132+
print("Layout 10000 text with new method used \(newLayoutTime)s.")
133+
print("Layout 10000 text with old method used \(oldLayoutTimeWithNumberOfLines)s with number of lines.")
134+
print("Layout 10000 text with new method used \(newLayoutTimeWithNumberOfLines)s with number of lines.")
135+
#expect(newLayoutTime < oldLayoutTime)
136+
#expect(newLayoutTimeWithNumberOfLines < oldLayoutTimeWithNumberOfLines)
137+
}
138+
100139
func measureTime(_ component: () -> any Component) -> TimeInterval {
101140
let view = UIView()
102141
let startTime = CACurrentMediaTime()

Tests/UIComponentTests/UIComponentTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import XCTest
44

55
@testable import UIComponent
66

7-
let text1 = "This is a test of layout methods"
7+
let text1 = "This is a test of layout methods, performance, and correctness."
88
let text2 = "This is a test of layout"
99
let font = UIFont.systemFont(ofSize: 16)
1010
let maxSize = CGSize(width: 100, height: CGFloat.infinity)

0 commit comments

Comments
 (0)