craft-native 0.0.120 → 0.0.121
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android/templates/MainActivityNative.kt.template +378 -54
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +9 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +12 -0
- package/dist/ios/src/index.js +38 -2
- package/dist/ios/templates/CraftNativeScreen.swift +414 -52
- package/dist/ios/templates/Info.plist.template +2 -0
- package/dist/ios/templates/project.yml.template +17 -0
- package/package.json +1 -1
|
@@ -18,16 +18,225 @@ struct CraftNativeScreen: UIViewControllerRepresentable {
|
|
|
18
18
|
func updateUIViewController(_ controller: UINavigationController, context: Context) {}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
private struct CraftNativeLayoutStyle {
|
|
22
|
+
let width: CGFloat?
|
|
23
|
+
let height: CGFloat?
|
|
24
|
+
let minWidth: CGFloat?
|
|
25
|
+
let maxWidth: CGFloat?
|
|
26
|
+
let minHeight: CGFloat?
|
|
27
|
+
let maxHeight: CGFloat?
|
|
28
|
+
let position: String
|
|
29
|
+
let top: CGFloat?
|
|
30
|
+
let right: CGFloat?
|
|
31
|
+
let bottom: CGFloat?
|
|
32
|
+
let left: CGFloat?
|
|
33
|
+
let alignSelf: String?
|
|
34
|
+
|
|
35
|
+
init(_ raw: [String: Any]) {
|
|
36
|
+
func number(_ value: Any?) -> CGFloat? { (value as? NSNumber).map { CGFloat(truncating: $0) } }
|
|
37
|
+
width = number(raw["width"])
|
|
38
|
+
height = number(raw["height"])
|
|
39
|
+
minWidth = number(raw["minWidth"])
|
|
40
|
+
maxWidth = number(raw["maxWidth"])
|
|
41
|
+
minHeight = number(raw["minHeight"])
|
|
42
|
+
maxHeight = number(raw["maxHeight"])
|
|
43
|
+
position = raw["position"] as? String ?? "relative"
|
|
44
|
+
top = number(raw["top"])
|
|
45
|
+
right = number(raw["right"])
|
|
46
|
+
bottom = number(raw["bottom"])
|
|
47
|
+
left = number(raw["left"])
|
|
48
|
+
alignSelf = raw["alignSelf"] as? String
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private final class CraftNativeFlowView: UIStackView {
|
|
53
|
+
var wrap = false { didSet { invalidateIntrinsicContentSize(); setNeedsLayout() } }
|
|
54
|
+
var alignItems = "stretch" { didSet { alignment = alignmentValue; setNeedsLayout() } }
|
|
55
|
+
var justifyContent = "flex-start" { didSet { distribution = distributionValue; setNeedsLayout() } }
|
|
56
|
+
var gap: CGFloat = 0 { didSet { spacing = gap; invalidateIntrinsicContentSize(); setNeedsLayout() } }
|
|
57
|
+
var rowGap: CGFloat? { didSet { invalidateIntrinsicContentSize(); setNeedsLayout() } }
|
|
58
|
+
var columnGap: CGFloat? { didSet { invalidateIntrinsicContentSize(); setNeedsLayout() } }
|
|
59
|
+
var padding = UIEdgeInsets.zero {
|
|
60
|
+
didSet {
|
|
61
|
+
layoutMargins = padding
|
|
62
|
+
isLayoutMarginsRelativeArrangement = true
|
|
63
|
+
invalidateIntrinsicContentSize()
|
|
64
|
+
setNeedsLayout()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
private var childStyles: [ObjectIdentifier: CraftNativeLayoutStyle] = [:]
|
|
68
|
+
|
|
69
|
+
override init(frame: CGRect) {
|
|
70
|
+
super.init(frame: frame)
|
|
71
|
+
alignment = .fill
|
|
72
|
+
distribution = .fill
|
|
73
|
+
isLayoutMarginsRelativeArrangement = true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
required init(coder: NSCoder) { super.init(coder: coder) }
|
|
77
|
+
|
|
78
|
+
private var alignmentValue: UIStackView.Alignment {
|
|
79
|
+
switch alignItems {
|
|
80
|
+
case "flex-start": return .leading
|
|
81
|
+
case "center": return .center
|
|
82
|
+
case "flex-end": return .trailing
|
|
83
|
+
case "baseline": return .firstBaseline
|
|
84
|
+
default: return .fill
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private var distributionValue: UIStackView.Distribution {
|
|
89
|
+
switch justifyContent {
|
|
90
|
+
case "center": return .equalCentering
|
|
91
|
+
case "space-between", "space-around", "space-evenly": return .equalSpacing
|
|
92
|
+
default: return .fill
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func setLayoutStyle(_ raw: [String: Any], for view: UIView) {
|
|
97
|
+
childStyles[ObjectIdentifier(view)] = CraftNativeLayoutStyle(raw)
|
|
98
|
+
invalidateIntrinsicContentSize()
|
|
99
|
+
setNeedsLayout()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func removeLayoutStyle(for view: UIView) { childStyles.removeValue(forKey: ObjectIdentifier(view)) }
|
|
103
|
+
|
|
104
|
+
override func didAddSubview(_ subview: UIView) {
|
|
105
|
+
super.didAddSubview(subview)
|
|
106
|
+
setNeedsLayout()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
override func willRemoveSubview(_ subview: UIView) {
|
|
110
|
+
childStyles.removeValue(forKey: ObjectIdentifier(subview))
|
|
111
|
+
super.willRemoveSubview(subview)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
override var intrinsicContentSize: CGSize {
|
|
115
|
+
let children = subviews.filter { !$0.isHidden && !($0 is CraftNativeFlexSpacer) }
|
|
116
|
+
guard !children.isEmpty else { return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) }
|
|
117
|
+
let sizes = children.map { measuredSize(for: $0, available: CGSize(width: 10_000, height: 10_000)) }
|
|
118
|
+
let main = sizes.map { axis == .horizontal ? $0.width : $0.height }.reduce(0, +)
|
|
119
|
+
let cross = sizes.map { axis == .horizontal ? $0.height : $0.width }.max() ?? 0
|
|
120
|
+
let mainGap = max(0, CGFloat(max(0, children.count - 1))) * mainGapValue
|
|
121
|
+
if axis == .horizontal {
|
|
122
|
+
return CGSize(width: padding.left + padding.right + main + mainGap, height: padding.top + padding.bottom + cross)
|
|
123
|
+
}
|
|
124
|
+
return CGSize(width: padding.left + padding.right + cross, height: padding.top + padding.bottom + main + mainGap)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
override func layoutSubviews() {
|
|
128
|
+
super.layoutSubviews()
|
|
129
|
+
let content = bounds.inset(by: padding)
|
|
130
|
+
let availableMain = axis == .horizontal ? content.width : content.height
|
|
131
|
+
var lines: [([UIView], [CGSize], CGFloat, CGFloat)] = []
|
|
132
|
+
var items: [UIView] = []
|
|
133
|
+
var sizes: [CGSize] = []
|
|
134
|
+
var main: CGFloat = 0
|
|
135
|
+
var cross: CGFloat = 0
|
|
136
|
+
|
|
137
|
+
func flush() {
|
|
138
|
+
guard !items.isEmpty else { return }
|
|
139
|
+
lines.append((items, sizes, main, cross))
|
|
140
|
+
items = []; sizes = []; main = 0; cross = 0
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for child in subviews where !child.isHidden && !(child is CraftNativeFlexSpacer) {
|
|
144
|
+
let style = childStyles[ObjectIdentifier(child)] ?? CraftNativeLayoutStyle([:])
|
|
145
|
+
if style.position == "absolute" { continue }
|
|
146
|
+
let size = measuredSize(for: child, available: content.size, style: style)
|
|
147
|
+
let childMain = axis == .horizontal ? size.width : size.height
|
|
148
|
+
let childCross = axis == .horizontal ? size.height : size.width
|
|
149
|
+
let next = items.isEmpty ? childMain : main + mainGapValue + childMain
|
|
150
|
+
if wrap && !items.isEmpty && next > availableMain { flush() }
|
|
151
|
+
items.append(child); sizes.append(size)
|
|
152
|
+
main = items.count == 1 ? childMain : main + mainGapValue + childMain
|
|
153
|
+
cross = max(cross, childCross)
|
|
154
|
+
}
|
|
155
|
+
flush()
|
|
156
|
+
|
|
157
|
+
var crossOffset: CGFloat = 0
|
|
158
|
+
for (lineItems, lineSizes, lineMain, lineCross) in lines {
|
|
159
|
+
let free = max(0, availableMain - lineMain)
|
|
160
|
+
let (leading, between) = distribution(free: free, count: lineItems.count)
|
|
161
|
+
var mainOffset = leading
|
|
162
|
+
for (index, child) in lineItems.enumerated() {
|
|
163
|
+
let style = childStyles[ObjectIdentifier(child)] ?? CraftNativeLayoutStyle([:])
|
|
164
|
+
let size = lineSizes[index]
|
|
165
|
+
let childCross = axis == .horizontal ? size.height : size.width
|
|
166
|
+
let alignment = style.alignSelf ?? alignItems
|
|
167
|
+
let crossSize: CGFloat = alignment == "stretch" && styleCrossDimension(style) == nil ? lineCross : childCross
|
|
168
|
+
let crossPosition: CGFloat
|
|
169
|
+
switch alignment {
|
|
170
|
+
case "center": crossPosition = (lineCross - crossSize) / 2
|
|
171
|
+
case "flex-end": crossPosition = lineCross - crossSize
|
|
172
|
+
default: crossPosition = 0
|
|
173
|
+
}
|
|
174
|
+
let frame: CGRect
|
|
175
|
+
if axis == .horizontal {
|
|
176
|
+
frame = CGRect(x: content.minX + mainOffset, y: content.minY + crossOffset + crossPosition, width: size.width, height: crossSize)
|
|
177
|
+
} else {
|
|
178
|
+
frame = CGRect(x: content.minX + crossOffset + crossPosition, y: content.minY + mainOffset, width: crossSize, height: size.height)
|
|
179
|
+
}
|
|
180
|
+
child.frame = frame.integral
|
|
181
|
+
mainOffset += (axis == .horizontal ? size.width : size.height) + mainGapValue + between
|
|
182
|
+
}
|
|
183
|
+
crossOffset += lineCross + crossGapValue
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for child in subviews where !child.isHidden && !(child is CraftNativeFlexSpacer) {
|
|
187
|
+
let style = childStyles[ObjectIdentifier(child)] ?? CraftNativeLayoutStyle([:])
|
|
188
|
+
guard style.position == "absolute" else { continue }
|
|
189
|
+
let size = measuredSize(for: child, available: content.size, style: style)
|
|
190
|
+
let width = style.left != nil && style.right != nil ? max(0, content.width - (style.left ?? 0) - (style.right ?? 0)) : size.width
|
|
191
|
+
let height = style.top != nil && style.bottom != nil ? max(0, content.height - (style.top ?? 0) - (style.bottom ?? 0)) : size.height
|
|
192
|
+
let x = style.left ?? (style.right.map { content.width - $0 - width } ?? 0)
|
|
193
|
+
let y = style.top ?? (style.bottom.map { content.height - $0 - height } ?? 0)
|
|
194
|
+
child.frame = CGRect(x: content.minX + x, y: content.minY + y, width: width, height: height).integral
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private var mainGapValue: CGFloat { axis == .horizontal ? (columnGap ?? gap) : (rowGap ?? gap) }
|
|
199
|
+
private var crossGapValue: CGFloat { axis == .horizontal ? (rowGap ?? 0) : (columnGap ?? 0) }
|
|
200
|
+
|
|
201
|
+
private func styleCrossDimension(_ style: CraftNativeLayoutStyle) -> CGFloat? {
|
|
202
|
+
axis == .horizontal ? style.height : style.width
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private func measuredSize(for view: UIView, available: CGSize, style: CraftNativeLayoutStyle? = nil) -> CGSize {
|
|
206
|
+
let style = style ?? childStyles[ObjectIdentifier(view)] ?? CraftNativeLayoutStyle([:])
|
|
207
|
+
let intrinsic = view.intrinsicContentSize
|
|
208
|
+
let fitted = view.sizeThatFits(available)
|
|
209
|
+
let width = style.width ?? (intrinsic.width > 0 && intrinsic.width != UIView.noIntrinsicMetric ? intrinsic.width : max(0, fitted.width))
|
|
210
|
+
let height = style.height ?? (intrinsic.height > 0 && intrinsic.height != UIView.noIntrinsicMetric ? intrinsic.height : max(0, fitted.height))
|
|
211
|
+
return CGSize(width: clamp(width, min: style.minWidth, max: style.maxWidth), height: clamp(height, min: style.minHeight, max: style.maxHeight))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private func clamp(_ value: CGFloat, min lower: CGFloat?, max upper: CGFloat?) -> CGFloat {
|
|
215
|
+
var result = value
|
|
216
|
+
if let lower { result = max(result, lower) }
|
|
217
|
+
if let upper { result = min(result, upper) }
|
|
218
|
+
return result
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private func distribution(free: CGFloat, count: Int) -> (CGFloat, CGFloat) {
|
|
222
|
+
guard count > 0 else { return (0, 0) }
|
|
223
|
+
switch justifyContent {
|
|
224
|
+
case "center": return (free / 2, 0)
|
|
225
|
+
case "flex-end": return (free, 0)
|
|
226
|
+
case "space-between": return (0, count > 1 ? free / CGFloat(count - 1) : 0)
|
|
227
|
+
case "space-around": return (free / CGFloat(count * 2), free / CGFloat(count))
|
|
228
|
+
case "space-evenly": return (free / CGFloat(count + 1), free / CGFloat(count + 1))
|
|
229
|
+
default: return (0, 0)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
21
234
|
private final class CraftNativeScrollView: UIScrollView {
|
|
22
|
-
let contentStack =
|
|
235
|
+
let contentStack = CraftNativeFlowView()
|
|
23
236
|
private var crossAxisConstraint: NSLayoutConstraint?
|
|
24
237
|
|
|
25
238
|
override init(frame: CGRect) {
|
|
26
239
|
super.init(frame: frame)
|
|
27
|
-
contentStack.axis = .vertical
|
|
28
|
-
contentStack.alignment = .fill
|
|
29
|
-
contentStack.distribution = .fill
|
|
30
|
-
contentStack.isLayoutMarginsRelativeArrangement = true
|
|
31
240
|
contentStack.translatesAutoresizingMaskIntoConstraints = false
|
|
32
241
|
addSubview(contentStack)
|
|
33
242
|
NSLayoutConstraint.activate([
|
|
@@ -55,12 +264,13 @@ private final class CraftNativeScrollView: UIScrollView {
|
|
|
55
264
|
|
|
56
265
|
private final class CraftNativeFlexSpacer: UIView {}
|
|
57
266
|
|
|
58
|
-
final class CraftNativeScreenController: UIViewController {
|
|
267
|
+
final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate {
|
|
59
268
|
private final class RenderedNode {
|
|
60
269
|
let identity: String
|
|
61
270
|
let type: String
|
|
62
271
|
let view: UIView
|
|
63
272
|
var protocolId: String?
|
|
273
|
+
var style: [String: Any] = [:]
|
|
64
274
|
var children: [RenderedNode] = []
|
|
65
275
|
var widthConstraint: NSLayoutConstraint?
|
|
66
276
|
var heightConstraint: NSLayoutConstraint?
|
|
@@ -76,12 +286,19 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
76
286
|
private let routeName: String?
|
|
77
287
|
private let routeParams: [String: Any]
|
|
78
288
|
private let jsContext = JSContext()!
|
|
79
|
-
private let rootStack =
|
|
289
|
+
private let rootStack = CraftNativeFlowView()
|
|
80
290
|
private var handlers: [ObjectIdentifier: String] = [:]
|
|
291
|
+
private var focusHandlers: [ObjectIdentifier: String] = [:]
|
|
292
|
+
private var blurHandlers: [ObjectIdentifier: String] = [:]
|
|
293
|
+
private var submitHandlers: [ObjectIdentifier: String] = [:]
|
|
294
|
+
private var scrollHandlers: [ObjectIdentifier: String] = [:]
|
|
295
|
+
private var scrollBeginHandlers: [ObjectIdentifier: String] = [:]
|
|
296
|
+
private var scrollEndHandlers: [ObjectIdentifier: String] = [:]
|
|
81
297
|
private var tapRecognizers: [ObjectIdentifier: UITapGestureRecognizer] = [:]
|
|
82
298
|
private var imageSources: [ObjectIdentifier: String] = [:]
|
|
83
299
|
private var imageTasks: [ObjectIdentifier: URLSessionDataTask] = [:]
|
|
84
300
|
private var renderedRoot: RenderedNode?
|
|
301
|
+
private weak var lastFocusedInput: UITextField?
|
|
85
302
|
private var flatListRows: [ObjectIdentifier: [String: RenderedNode]] = [:]
|
|
86
303
|
private var flatListOwners: [String: String] = [:]
|
|
87
304
|
private let mutationDocument = CraftNativeMutationDocument()
|
|
@@ -115,8 +332,7 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
115
332
|
navigationItem.title = routeName ?? config.appName
|
|
116
333
|
view.backgroundColor = config.resolvedBackgroundColor
|
|
117
334
|
rootStack.axis = .vertical
|
|
118
|
-
rootStack.
|
|
119
|
-
rootStack.distribution = .fill
|
|
335
|
+
rootStack.alignItems = "stretch"
|
|
120
336
|
rootStack.translatesAutoresizingMaskIntoConstraints = false
|
|
121
337
|
view.addSubview(rootStack)
|
|
122
338
|
NSLayoutConstraint.activate([
|
|
@@ -208,6 +424,7 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
208
424
|
label.textAlignment = .center
|
|
209
425
|
label.text = message
|
|
210
426
|
rootStack.addArrangedSubview(label)
|
|
427
|
+
rootStack.setLayoutStyle(["alignSelf": "stretch"], for: label)
|
|
211
428
|
NSLog("[craft native] %@", message)
|
|
212
429
|
}
|
|
213
430
|
|
|
@@ -334,6 +551,7 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
334
551
|
}
|
|
335
552
|
|
|
336
553
|
private func applyTargetedUpdates(_ ids: [String]) -> Bool {
|
|
554
|
+
let focused = firstResponder(in: rootStack) ?? lastFocusedInput
|
|
337
555
|
var applied = Set<String>()
|
|
338
556
|
for id in ids {
|
|
339
557
|
let target = flatListOwners[id] ?? id
|
|
@@ -341,6 +559,7 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
341
559
|
guard let document = mutationDocument.node(target), let previous = renderedNode(target) else { return false }
|
|
342
560
|
_ = reconcile(document, identity: previous.identity, path: target, previous: previous)
|
|
343
561
|
}
|
|
562
|
+
restoreFocus(focused)
|
|
344
563
|
return true
|
|
345
564
|
}
|
|
346
565
|
|
|
@@ -354,13 +573,16 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
354
573
|
}
|
|
355
574
|
|
|
356
575
|
private func renderCommitted(_ document: [String: Any]) {
|
|
576
|
+
let focused = firstResponder(in: rootStack) ?? lastFocusedInput
|
|
357
577
|
let previous = renderedRoot
|
|
358
578
|
let next = reconcile(document, identity: "root", path: "root", previous: previous)
|
|
359
579
|
if previous?.view !== next.view {
|
|
360
580
|
if let old = previous { detach(old, from: rootStack) }
|
|
361
581
|
rootStack.addArrangedSubview(next.view)
|
|
362
582
|
}
|
|
583
|
+
rootStack.setLayoutStyle(next.style, for: next.view)
|
|
363
584
|
renderedRoot = next
|
|
585
|
+
restoreFocus(focused)
|
|
364
586
|
}
|
|
365
587
|
|
|
366
588
|
private func clearRenderedTree() {
|
|
@@ -368,6 +590,19 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
368
590
|
renderedRoot = nil
|
|
369
591
|
}
|
|
370
592
|
|
|
593
|
+
private func firstResponder(in view: UIView) -> UIView? {
|
|
594
|
+
if view.isFirstResponder { return view }
|
|
595
|
+
for child in view.subviews {
|
|
596
|
+
if let responder = firstResponder(in: child) { return responder }
|
|
597
|
+
}
|
|
598
|
+
return nil
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private func restoreFocus(_ focused: UIView?) {
|
|
602
|
+
guard let focused, focused.window != nil, !focused.isFirstResponder else { return }
|
|
603
|
+
_ = focused.becomeFirstResponder()
|
|
604
|
+
}
|
|
605
|
+
|
|
371
606
|
private func reconcile(_ node: [String: Any], identity: String, path: String, previous: RenderedNode?) -> RenderedNode {
|
|
372
607
|
let type = node["type"] as? String ?? "View"
|
|
373
608
|
let props = node["props"] as? [String: Any] ?? [:]
|
|
@@ -383,6 +618,7 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
383
618
|
|
|
384
619
|
let result = current.view
|
|
385
620
|
current.protocolId = node["id"] as? String
|
|
621
|
+
current.style = style
|
|
386
622
|
result.accessibilityIdentifier = accessibilityIdentifier(node, props: props) ?? path
|
|
387
623
|
if type != "View" && type != "SafeAreaView" && type != "ScrollView" && type != "FlatList" {
|
|
388
624
|
result.setContentHuggingPriority(.required, for: .vertical)
|
|
@@ -408,8 +644,26 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
408
644
|
field.textColor = color(style["color"]) ?? .label
|
|
409
645
|
field.font = textFont(style, default: field.font ?? .systemFont(ofSize: UIFont.systemFontSize))
|
|
410
646
|
field.textAlignment = textAlignment(style["textAlign"])
|
|
647
|
+
let desiredKeyboardType = keyboardType(props["keyboardType"])
|
|
648
|
+
if field.keyboardType != desiredKeyboardType { field.keyboardType = desiredKeyboardType }
|
|
649
|
+
let desiredReturnKeyType = returnKeyType(props["returnKeyType"])
|
|
650
|
+
if field.returnKeyType != desiredReturnKeyType { field.returnKeyType = desiredReturnKeyType }
|
|
651
|
+
let desiredAutocorrection: UITextAutocorrectionType = props["autoCorrect"] as? Bool == false ? .no : .default
|
|
652
|
+
if field.autocorrectionType != desiredAutocorrection { field.autocorrectionType = desiredAutocorrection }
|
|
653
|
+
let desiredCapitalization = capitalizationType(props["autoCapitalize"])
|
|
654
|
+
if field.autocapitalizationType != desiredCapitalization { field.autocapitalizationType = desiredCapitalization }
|
|
655
|
+
let desiredSecureEntry = props["secureTextEntry"] as? Bool == true
|
|
656
|
+
if field.isSecureTextEntry != desiredSecureEntry { field.isSecureTextEntry = desiredSecureEntry }
|
|
657
|
+
let desiredEnabled = props["editable"] as? Bool != false
|
|
658
|
+
if field.isEnabled != desiredEnabled { field.isEnabled = desiredEnabled }
|
|
411
659
|
updateField(field, value: props["value"] as? String)
|
|
412
660
|
updateHandler(events["onChange"] ?? events["onChangeText"], for: field)
|
|
661
|
+
updateAuxiliaryHandler(events["onFocus"], in: &focusHandlers, for: field)
|
|
662
|
+
updateAuxiliaryHandler(events["onBlur"] ?? events["onEndEditing"], in: &blurHandlers, for: field)
|
|
663
|
+
updateAuxiliaryHandler(events["onSubmitEditing"], in: &submitHandlers, for: field)
|
|
664
|
+
if props["autoFocus"] as? Bool == true, !field.isFirstResponder {
|
|
665
|
+
DispatchQueue.main.async { _ = field.becomeFirstResponder() }
|
|
666
|
+
}
|
|
413
667
|
case "Image":
|
|
414
668
|
let image = result as! UIImageView
|
|
415
669
|
image.contentMode = imageContentMode(style["resizeMode"] ?? props["resizeMode"])
|
|
@@ -420,6 +674,15 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
420
674
|
let direction = (props["horizontal"] as? Bool) == true || style["flexDirection"] as? String == "row"
|
|
421
675
|
? NSLayoutConstraint.Axis.horizontal : .vertical
|
|
422
676
|
scroll.setAxis(direction)
|
|
677
|
+
scroll.delegate = self
|
|
678
|
+
scroll.isScrollEnabled = props["scrollEnabled"] as? Bool != false
|
|
679
|
+
scroll.showsVerticalScrollIndicator = props["showsVerticalScrollIndicator"] as? Bool != false
|
|
680
|
+
scroll.showsHorizontalScrollIndicator = props["showsHorizontalScrollIndicator"] as? Bool != false
|
|
681
|
+
scroll.alwaysBounceVertical = props["alwaysBounceVertical"] as? Bool ?? (direction == .vertical)
|
|
682
|
+
scroll.alwaysBounceHorizontal = props["alwaysBounceHorizontal"] as? Bool ?? (direction == .horizontal)
|
|
683
|
+
updateAuxiliaryHandler(events["onScroll"], in: &scrollHandlers, for: scroll)
|
|
684
|
+
updateAuxiliaryHandler(events["onScrollBeginDrag"], in: &scrollBeginHandlers, for: scroll)
|
|
685
|
+
updateAuxiliaryHandler(events["onScrollEndDrag"], in: &scrollEndHandlers, for: scroll)
|
|
423
686
|
configureStack(scroll.contentStack, style: style)
|
|
424
687
|
reconcileChildren(children, in: scroll.contentStack, parent: current, path: path, style: style)
|
|
425
688
|
case "FlatList":
|
|
@@ -433,7 +696,7 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
433
696
|
events: events
|
|
434
697
|
)
|
|
435
698
|
default:
|
|
436
|
-
let stack = result as!
|
|
699
|
+
let stack = result as! CraftNativeFlowView
|
|
437
700
|
configureStack(stack, style: style)
|
|
438
701
|
reconcileChildren(children, in: stack, parent: current, path: path, style: style)
|
|
439
702
|
}
|
|
@@ -458,6 +721,9 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
458
721
|
let field = UITextField()
|
|
459
722
|
field.borderStyle = .roundedRect
|
|
460
723
|
field.addTarget(self, action: #selector(textChanged(_:)), for: .editingChanged)
|
|
724
|
+
field.addTarget(self, action: #selector(textFocused(_:)), for: .editingDidBegin)
|
|
725
|
+
field.addTarget(self, action: #selector(textBlurred(_:)), for: .editingDidEnd)
|
|
726
|
+
field.addTarget(self, action: #selector(textSubmitted(_:)), for: .editingDidEndOnExit)
|
|
461
727
|
return field
|
|
462
728
|
case "Image":
|
|
463
729
|
return UIImageView()
|
|
@@ -466,11 +732,7 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
466
732
|
case "FlatList":
|
|
467
733
|
return CraftNativeFlatList()
|
|
468
734
|
default:
|
|
469
|
-
|
|
470
|
-
stack.alignment = .fill
|
|
471
|
-
stack.distribution = .fill
|
|
472
|
-
stack.isLayoutMarginsRelativeArrangement = true
|
|
473
|
-
return stack
|
|
735
|
+
return CraftNativeFlowView()
|
|
474
736
|
}
|
|
475
737
|
}
|
|
476
738
|
|
|
@@ -490,7 +752,7 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
490
752
|
|
|
491
753
|
private func reconcileChildren(
|
|
492
754
|
_ children: [Any],
|
|
493
|
-
in stack:
|
|
755
|
+
in stack: CraftNativeFlowView,
|
|
494
756
|
parent: RenderedNode,
|
|
495
757
|
path: String,
|
|
496
758
|
style: [String: Any]
|
|
@@ -519,11 +781,13 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
519
781
|
detach(child, from: stack)
|
|
520
782
|
}
|
|
521
783
|
for (index, child) in next.enumerated() {
|
|
522
|
-
if index < stack.arrangedSubviews.count, stack.arrangedSubviews[index] === child.view {
|
|
523
|
-
|
|
524
|
-
|
|
784
|
+
if index < stack.arrangedSubviews.count, stack.arrangedSubviews[index] === child.view {
|
|
785
|
+
stack.setLayoutStyle(child.style, for: child.view)
|
|
786
|
+
continue
|
|
525
787
|
}
|
|
526
|
-
stack.
|
|
788
|
+
if child.view.superview === stack { stack.removeArrangedSubview(child.view) }
|
|
789
|
+
stack.insertArrangedSubview(child.view, at: min(index, stack.arrangedSubviews.count))
|
|
790
|
+
stack.setLayoutStyle(child.style, for: child.view)
|
|
527
791
|
}
|
|
528
792
|
parent.children = next
|
|
529
793
|
addJustificationSpacers(to: stack, value: style["justifyContent"] as? String)
|
|
@@ -584,8 +848,9 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
584
848
|
}
|
|
585
849
|
}
|
|
586
850
|
|
|
587
|
-
private func detach(_ node: RenderedNode, from stack:
|
|
851
|
+
private func detach(_ node: RenderedNode, from stack: CraftNativeFlowView) {
|
|
588
852
|
forgetHandlers(node)
|
|
853
|
+
stack.removeLayoutStyle(for: node.view)
|
|
589
854
|
stack.removeArrangedSubview(node.view)
|
|
590
855
|
node.view.removeFromSuperview()
|
|
591
856
|
}
|
|
@@ -599,6 +864,12 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
599
864
|
}
|
|
600
865
|
let id = ObjectIdentifier(node.view)
|
|
601
866
|
handlers.removeValue(forKey: id)
|
|
867
|
+
focusHandlers.removeValue(forKey: id)
|
|
868
|
+
blurHandlers.removeValue(forKey: id)
|
|
869
|
+
submitHandlers.removeValue(forKey: id)
|
|
870
|
+
scrollHandlers.removeValue(forKey: id)
|
|
871
|
+
scrollBeginHandlers.removeValue(forKey: id)
|
|
872
|
+
scrollEndHandlers.removeValue(forKey: id)
|
|
602
873
|
if let recognizer = tapRecognizers.removeValue(forKey: id) {
|
|
603
874
|
node.view.removeGestureRecognizer(recognizer)
|
|
604
875
|
}
|
|
@@ -613,6 +884,16 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
613
884
|
else { handlers.removeValue(forKey: id) }
|
|
614
885
|
}
|
|
615
886
|
|
|
887
|
+
private func updateAuxiliaryHandler(
|
|
888
|
+
_ handler: String?,
|
|
889
|
+
in handlers: inout [ObjectIdentifier: String],
|
|
890
|
+
for view: UIView
|
|
891
|
+
) {
|
|
892
|
+
let id = ObjectIdentifier(view)
|
|
893
|
+
if let handler, !handler.isEmpty { handlers[id] = handler }
|
|
894
|
+
else { handlers.removeValue(forKey: id) }
|
|
895
|
+
}
|
|
896
|
+
|
|
616
897
|
private func updatePressHandler(_ handler: String?, for view: UIView) {
|
|
617
898
|
let id = ObjectIdentifier(view)
|
|
618
899
|
updateHandler(handler, for: view)
|
|
@@ -639,26 +920,19 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
639
920
|
}
|
|
640
921
|
}
|
|
641
922
|
|
|
642
|
-
private func configureStack(_ stack:
|
|
923
|
+
private func configureStack(_ stack: CraftNativeFlowView, style: [String: Any]) {
|
|
643
924
|
let direction = style["flexDirection"] as? String
|
|
644
925
|
stack.axis = direction == "row" || direction == "row-reverse" ? .horizontal : .vertical
|
|
645
|
-
stack.
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
case "baseline": stack.alignment = .firstBaseline
|
|
652
|
-
default: stack.alignment = .fill
|
|
653
|
-
}
|
|
654
|
-
switch style["justifyContent"] as? String {
|
|
655
|
-
case "space-between", "space-around", "space-evenly": stack.distribution = .equalSpacing
|
|
656
|
-
default: stack.distribution = .fill
|
|
657
|
-
}
|
|
926
|
+
stack.wrap = style["flexWrap"] as? String == "wrap" || style["flexWrap"] as? Bool == true
|
|
927
|
+
stack.alignItems = style["alignItems"] as? String ?? "stretch"
|
|
928
|
+
stack.justifyContent = style["justifyContent"] as? String ?? "flex-start"
|
|
929
|
+
stack.gap = number(style["gap"]) ?? 0
|
|
930
|
+
stack.rowGap = number(style["rowGap"])
|
|
931
|
+
stack.columnGap = number(style["columnGap"])
|
|
658
932
|
let padding = number(style["padding"]) ?? 0
|
|
659
933
|
let horizontal = number(style["paddingHorizontal"]) ?? padding
|
|
660
934
|
let vertical = number(style["paddingVertical"]) ?? padding
|
|
661
|
-
stack.
|
|
935
|
+
stack.padding = UIEdgeInsets(
|
|
662
936
|
top: number(style["paddingTop"]) ?? vertical,
|
|
663
937
|
left: number(style["paddingLeft"]) ?? horizontal,
|
|
664
938
|
bottom: number(style["paddingBottom"]) ?? vertical,
|
|
@@ -666,15 +940,13 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
666
940
|
)
|
|
667
941
|
}
|
|
668
942
|
|
|
669
|
-
private func addJustificationSpacers(to stack:
|
|
670
|
-
guard !stack.arrangedSubviews.isEmpty else { return }
|
|
671
|
-
let leading = value == "center" || value == "flex-end"
|
|
672
|
-
let trailing = value == nil || value == "flex-start" || value == "center"
|
|
673
|
-
if leading { stack.insertArrangedSubview(CraftNativeFlexSpacer(), at: 0) }
|
|
674
|
-
if trailing { stack.addArrangedSubview(CraftNativeFlexSpacer()) }
|
|
675
|
-
}
|
|
943
|
+
private func addJustificationSpacers(to stack: CraftNativeFlowView, value: String?) { }
|
|
676
944
|
|
|
677
945
|
private func applyViewStyle(_ style: [String: Any], to view: UIView, node: RenderedNode) {
|
|
946
|
+
let width = style["minWidth"] == nil && style["maxWidth"] == nil ? number(style["width"]) : nil
|
|
947
|
+
let height = style["minHeight"] == nil && style["maxHeight"] == nil ? number(style["height"]) : nil
|
|
948
|
+
updateDimension(width, constraint: &node.widthConstraint, anchor: view.widthAnchor)
|
|
949
|
+
updateDimension(height, constraint: &node.heightConstraint, anchor: view.heightAnchor)
|
|
678
950
|
view.backgroundColor = color(style["backgroundColor"]) ?? .clear
|
|
679
951
|
view.alpha = number(style["opacity"]) ?? 1
|
|
680
952
|
view.isHidden = style["display"] as? String == "none"
|
|
@@ -682,8 +954,6 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
682
954
|
view.layer.borderWidth = number(style["borderWidth"]) ?? 0
|
|
683
955
|
view.layer.borderColor = (color(style["borderColor"]) ?? .clear).cgColor
|
|
684
956
|
view.clipsToBounds = style["overflow"] as? String == "hidden"
|
|
685
|
-
updateDimension(number(style["width"]), constraint: &node.widthConstraint, anchor: view.widthAnchor)
|
|
686
|
-
updateDimension(number(style["height"]), constraint: &node.heightConstraint, anchor: view.heightAnchor)
|
|
687
957
|
}
|
|
688
958
|
|
|
689
959
|
private func configureText(_ label: UILabel, text: String, style: [String: Any]) {
|
|
@@ -822,16 +1092,27 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
822
1092
|
private func applyAccessibility(_ props: [String: Any], type: String, to view: UIView) {
|
|
823
1093
|
view.accessibilityLabel = props["accessibilityLabel"] as? String
|
|
824
1094
|
view.accessibilityHint = props["accessibilityHint"] as? String
|
|
1095
|
+
if let value = props["accessibilityValue"] {
|
|
1096
|
+
view.accessibilityValue = (value as? String) ?? (value as? NSNumber)?.stringValue
|
|
1097
|
+
}
|
|
825
1098
|
let role = props["accessibilityRole"] as? String
|
|
826
|
-
view.isAccessibilityElement = view.accessibilityLabel != nil || role != nil || ["Text", "Button", "Image", "TextInput"].contains(type)
|
|
1099
|
+
view.isAccessibilityElement = role != "none" && (view.accessibilityLabel != nil || role != nil || ["Text", "Button", "Image", "TextInput"].contains(type))
|
|
1100
|
+
var traits: UIAccessibilityTraits = []
|
|
827
1101
|
switch role ?? type.lowercased() {
|
|
828
|
-
case "button":
|
|
829
|
-
case "image":
|
|
830
|
-
case "header":
|
|
831
|
-
case "link":
|
|
832
|
-
case "search":
|
|
833
|
-
default:
|
|
1102
|
+
case "button": traits.insert(.button)
|
|
1103
|
+
case "image": traits.insert(.image)
|
|
1104
|
+
case "header": traits.insert(.header)
|
|
1105
|
+
case "link": traits.insert(.link)
|
|
1106
|
+
case "search": traits.insert(.searchField)
|
|
1107
|
+
default: break
|
|
1108
|
+
}
|
|
1109
|
+
if let state = props["accessibilityState"] as? [String: Any] {
|
|
1110
|
+
if state["disabled"] as? Bool == true { traits.insert(.notEnabled) }
|
|
1111
|
+
if state["selected"] as? Bool == true { traits.insert(.selected) }
|
|
1112
|
+
if state["checked"] as? Bool == true { traits.insert(.selected) }
|
|
834
1113
|
}
|
|
1114
|
+
if let control = view as? UIControl, !control.isEnabled { traits.insert(.notEnabled) }
|
|
1115
|
+
view.accessibilityTraits = traits
|
|
835
1116
|
}
|
|
836
1117
|
|
|
837
1118
|
private func updateDimension(_ value: CGFloat?, constraint: inout NSLayoutConstraint?, anchor: NSLayoutDimension) {
|
|
@@ -848,13 +1129,40 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
848
1129
|
}
|
|
849
1130
|
|
|
850
1131
|
@objc private func buttonPressed(_ sender: UIButton) {
|
|
1132
|
+
let focused = firstResponder(in: rootStack) ?? lastFocusedInput
|
|
851
1133
|
guard let handler = handlers[ObjectIdentifier(sender)] else { return }
|
|
852
1134
|
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]])
|
|
1135
|
+
restoreFocus(focused)
|
|
1136
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self, weak focused] in
|
|
1137
|
+
self?.restoreFocus(focused)
|
|
1138
|
+
}
|
|
853
1139
|
}
|
|
854
1140
|
|
|
855
1141
|
@objc private func textChanged(_ sender: UITextField) {
|
|
1142
|
+
lastFocusedInput = sender
|
|
1143
|
+
let focused = sender.isFirstResponder ? sender : firstResponder(in: rootStack)
|
|
856
1144
|
guard let handler = handlers[ObjectIdentifier(sender)] else { return }
|
|
857
1145
|
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["text": sender.text ?? ""]])
|
|
1146
|
+
restoreFocus(focused)
|
|
1147
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self, weak focused] in
|
|
1148
|
+
self?.restoreFocus(focused)
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
@objc private func textFocused(_ sender: UITextField) {
|
|
1153
|
+
lastFocusedInput = sender
|
|
1154
|
+
guard let handler = focusHandlers[ObjectIdentifier(sender)] else { return }
|
|
1155
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]])
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
@objc private func textBlurred(_ sender: UITextField) {
|
|
1159
|
+
guard let handler = blurHandlers[ObjectIdentifier(sender)] else { return }
|
|
1160
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["text": sender.text ?? ""]])
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
@objc private func textSubmitted(_ sender: UITextField) {
|
|
1164
|
+
guard let handler = submitHandlers[ObjectIdentifier(sender)] else { return }
|
|
1165
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["text": sender.text ?? ""]])
|
|
858
1166
|
}
|
|
859
1167
|
|
|
860
1168
|
@objc private func viewPressed(_ sender: UITapGestureRecognizer) {
|
|
@@ -862,6 +1170,29 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
862
1170
|
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]])
|
|
863
1171
|
}
|
|
864
1172
|
|
|
1173
|
+
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
1174
|
+
guard let handler = scrollHandlers[ObjectIdentifier(scrollView)] else { return }
|
|
1175
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": scrollEvent(scrollView)])
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
1179
|
+
guard let handler = scrollBeginHandlers[ObjectIdentifier(scrollView)] else { return }
|
|
1180
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": scrollEvent(scrollView)])
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
1184
|
+
guard let handler = scrollEndHandlers[ObjectIdentifier(scrollView)] else { return }
|
|
1185
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": scrollEvent(scrollView)])
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
private func scrollEvent(_ scrollView: UIScrollView) -> [String: Any] {
|
|
1189
|
+
[
|
|
1190
|
+
"contentOffset": ["x": scrollView.contentOffset.x, "y": scrollView.contentOffset.y],
|
|
1191
|
+
"contentSize": ["width": scrollView.contentSize.width, "height": scrollView.contentSize.height],
|
|
1192
|
+
"layoutMeasurement": ["width": scrollView.bounds.width, "height": scrollView.bounds.height],
|
|
1193
|
+
]
|
|
1194
|
+
}
|
|
1195
|
+
|
|
865
1196
|
private func send(type: String, payload: [String: Any], correlationId: String? = nil) {
|
|
866
1197
|
var message: [String: Any] = ["type": type, "payload": payload]
|
|
867
1198
|
if let correlationId = correlationId { message["correlationId"] = correlationId }
|
|
@@ -874,6 +1205,37 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
874
1205
|
(value as? NSNumber).map { CGFloat(truncating: $0) }
|
|
875
1206
|
}
|
|
876
1207
|
|
|
1208
|
+
private func keyboardType(_ value: Any?) -> UIKeyboardType {
|
|
1209
|
+
switch value as? String {
|
|
1210
|
+
case "email-address": return .emailAddress
|
|
1211
|
+
case "numeric": return .numberPad
|
|
1212
|
+
case "phone-pad": return .phonePad
|
|
1213
|
+
case "decimal-pad": return .decimalPad
|
|
1214
|
+
case "url": return .URL
|
|
1215
|
+
default: return .default
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
private func returnKeyType(_ value: Any?) -> UIReturnKeyType {
|
|
1220
|
+
switch value as? String {
|
|
1221
|
+
case "done": return .done
|
|
1222
|
+
case "go": return .go
|
|
1223
|
+
case "next": return .next
|
|
1224
|
+
case "search": return .search
|
|
1225
|
+
case "send": return .send
|
|
1226
|
+
default: return .default
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
private func capitalizationType(_ value: Any?) -> UITextAutocapitalizationType {
|
|
1231
|
+
switch value as? String {
|
|
1232
|
+
case "none": return .none
|
|
1233
|
+
case "words": return .words
|
|
1234
|
+
case "characters": return .allCharacters
|
|
1235
|
+
default: return .sentences
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
877
1239
|
private func color(_ value: Any?) -> UIColor? {
|
|
878
1240
|
guard let hex = value as? String else { return nil }
|
|
879
1241
|
return UIColor(hex: hex)
|