Skip to content

Commit ade2d1c

Browse files
authored
Merge pull request #31 from patrickkabwe/perf/ios-setFragments-optimizations
perf(iOS): Optimize NitroText fragment rendering and performance
2 parents 4fa85a1 + 4189b31 commit ade2d1c

8 files changed

Lines changed: 198 additions & 119 deletions

ios/HybridNitroText.swift

Lines changed: 28 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class HybridNitroText: HybridNitroTextSpec, NitroTextViewDelegate {
1212
private let textView = NitroTextView()
1313
var view: UIView { textView }
1414
let nitroTextImpl: NitroTextImpl
15+
private var needsApply: Bool = false
1516

1617
override init() {
1718
self.nitroTextImpl = NitroTextImpl(textView)
@@ -30,9 +31,7 @@ class HybridNitroText: HybridNitroTextSpec, NitroTextViewDelegate {
3031
// Props
3132

3233
var fragments: [Fragment]? {
33-
didSet {
34-
applyFragmentsAndProps()
35-
}
34+
didSet { markNeedsApply() }
3635
}
3736

3837
var selectable: Bool? {
@@ -44,7 +43,7 @@ class HybridNitroText: HybridNitroTextSpec, NitroTextViewDelegate {
4443
var allowFontScaling: Bool? {
4544
didSet {
4645
nitroTextImpl.setAllowFontScaling(allowFontScaling)
47-
applyFragmentsAndProps()
46+
markNeedsApply()
4847
}
4948
}
5049

@@ -56,72 +55,60 @@ class HybridNitroText: HybridNitroTextSpec, NitroTextViewDelegate {
5655
var fontSize: Double? {
5756
didSet {
5857
nitroTextImpl.setFontSize(fontSize)
59-
applyFragmentsAndProps()
58+
markNeedsApply()
6059
}
6160
}
6261

6362
var fontWeight: FontWeight? {
64-
didSet {
65-
applyFragmentsAndProps()
66-
}
63+
didSet { markNeedsApply() }
6764
}
6865

6966
var fontColor: String? {
7067
didSet {
7168
textView.textColor = ColorParser.parse(fontColor)
72-
applyFragmentsAndProps()
69+
markNeedsApply()
7370
}
7471
}
7572

7673
var fragmentBackgroundColor: String? {
77-
didSet {
78-
applyFragmentsAndProps()
79-
}
74+
didSet { markNeedsApply() }
8075
}
8176

8277
var fontStyle: FontStyle? {
83-
didSet {
84-
applyFragmentsAndProps()
85-
}
78+
didSet { markNeedsApply() }
8679
}
8780

8881
var fontFamily: String? {
8982
didSet {
9083
nitroTextImpl.setFontFamily(fontFamily)
91-
applyFragmentsAndProps()
84+
markNeedsApply()
9285
}
9386
}
9487

9588
var textAlign: TextAlign? {
9689
didSet {
9790
nitroTextImpl.setTextAlign(textAlign)
98-
applyFragmentsAndProps()
91+
markNeedsApply()
9992
}
10093
}
10194

10295
var textTransform: TextTransform? {
10396
didSet {
10497
nitroTextImpl.setTextTransform(textTransform)
105-
applyFragmentsAndProps()
98+
markNeedsApply()
10699
}
107100
}
108101

109102
var textDecorationLine: TextDecorationLine? {
110-
didSet {
111-
applyFragmentsAndProps()
112-
}
103+
didSet { markNeedsApply() }
113104
}
114105

115106
var textDecorationColor: String? {
116-
didSet {
117-
applyFragmentsAndProps()
118-
}
107+
didSet { markNeedsApply() }
119108
}
120109

121110
var textDecorationStyle: TextDecorationStyle? {
122-
didSet {
123-
applyFragmentsAndProps()
124-
}
111+
didSet { markNeedsApply() }
125112
}
126113

127114
var selectionColor: String? {
@@ -133,21 +120,15 @@ class HybridNitroText: HybridNitroTextSpec, NitroTextViewDelegate {
133120
}
134121

135122
var lineHeight: Double? {
136-
didSet {
137-
applyFragmentsAndProps()
138-
}
123+
didSet { markNeedsApply() }
139124
}
140125

141126
var letterSpacing: Double? {
142-
didSet {
143-
applyFragmentsAndProps()
144-
}
127+
didSet { markNeedsApply() }
145128
}
146129

147130
var text: String? {
148-
didSet {
149-
applyFragmentsAndProps()
150-
}
131+
didSet { markNeedsApply() }
151132
}
152133

153134
var numberOfLines: Double? {
@@ -165,35 +146,35 @@ class HybridNitroText: HybridNitroTextSpec, NitroTextViewDelegate {
165146
var dynamicTypeRamp: DynamicTypeRamp? {
166147
didSet {
167148
nitroTextImpl.setDynamicTypeRamp(dynamicTypeRamp)
168-
applyFragmentsAndProps()
149+
markNeedsApply()
169150
}
170151
}
171152

172153
var lineBreakStrategyIOS: LineBreakStrategyIOS? {
173154
didSet {
174155
nitroTextImpl.setLineBreakStrategyIOS(lineBreakStrategyIOS)
175-
applyFragmentsAndProps()
156+
markNeedsApply()
176157
}
177158
}
178159

179160
var maxFontSizeMultiplier: Double? {
180161
didSet {
181162
nitroTextImpl.setMaxFontSizeMultiplier(maxFontSizeMultiplier)
182-
applyFragmentsAndProps()
163+
markNeedsApply()
183164
}
184165
}
185166

186167
var adjustsFontSizeToFit: Bool? {
187168
didSet {
188169
nitroTextImpl.setAdjustsFontSizeToFit(adjustsFontSizeToFit)
189-
applyFragmentsAndProps()
170+
markNeedsApply()
190171
}
191172
}
192173

193174
var minimumFontScale: Double? {
194175
didSet {
195176
nitroTextImpl.setMinimumFontScale(minimumFontScale)
196-
applyFragmentsAndProps()
177+
markNeedsApply()
197178
}
198179
}
199180

@@ -218,6 +199,12 @@ class HybridNitroText: HybridNitroTextSpec, NitroTextViewDelegate {
218199
}
219200

220201
func afterUpdate() {
202+
if needsApply {
203+
applyFragmentsAndProps()
204+
needsApply = false
205+
}
221206
textView.setNeedsLayout()
222207
}
208+
209+
private func markNeedsApply() { needsApply = true }
223210
}

ios/NitroTextImpl+Attributes.swift

Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ extension NitroTextImpl {
1212
for fragment: Fragment,
1313
defaultColor: UIColor
1414
) -> [NSAttributedString.Key: Any] {
15-
var attrs: [NSAttributedString.Key: Any] = [:]
15+
var attrs: [NSAttributedString.Key: Any] = Dictionary(minimumCapacity: 8)
1616

1717
let font = makeFont(for: fragment, defaultPointSize: nitroTextView?.font?.pointSize)
1818
attrs[.font] = font.value
@@ -35,14 +35,15 @@ extension NitroTextImpl {
3535

3636
// Underline / Strikethrough from textDecorationLine
3737
if let deco = fragment.textDecorationLine {
38+
let styleRaw = nsUnderlineStyle(from: fragment.textDecorationStyle)
3839
switch deco {
3940
case .underline:
40-
attrs[.underlineStyle] = nsUnderlineStyle(from: fragment.textDecorationStyle)
41+
attrs[.underlineStyle] = styleRaw
4142
case .lineThrough:
42-
attrs[.strikethroughStyle] = nsUnderlineStyle(from: fragment.textDecorationStyle)
43+
attrs[.strikethroughStyle] = styleRaw
4344
case .underlineLineThrough:
44-
attrs[.underlineStyle] = nsUnderlineStyle(from: fragment.textDecorationStyle)
45-
attrs[.strikethroughStyle] = nsUnderlineStyle(from: fragment.textDecorationStyle)
45+
attrs[.underlineStyle] = styleRaw
46+
attrs[.strikethroughStyle] = styleRaw
4647
case .none:
4748
break
4849
}
@@ -56,39 +57,7 @@ extension NitroTextImpl {
5657

5758
return attrs
5859
}
59-
60-
func makeParagraphStyle(for fragment: Fragment) -> NSMutableParagraphStyle {
61-
let para = NSMutableParagraphStyle()
62-
63-
if let lineHeight = fragment.lineHeight, lineHeight > 0 {
64-
let baseSize: CGFloat = {
65-
if let fs = fragment.fontSize { return CGFloat(fs) }
66-
return nitroTextView?.font?.pointSize ?? CGFloat(14.0)
67-
}()
68-
let fontScaleMultiplier = allowFontScaling ? getScaleFactor(requestedSize: baseSize) : 1.0
69-
let lh = CGFloat(lineHeight) * fontScaleMultiplier
70-
para.minimumLineHeight = lh
71-
para.maximumLineHeight = lh
72-
}
73-
74-
if let align = fragment.textAlign {
75-
switch align {
76-
case .center: para.alignment = .center
77-
case .right: para.alignment = .right
78-
case .justify: para.alignment = .justified
79-
case .left: para.alignment = .left
80-
case .auto: para.alignment = .natural
81-
}
82-
} else {
83-
para.alignment = currentTextAlignment
84-
}
85-
86-
if #available(iOS 14.0, *), let _ = nitroTextView {
87-
para.lineBreakStrategy = currentLineBreakStrategy
88-
}
89-
return para
90-
}
91-
60+
9261
func resolveColor(for fragment: Fragment, defaultColor: UIColor) -> UIColor {
9362
if let value = fragment.fontColor, let parsed = ColorParser.parse(value) {
9463
return parsed
@@ -111,7 +80,7 @@ extension NitroTextImpl {
11180
}
11281

11382
func transform(_ text: String, with fragment: Fragment) -> String {
114-
let effective: TextTransform = {
83+
let textTransform: TextTransform = {
11584
if let ft = fragment.textTransform {
11685
switch ft {
11786
case .uppercase: return .uppercase
@@ -123,7 +92,7 @@ extension NitroTextImpl {
12392
return currentTransform
12493
}()
12594

126-
switch effective {
95+
switch textTransform {
12796
case .uppercase: return text.uppercased()
12897
case .lowercase: return text.lowercased()
12998
case .capitalize: return text.capitalized

ios/NitroTextImpl+Fragment.swift

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,27 +52,63 @@ extension NitroTextImpl {
5252
return
5353
}
5454

55+
if !hasApplicableTop(top), fragments.allSatisfy({ $0.text != nil }) {
56+
setFragments(fragments)
57+
return
58+
}
59+
5560
var merged: [Fragment] = []
5661
merged.reserveCapacity(fragments.count)
5762

5863
for var frag in fragments {
59-
if frag.text == nil { frag.text = "" }
60-
if frag.fontSize == nil, let v = top.fontSize { frag.fontSize = v }
61-
if frag.fontWeight == nil, let v = top.fontWeight { frag.fontWeight = v }
62-
if frag.fontStyle == nil, let v = top.fontStyle { frag.fontStyle = v }
63-
if frag.fontFamily == nil, let v = top.fontFamily, !v.isEmpty { frag.fontFamily = v }
64-
if frag.lineHeight == nil, let v = top.lineHeight, v > 0 { frag.lineHeight = v }
65-
if frag.letterSpacing == nil, let v = top.letterSpacing { frag.letterSpacing = v }
66-
if frag.fontColor == nil, let v = top.fontColor, !v.isEmpty { frag.fontColor = v }
67-
if frag.selectionColor == nil, let v = top.selectionColor, !v.isEmpty { frag.selectionColor = v }
68-
if frag.textAlign == nil, let v = top.textAlign { frag.textAlign = v }
69-
if frag.textTransform == nil, let v = top.textTransform { frag.textTransform = v }
70-
if frag.textDecorationLine == nil, let v = top.textDecorationLine { frag.textDecorationLine = v }
71-
if frag.textDecorationColor == nil, let v = top.textDecorationColor, !v.isEmpty { frag.textDecorationColor = v }
72-
if frag.textDecorationStyle == nil, let v = top.textDecorationStyle { frag.textDecorationStyle = v }
64+
mergeTop(into: &frag, with: top)
7365
merged.append(frag)
7466
}
7567
setFragments(merged)
7668
}
7769

7870
}
71+
72+
// MARK: - Merge helpers
73+
private extension NitroTextImpl {
74+
@inline(__always)
75+
func hasApplicableTop(_ top: FragmentTopDefaults) -> Bool {
76+
if top.fontSize != nil { return true }
77+
if top.fontWeight != nil { return true }
78+
if let s = top.fontColor, !s.isEmpty { return true }
79+
if top.fontStyle != nil { return true }
80+
if let s = top.fontFamily, !s.isEmpty { return true }
81+
if let v = top.lineHeight, v > 0 { return true }
82+
if top.letterSpacing != nil { return true }
83+
if top.textAlign != nil { return true }
84+
if top.textTransform != nil { return true }
85+
if top.textDecorationLine != nil { return true }
86+
if let s = top.textDecorationColor, !s.isEmpty { return true }
87+
if top.textDecorationStyle != nil { return true }
88+
if let s = top.selectionColor, !s.isEmpty { return true }
89+
return false
90+
}
91+
92+
@inline(__always)
93+
func mergeTop(into frag: inout Fragment, with top: FragmentTopDefaults) {
94+
if frag.text == nil { frag.text = "" }
95+
96+
if frag.fontSize == nil, let v = top.fontSize { frag.fontSize = v }
97+
if frag.fontWeight == nil, let v = top.fontWeight { frag.fontWeight = v }
98+
if frag.fontStyle == nil, let v = top.fontStyle { frag.fontStyle = v }
99+
100+
if frag.fontFamily == nil, let v = top.fontFamily, !v.isEmpty { frag.fontFamily = v }
101+
if frag.lineHeight == nil, let v = top.lineHeight, v > 0 { frag.lineHeight = v }
102+
if frag.letterSpacing == nil, let v = top.letterSpacing { frag.letterSpacing = v }
103+
104+
if frag.fontColor == nil, let v = top.fontColor, !v.isEmpty { frag.fontColor = v }
105+
if frag.selectionColor == nil, let v = top.selectionColor, !v.isEmpty { frag.selectionColor = v }
106+
107+
if frag.textAlign == nil, let v = top.textAlign { frag.textAlign = v }
108+
if frag.textTransform == nil, let v = top.textTransform { frag.textTransform = v }
109+
110+
if frag.textDecorationLine == nil, let v = top.textDecorationLine { frag.textDecorationLine = v }
111+
if frag.textDecorationColor == nil, let v = top.textDecorationColor, !v.isEmpty { frag.textDecorationColor = v }
112+
if frag.textDecorationStyle == nil, let v = top.textDecorationStyle { frag.textDecorationStyle = v }
113+
}
114+
}

0 commit comments

Comments
 (0)