Skip to content

Commit d7791c4

Browse files
committed
refactor(ios): move fragment/defaults merge into NitroTextImpl\n\n- Add FragmentTopDefaults and apply(fragments:text:top:) in NitroTextImpl\n- Delegate merging from HybridNitroText to NitroTextImpl\n- Keeps Hybrid class thin and centralizes render logic
1 parent fe367ad commit d7791c4

3 files changed

Lines changed: 118 additions & 86 deletions

File tree

ios/HybridNitroText.swift

Lines changed: 25 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import Foundation
99
import UIKit
1010

1111
class HybridNitroText : HybridNitroTextSpec, NitroTextViewDelegate {
12-
var view: UIView = NitroTextView()
13-
var nitroTextImpl: NitroTextImpl?
12+
private let textView = NitroTextView()
13+
var view: UIView { textView }
14+
let nitroTextImpl: NitroTextImpl
1415

1516
override init() {
16-
nitroTextImpl = NitroTextImpl(view as! NitroTextView)
17+
self.nitroTextImpl = NitroTextImpl(textView)
18+
super.init()
1719
}
1820

1921
func onNitroTextMeasured(height: Double) {
@@ -30,17 +32,17 @@ class HybridNitroText : HybridNitroTextSpec, NitroTextViewDelegate {
3032

3133
var selectable: Bool? {
3234
didSet {
33-
nitroTextImpl?.setSelectable(selectable)
35+
nitroTextImpl.setSelectable(selectable)
3436
}
3537
}
3638

3739
var onSelectableTextMeasured: ((Double) -> Void)? {
3840
didSet {
3941
if onSelectableTextMeasured == nil {
40-
(view as! NitroTextView).nitroTextDelegate = nil
42+
textView.nitroTextDelegate = nil
4143
return
4244
}
43-
(view as! NitroTextView).nitroTextDelegate = self
45+
textView.nitroTextDelegate = self
4446
}
4547
}
4648

@@ -58,7 +60,7 @@ class HybridNitroText : HybridNitroTextSpec, NitroTextViewDelegate {
5860

5961
var fontColor: String? {
6062
didSet {
61-
(view as! NitroTextView).textColor = ColorParser.parse(fontColor)
63+
textView.textColor = ColorParser.parse(fontColor)
6264
applyFragmentsAndProps()
6365
}
6466
}
@@ -71,15 +73,13 @@ class HybridNitroText : HybridNitroTextSpec, NitroTextViewDelegate {
7173

7274
var textAlign: TextAlign? {
7375
didSet {
74-
nitroTextImpl?.setTextAlign(textAlign)
75-
applyFragmentsAndProps()
76+
nitroTextImpl.setTextAlign(textAlign)
7677
}
7778
}
7879

7980
var textTransform: TextTransform? {
8081
didSet {
81-
nitroTextImpl?.setTextTransform(textTransform)
82-
applyFragmentsAndProps()
82+
nitroTextImpl.setTextTransform(textTransform)
8383
}
8484
}
8585

@@ -97,75 +97,31 @@ class HybridNitroText : HybridNitroTextSpec, NitroTextViewDelegate {
9797

9898
var numberOfLines: Double? {
9999
didSet {
100-
nitroTextImpl?.setNumberOfLines(numberOfLines)
100+
nitroTextImpl.setNumberOfLines(numberOfLines)
101101
}
102102
}
103103

104104
var ellipsizeMode: EllipsizeMode? {
105105
didSet {
106-
nitroTextImpl?.setEllipsizeMode(ellipsizeMode)
107-
applyFragmentsAndProps()
106+
nitroTextImpl.setEllipsizeMode(ellipsizeMode)
108107
}
109108
}
110109

111-
// Merge per-fragment props with top-level fallbacks and apply to NitroTextImpl
110+
// Merge per-fragment props with top-level fallbacks and apply (delegated to NitroTextImpl)
112111
private func applyFragmentsAndProps() {
113-
guard let nitroTextImpl else { return }
114-
115-
// If there are no per-fragment entries, but we have `text`, build a single fragment
116-
guard let source = fragments, !source.isEmpty else {
117-
if let t = text {
118-
let single = Fragment(
119-
fontSize: fontSize,
120-
fontWeight: fontWeight,
121-
fontColor: fontColor,
122-
fontStyle: fontStyle,
123-
lineHeight: lineHeight,
124-
text: t,
125-
numberOfLines: numberOfLines,
126-
textAlign: textAlign,
127-
textTransform: textTransform
128-
)
129-
nitroTextImpl.setFragments([single])
130-
} else {
131-
nitroTextImpl.setFragments(nil)
132-
}
133-
return
134-
}
135-
136-
var merged: [Fragment] = []
137-
merged.reserveCapacity(source.count)
138-
139-
for var frag in source {
140-
if frag.text == nil {
141-
frag.text = ""
142-
}
143-
if frag.fontSize == nil, let top = fontSize {
144-
frag.fontSize = top
145-
}
146-
if frag.fontWeight == nil, let top = fontWeight {
147-
frag.fontWeight = top
148-
}
149-
if frag.fontStyle == nil, let top = fontStyle {
150-
frag.fontStyle = top
151-
}
152-
if frag.lineHeight == nil, let top = lineHeight, top > 0 {
153-
frag.lineHeight = top
154-
}
155-
156-
if frag.fontColor == nil, let top = fontColor, !top.isEmpty {
157-
frag.fontColor = top
158-
}
159-
if frag.textAlign == nil, let ta = textAlign { frag.textAlign = ta }
160-
if frag.textTransform == nil, let tt = textTransform { frag.textTransform = tt }
161-
162-
merged.append(frag)
163-
}
164-
165-
nitroTextImpl.setFragments(merged)
112+
let top = NitroTextImpl.FragmentTopDefaults(
113+
fontSize: fontSize,
114+
fontWeight: fontWeight,
115+
fontColor: fontColor,
116+
fontStyle: fontStyle,
117+
lineHeight: lineHeight,
118+
textAlign: textAlign,
119+
textTransform: textTransform
120+
)
121+
nitroTextImpl.apply(fragments: fragments, text: text, top: top)
166122
}
167123

168124
func afterUpdate() {
169-
view.setNeedsLayout()
125+
textView.setNeedsLayout()
170126
}
171127
}

ios/NitroTextImpl.swift

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import UIKit
99

1010
final class NitroTextImpl {
11-
private let nitroTextView : NitroTextView
11+
private weak var nitroTextView : NitroTextView?
1212
private var currentTextAlignment: NSTextAlignment = .natural
1313
private var currentTransform: TextTransform = .none
1414
private var currentEllipsize: NSLineBreakMode = .byTruncatingTail
@@ -18,14 +18,13 @@ final class NitroTextImpl {
1818
}
1919

2020
func setSelectable(_ selectable: Bool?) {
21-
nitroTextView.isSelectable = selectable ?? true
21+
nitroTextView?.isSelectable = selectable ?? true
2222
}
2323

2424
func setNumberOfLines(_ value: Double?) {
2525
let n = Int(value ?? 0)
26-
nitroTextView.textContainer.maximumNumberOfLines = n
27-
nitroTextView.textContainer.lineBreakMode = effectiveLineBreakMode(forLines: n)
28-
nitroTextView.setNeedsLayout()
26+
nitroTextView?.textContainer.maximumNumberOfLines = n
27+
nitroTextView?.textContainer.lineBreakMode = effectiveLineBreakMode(forLines: n)
2928
}
3029

3130
func setEllipsizeMode(_ mode: EllipsizeMode?) {
@@ -37,9 +36,8 @@ final class NitroTextImpl {
3736
default: currentEllipsize = .byTruncatingTail
3837
}
3938
// Re-apply to container based on current numberOfLines
40-
let n = nitroTextView.textContainer.maximumNumberOfLines
41-
nitroTextView.textContainer.lineBreakMode = effectiveLineBreakMode(forLines: n)
42-
nitroTextView.setNeedsLayout()
39+
guard let n = nitroTextView?.textContainer.maximumNumberOfLines else { return }
40+
nitroTextView?.textContainer.lineBreakMode = effectiveLineBreakMode(forLines: n)
4341
}
4442

4543
private func effectiveLineBreakMode(forLines n: Int) -> NSLineBreakMode {
@@ -51,20 +49,24 @@ final class NitroTextImpl {
5149
switch currentEllipsize {
5250
case .byClipping:
5351
return .byClipping
54-
default:
52+
case .byTruncatingHead:
53+
return .byTruncatingHead
54+
case .byTruncatingMiddle:
5555
return .byTruncatingMiddle
56+
default:
57+
return .byTruncatingTail
5658
}
5759
}
5860

5961
func setText(_ attributedText: NSAttributedString) {
60-
if let storage = nitroTextView.tkStorage ?? nitroTextView.layoutManager.textStorage {
62+
if let storage = nitroTextView?.tkStorage ?? nitroTextView?.layoutManager.textStorage {
6163
storage.beginEditing()
6264
storage.setAttributedString(attributedText)
6365
storage.endEditing()
6466
} else {
65-
nitroTextView.attributedText = attributedText
67+
nitroTextView?.attributedText = attributedText
6668
}
67-
nitroTextView.setNeedsLayout()
69+
nitroTextView?.setNeedsLayout()
6870
}
6971

7072
func setPlainText(_ value: String?) {
@@ -80,7 +82,7 @@ final class NitroTextImpl {
8082
case .some(.left): currentTextAlignment = .left
8183
default: currentTextAlignment = .natural
8284
}
83-
nitroTextView.textAlignment = currentTextAlignment
85+
nitroTextView?.textAlignment = currentTextAlignment
8486
}
8587

8688
func setTextTransform(_ transform: TextTransform?) {
@@ -94,12 +96,12 @@ final class NitroTextImpl {
9496

9597
func setFragments(_ fragments: [Fragment]?) {
9698
guard let fragments = fragments, !fragments.isEmpty else {
97-
nitroTextView.attributedText = nil
99+
nitroTextView?.attributedText = nil
98100
return
99101
}
100102

101103
let result = NSMutableAttributedString()
102-
let defaultColor = nitroTextView.textColor ?? UIColor.label
104+
let defaultColor = nitroTextView?.textColor ?? UIColor.label
103105

104106
for fragment in fragments {
105107
guard let rawText = fragment.text else { continue }
@@ -135,7 +137,7 @@ final class NitroTextImpl {
135137
private func makeFont(for fragment: Fragment) -> (value: UIFont, isItalic: Bool) {
136138
let resolvedSize: CGFloat = {
137139
if let s = fragment.fontSize { return CGFloat(s) }
138-
if let current = nitroTextView.font?.pointSize { return current }
140+
if let current = nitroTextView?.font?.pointSize { return current }
139141
return 14.0
140142
}()
141143
let weightToken = fragment.fontWeight ?? FontWeight.normal
@@ -177,7 +179,7 @@ final class NitroTextImpl {
177179
para.alignment = currentTextAlignment
178180
}
179181

180-
let n = nitroTextView.textContainer.maximumNumberOfLines
182+
guard let n = nitroTextView?.textContainer.maximumNumberOfLines else { return para }
181183
para.lineBreakMode = effectiveLineBreakMode(forLines: n)
182184
return para
183185
}
@@ -213,6 +215,54 @@ final class NitroTextImpl {
213215

214216

215217
extension NitroTextImpl {
218+
struct FragmentTopDefaults {
219+
let fontSize: Double?
220+
let fontWeight: FontWeight?
221+
let fontColor: String?
222+
let fontStyle: FontStyle?
223+
let lineHeight: Double?
224+
let textAlign: TextAlign?
225+
let textTransform: TextTransform?
226+
}
227+
228+
func apply(fragments: [Fragment]?, text: String?, top: FragmentTopDefaults) {
229+
// Fast path: no fragments, but we have plain text
230+
guard let fragments, !fragments.isEmpty else {
231+
if let t = text {
232+
let single = Fragment(
233+
fontSize: top.fontSize,
234+
fontWeight: top.fontWeight,
235+
fontColor: top.fontColor,
236+
fontStyle: top.fontStyle,
237+
lineHeight: top.lineHeight,
238+
text: t,
239+
numberOfLines: nil,
240+
textAlign: top.textAlign,
241+
textTransform: top.textTransform
242+
)
243+
setFragments([single])
244+
} else {
245+
setFragments(nil)
246+
}
247+
return
248+
}
249+
250+
var merged: [Fragment] = []
251+
merged.reserveCapacity(fragments.count)
252+
253+
for var frag in fragments {
254+
if frag.text == nil { frag.text = "" }
255+
if frag.fontSize == nil, let v = top.fontSize { frag.fontSize = v }
256+
if frag.fontWeight == nil, let v = top.fontWeight { frag.fontWeight = v }
257+
if frag.fontStyle == nil, let v = top.fontStyle { frag.fontStyle = v }
258+
if frag.lineHeight == nil, let v = top.lineHeight, v > 0 { frag.lineHeight = v }
259+
if frag.fontColor == nil, let v = top.fontColor, !v.isEmpty { frag.fontColor = v }
260+
if frag.textAlign == nil, let v = top.textAlign { frag.textAlign = v }
261+
if frag.textTransform == nil, let v = top.textTransform { frag.textTransform = v }
262+
merged.append(frag)
263+
}
264+
setFragments(merged)
265+
}
216266
private static func fontWeightFromString(_ s: FontWeight) -> UIFont.Weight {
217267
switch s {
218268
case .ultralight:

ios/NitroTextShadowNode.hpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ namespace margelo::nitro::nitrotext::views
202202
break;
203203
}
204204
}
205+
205206
// NOTE: You can also map fontWeight/fontStyle/fontColor into
206207
// textAttributes here when you settle on your Fragment <->
207208
// TextAttributes mapping.
@@ -213,6 +214,7 @@ namespace margelo::nitro::nitrotext::views
213214
.parentShadowView = react::ShadowView(*this)});
214215

215216
react::ParagraphAttributes paragraphAttributes;
217+
216218
if (props.numberOfLines.value.has_value())
217219
{
218220
auto n =
@@ -222,6 +224,30 @@ namespace margelo::nitro::nitrotext::views
222224
paragraphAttributes.maximumNumberOfLines = n;
223225
}
224226
}
227+
228+
if (props.ellipsizeMode.value.has_value())
229+
{
230+
using NitroEllipsizeMode = margelo::nitro::nitrotext::EllipsizeMode;
231+
using RNEllipsizeMode = facebook::react::EllipsizeMode;
232+
switch (props.ellipsizeMode.value.value())
233+
{
234+
case NitroEllipsizeMode::CLIP:
235+
paragraphAttributes.ellipsizeMode = RNEllipsizeMode::Clip;
236+
break;
237+
case NitroEllipsizeMode::HEAD:
238+
paragraphAttributes.ellipsizeMode = RNEllipsizeMode::Head;
239+
break;
240+
case NitroEllipsizeMode::MIDDLE:
241+
paragraphAttributes.ellipsizeMode = RNEllipsizeMode::Middle;
242+
break;
243+
case NitroEllipsizeMode::TAIL:
244+
paragraphAttributes.ellipsizeMode = RNEllipsizeMode::Tail;
245+
break;
246+
default:
247+
paragraphAttributes.ellipsizeMode = RNEllipsizeMode::Tail;
248+
break;
249+
}
250+
}
225251

226252
// 3) Ensure we have a TextLayoutManager (set in ComponentDescriptor::adopt)
227253
if (!textLayoutManager_)

0 commit comments

Comments
 (0)