craft-native 0.0.123 → 0.0.124
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/README.md +3 -0
- package/dist/android/src/index.d.ts +3 -0
- package/dist/android/src/index.js +17 -0
- package/dist/android/templates/AndroidManifest.xml.template +1 -1
- package/dist/android/templates/CraftNativeFlatList.kt.template +113 -6
- package/dist/android/templates/MainActivity.kt.template +17 -0
- package/dist/android/templates/MainActivityNative.kt.template +826 -68
- package/dist/api/mobile.d.ts +207 -2
- package/dist/cli.js +1 -1
- package/dist/index.cjs +22 -1
- package/dist/index.js +22 -1
- package/dist/ios/src/index.d.ts +76 -2
- package/dist/ios/src/index.js +134 -43
- package/dist/ios/templates/CraftApp.swift +1086 -67
- package/dist/ios/templates/CraftNativeActions.swift +97 -9
- package/dist/ios/templates/CraftNativeFlatList.swift +92 -12
- package/dist/ios/templates/CraftNativeScreen.swift +876 -64
- package/dist/ios/templates/CraftNativeUI.swift +562 -0
- package/dist/ios/templates/Info.plist.template +2 -1
- package/dist/mobile.js +138 -0
- package/package.json +1 -1
|
@@ -5,6 +5,18 @@ import UIKit
|
|
|
5
5
|
|
|
6
6
|
private let craftNativeCapabilityTimeoutMilliseconds = 30_000
|
|
7
7
|
|
|
8
|
+
private func craftNativeGridColumnCount(_ value: Any?) -> Int {
|
|
9
|
+
if let number = value as? NSNumber { return max(1, number.intValue) }
|
|
10
|
+
guard let text = value as? String else { return 1 }
|
|
11
|
+
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
12
|
+
if trimmed.hasPrefix("repeat(") {
|
|
13
|
+
let digits = trimmed.dropFirst("repeat(".count).prefix { $0.isNumber }
|
|
14
|
+
if let count = Int(digits) { return max(1, count) }
|
|
15
|
+
}
|
|
16
|
+
let tracks = trimmed.split(whereSeparator: { $0 == " " || $0 == "\t" || $0 == "\n" })
|
|
17
|
+
return max(1, tracks.count)
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
/// A WebView-free host for the first stx-native vertical slice. The bundled
|
|
9
21
|
/// JavaScript sends whole, compiled view trees to UIKit and receives control
|
|
10
22
|
/// events and Craft API replies through JavaScriptCore.
|
|
@@ -31,6 +43,16 @@ private struct CraftNativeLayoutStyle {
|
|
|
31
43
|
let bottom: CGFloat?
|
|
32
44
|
let left: CGFloat?
|
|
33
45
|
let alignSelf: String?
|
|
46
|
+
let flexGrow: CGFloat
|
|
47
|
+
let flexShrink: CGFloat
|
|
48
|
+
let flexBasis: CGFloat?
|
|
49
|
+
let marginTop: CGFloat
|
|
50
|
+
let marginRight: CGFloat
|
|
51
|
+
let marginBottom: CGFloat
|
|
52
|
+
let marginLeft: CGFloat
|
|
53
|
+
let display: String
|
|
54
|
+
let gridColumns: Int
|
|
55
|
+
let gridAutoRows: CGFloat?
|
|
34
56
|
|
|
35
57
|
init(_ raw: [String: Any]) {
|
|
36
58
|
func number(_ value: Any?) -> CGFloat? { (value as? NSNumber).map { CGFloat(truncating: $0) } }
|
|
@@ -46,6 +68,19 @@ private struct CraftNativeLayoutStyle {
|
|
|
46
68
|
bottom = number(raw["bottom"])
|
|
47
69
|
left = number(raw["left"])
|
|
48
70
|
alignSelf = raw["alignSelf"] as? String
|
|
71
|
+
flexGrow = number(raw["flexGrow"] ?? raw["flex"]) ?? 0
|
|
72
|
+
flexShrink = number(raw["flexShrink"]) ?? 0
|
|
73
|
+
flexBasis = number(raw["flexBasis"])
|
|
74
|
+
let margin = number(raw["margin"]) ?? 0
|
|
75
|
+
let horizontalMargin = number(raw["marginHorizontal"]) ?? margin
|
|
76
|
+
let verticalMargin = number(raw["marginVertical"]) ?? margin
|
|
77
|
+
marginTop = number(raw["marginTop"]) ?? verticalMargin
|
|
78
|
+
marginRight = number(raw["marginRight"]) ?? horizontalMargin
|
|
79
|
+
marginBottom = number(raw["marginBottom"]) ?? verticalMargin
|
|
80
|
+
marginLeft = number(raw["marginLeft"]) ?? horizontalMargin
|
|
81
|
+
display = raw["display"] as? String ?? "flex"
|
|
82
|
+
gridColumns = craftNativeGridColumnCount(raw["gridTemplateColumns"] ?? raw["gridColumns"])
|
|
83
|
+
gridAutoRows = number(raw["gridAutoRows"])
|
|
49
84
|
}
|
|
50
85
|
}
|
|
51
86
|
|
|
@@ -56,6 +91,9 @@ private final class CraftNativeFlowView: UIStackView {
|
|
|
56
91
|
var gap: CGFloat = 0 { didSet { spacing = gap; invalidateIntrinsicContentSize(); setNeedsLayout() } }
|
|
57
92
|
var rowGap: CGFloat? { didSet { invalidateIntrinsicContentSize(); setNeedsLayout() } }
|
|
58
93
|
var columnGap: CGFloat? { didSet { invalidateIntrinsicContentSize(); setNeedsLayout() } }
|
|
94
|
+
var grid = false { didSet { invalidateIntrinsicContentSize(); setNeedsLayout() } }
|
|
95
|
+
var gridColumns = 1 { didSet { invalidateIntrinsicContentSize(); setNeedsLayout() } }
|
|
96
|
+
var gridAutoRows: CGFloat? { didSet { invalidateIntrinsicContentSize(); setNeedsLayout() } }
|
|
59
97
|
var padding = UIEdgeInsets.zero {
|
|
60
98
|
didSet {
|
|
61
99
|
layoutMargins = padding
|
|
@@ -112,11 +150,20 @@ private final class CraftNativeFlowView: UIStackView {
|
|
|
112
150
|
}
|
|
113
151
|
|
|
114
152
|
override var intrinsicContentSize: CGSize {
|
|
115
|
-
let children = subviews.filter {
|
|
153
|
+
let children = subviews.filter {
|
|
154
|
+
!$0.isHidden && !($0 is CraftNativeFlexSpacer)
|
|
155
|
+
&& (childStyles[ObjectIdentifier($0)] ?? CraftNativeLayoutStyle([:])).position != "absolute"
|
|
156
|
+
}
|
|
116
157
|
guard !children.isEmpty else { return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) }
|
|
158
|
+
if grid { return gridIntrinsicContentSize(children) }
|
|
117
159
|
let sizes = children.map { measuredSize(for: $0, available: CGSize(width: 10_000, height: 10_000)) }
|
|
118
|
-
let
|
|
119
|
-
let
|
|
160
|
+
let styles = children.map { childStyles[ObjectIdentifier($0)] ?? CraftNativeLayoutStyle([:]) }
|
|
161
|
+
let main = sizes.enumerated().map { index, size in
|
|
162
|
+
(axis == .horizontal ? size.width : size.height) + mainMargins(styles[index])
|
|
163
|
+
}.reduce(0, +)
|
|
164
|
+
let cross = sizes.enumerated().map { index, size in
|
|
165
|
+
(axis == .horizontal ? size.height : size.width) + crossMargins(styles[index])
|
|
166
|
+
}.max() ?? 0
|
|
120
167
|
let mainGap = max(0, CGFloat(max(0, children.count - 1))) * mainGapValue
|
|
121
168
|
if axis == .horizontal {
|
|
122
169
|
return CGSize(width: padding.left + padding.right + main + mainGap, height: padding.top + padding.bottom + cross)
|
|
@@ -127,6 +174,10 @@ private final class CraftNativeFlowView: UIStackView {
|
|
|
127
174
|
override func layoutSubviews() {
|
|
128
175
|
super.layoutSubviews()
|
|
129
176
|
let content = bounds.inset(by: padding)
|
|
177
|
+
if grid {
|
|
178
|
+
layoutGrid(content: content)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
130
181
|
let availableMain = axis == .horizontal ? content.width : content.height
|
|
131
182
|
var lines: [([UIView], [CGSize], CGFloat, CGFloat)] = []
|
|
132
183
|
var items: [UIView] = []
|
|
@@ -144,8 +195,8 @@ private final class CraftNativeFlowView: UIStackView {
|
|
|
144
195
|
let style = childStyles[ObjectIdentifier(child)] ?? CraftNativeLayoutStyle([:])
|
|
145
196
|
if style.position == "absolute" { continue }
|
|
146
197
|
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
|
|
198
|
+
let childMain = (axis == .horizontal ? size.width : size.height) + mainMargins(style)
|
|
199
|
+
let childCross = (axis == .horizontal ? size.height : size.width) + crossMargins(style)
|
|
149
200
|
let next = items.isEmpty ? childMain : main + mainGapValue + childMain
|
|
150
201
|
if wrap && !items.isEmpty && next > availableMain { flush() }
|
|
151
202
|
items.append(child); sizes.append(size)
|
|
@@ -156,29 +207,39 @@ private final class CraftNativeFlowView: UIStackView {
|
|
|
156
207
|
|
|
157
208
|
var crossOffset: CGFloat = 0
|
|
158
209
|
for (lineItems, lineSizes, lineMain, lineCross) in lines {
|
|
159
|
-
let
|
|
210
|
+
let styles = lineItems.map { childStyles[ObjectIdentifier($0)] ?? CraftNativeLayoutStyle([:]) }
|
|
211
|
+
let distributed = distributeMainAxisSizes(lineSizes, styles: styles, available: availableMain, lineMain: lineMain)
|
|
212
|
+
let distributedMain = distributed.enumerated().reduce(0) { total, entry in
|
|
213
|
+
total + (axis == .horizontal ? entry.element.width : entry.element.height) + mainMargins(styles[entry.offset])
|
|
214
|
+
} + CGFloat(max(0, lineItems.count - 1)) * mainGapValue
|
|
215
|
+
let free = max(0, availableMain - distributedMain)
|
|
160
216
|
let (leading, between) = distribution(free: free, count: lineItems.count)
|
|
161
217
|
var mainOffset = leading
|
|
162
218
|
for (index, child) in lineItems.enumerated() {
|
|
163
219
|
let style = childStyles[ObjectIdentifier(child)] ?? CraftNativeLayoutStyle([:])
|
|
164
|
-
let size =
|
|
220
|
+
let size = distributed[index]
|
|
165
221
|
let childCross = axis == .horizontal ? size.height : size.width
|
|
222
|
+
let mainLeading = axis == .horizontal ? style.marginLeft : style.marginTop
|
|
223
|
+
let mainTrailing = axis == .horizontal ? style.marginRight : style.marginBottom
|
|
224
|
+
let crossLeading = axis == .horizontal ? style.marginTop : style.marginLeft
|
|
225
|
+
let crossTrailing = axis == .horizontal ? style.marginBottom : style.marginRight
|
|
166
226
|
let alignment = style.alignSelf ?? alignItems
|
|
167
|
-
let crossSize: CGFloat = alignment == "stretch" && styleCrossDimension(style) == nil
|
|
227
|
+
let crossSize: CGFloat = alignment == "stretch" && styleCrossDimension(style) == nil
|
|
228
|
+
? max(0, lineCross - crossLeading - crossTrailing) : childCross
|
|
168
229
|
let crossPosition: CGFloat
|
|
169
230
|
switch alignment {
|
|
170
|
-
case "center": crossPosition = (lineCross - crossSize) / 2
|
|
171
|
-
case "flex-end": crossPosition = lineCross - crossSize
|
|
172
|
-
default: crossPosition =
|
|
231
|
+
case "center": crossPosition = crossLeading + (lineCross - crossLeading - crossTrailing - crossSize) / 2
|
|
232
|
+
case "flex-end": crossPosition = lineCross - crossTrailing - crossSize
|
|
233
|
+
default: crossPosition = crossLeading
|
|
173
234
|
}
|
|
174
235
|
let frame: CGRect
|
|
175
236
|
if axis == .horizontal {
|
|
176
|
-
frame = CGRect(x: content.minX + mainOffset, y: content.minY + crossOffset + crossPosition, width: size.width, height: crossSize)
|
|
237
|
+
frame = CGRect(x: content.minX + mainOffset + mainLeading, y: content.minY + crossOffset + crossPosition, width: size.width, height: crossSize)
|
|
177
238
|
} else {
|
|
178
|
-
frame = CGRect(x: content.minX + crossOffset + crossPosition, y: content.minY + mainOffset, width: crossSize, height: size.height)
|
|
239
|
+
frame = CGRect(x: content.minX + crossOffset + crossPosition, y: content.minY + mainOffset + mainLeading, width: crossSize, height: size.height)
|
|
179
240
|
}
|
|
180
241
|
child.frame = frame.integral
|
|
181
|
-
mainOffset += (axis == .horizontal ? size.width : size.height) + mainGapValue + between
|
|
242
|
+
mainOffset += mainLeading + (axis == .horizontal ? size.width : size.height) + mainTrailing + mainGapValue + between
|
|
182
243
|
}
|
|
183
244
|
crossOffset += lineCross + crossGapValue
|
|
184
245
|
}
|
|
@@ -195,9 +256,93 @@ private final class CraftNativeFlowView: UIStackView {
|
|
|
195
256
|
}
|
|
196
257
|
}
|
|
197
258
|
|
|
259
|
+
private func gridIntrinsicContentSize(_ children: [UIView]) -> CGSize {
|
|
260
|
+
let columns = max(1, gridColumns)
|
|
261
|
+
let horizontalGap = columnGap ?? gap
|
|
262
|
+
let verticalGap = rowGap ?? gap
|
|
263
|
+
var columnWidth: CGFloat = 0
|
|
264
|
+
var rowHeights: [CGFloat] = []
|
|
265
|
+
let flowChildren = children.filter {
|
|
266
|
+
(childStyles[ObjectIdentifier($0)] ?? CraftNativeLayoutStyle([:])).position != "absolute"
|
|
267
|
+
}
|
|
268
|
+
for (index, child) in flowChildren.enumerated() {
|
|
269
|
+
let style = childStyles[ObjectIdentifier(child)] ?? CraftNativeLayoutStyle([:])
|
|
270
|
+
let size = measuredSize(for: child, available: CGSize(width: 10_000, height: 10_000), style: style)
|
|
271
|
+
let row = index / columns
|
|
272
|
+
while rowHeights.count <= row { rowHeights.append(gridAutoRows ?? 0) }
|
|
273
|
+
columnWidth = max(columnWidth, size.width + style.marginLeft + style.marginRight)
|
|
274
|
+
rowHeights[row] = max(rowHeights[row], size.height + style.marginTop + style.marginBottom)
|
|
275
|
+
}
|
|
276
|
+
let width = padding.left + padding.right + CGFloat(columns) * columnWidth + CGFloat(max(0, columns - 1)) * horizontalGap
|
|
277
|
+
let height = padding.top + padding.bottom + rowHeights.reduce(0, +) + CGFloat(max(0, rowHeights.count - 1)) * verticalGap
|
|
278
|
+
return CGSize(width: width, height: height)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private func layoutGrid(content: CGRect) {
|
|
282
|
+
let children = subviews.filter { !$0.isHidden && !($0 is CraftNativeFlexSpacer) }
|
|
283
|
+
let flowChildren = children.filter {
|
|
284
|
+
(childStyles[ObjectIdentifier($0)] ?? CraftNativeLayoutStyle([:])).position != "absolute"
|
|
285
|
+
}
|
|
286
|
+
let columns = max(1, gridColumns)
|
|
287
|
+
let horizontalGap = columnGap ?? gap
|
|
288
|
+
let verticalGap = rowGap ?? gap
|
|
289
|
+
let cellWidth = max(0, (content.width - CGFloat(max(0, columns - 1)) * horizontalGap) / CGFloat(columns))
|
|
290
|
+
var rowHeights: [CGFloat] = []
|
|
291
|
+
var sizes: [CGSize] = []
|
|
292
|
+
for (index, child) in flowChildren.enumerated() {
|
|
293
|
+
let style = childStyles[ObjectIdentifier(child)] ?? CraftNativeLayoutStyle([:])
|
|
294
|
+
let size = measuredSize(
|
|
295
|
+
for: child,
|
|
296
|
+
available: CGSize(width: max(0, cellWidth - style.marginLeft - style.marginRight), height: content.height),
|
|
297
|
+
style: style
|
|
298
|
+
)
|
|
299
|
+
sizes.append(size)
|
|
300
|
+
let row = index / columns
|
|
301
|
+
while rowHeights.count <= row { rowHeights.append(gridAutoRows ?? 0) }
|
|
302
|
+
rowHeights[row] = max(rowHeights[row], size.height + style.marginTop + style.marginBottom)
|
|
303
|
+
}
|
|
304
|
+
var rowTop = content.minY
|
|
305
|
+
for (index, child) in flowChildren.enumerated() {
|
|
306
|
+
let style = childStyles[ObjectIdentifier(child)] ?? CraftNativeLayoutStyle([:])
|
|
307
|
+
let row = index / columns
|
|
308
|
+
let column = index % columns
|
|
309
|
+
let rowHeight = rowHeights[row]
|
|
310
|
+
let size = sizes[index]
|
|
311
|
+
let align = style.alignSelf ?? alignItems
|
|
312
|
+
let availableWidth = max(0, cellWidth - style.marginLeft - style.marginRight)
|
|
313
|
+
let availableHeight = max(0, rowHeight - style.marginTop - style.marginBottom)
|
|
314
|
+
let width = align == "stretch" && style.width == nil ? availableWidth : min(availableWidth, size.width)
|
|
315
|
+
let height = align == "stretch" && style.height == nil ? availableHeight : min(availableHeight, size.height)
|
|
316
|
+
let x = content.minX + CGFloat(column) * (cellWidth + horizontalGap) + style.marginLeft
|
|
317
|
+
+ (align == "center" ? (availableWidth - width) / 2 : align == "flex-end" ? availableWidth - width : 0)
|
|
318
|
+
let y = rowTop + style.marginTop
|
|
319
|
+
+ (align == "center" ? (availableHeight - height) / 2 : align == "flex-end" ? availableHeight - height : 0)
|
|
320
|
+
child.frame = CGRect(x: x, y: y, width: width, height: height).integral
|
|
321
|
+
if column == columns - 1 || index == flowChildren.count - 1 { rowTop += rowHeight + verticalGap }
|
|
322
|
+
}
|
|
323
|
+
for child in children {
|
|
324
|
+
let style = childStyles[ObjectIdentifier(child)] ?? CraftNativeLayoutStyle([:])
|
|
325
|
+
guard style.position == "absolute" else { continue }
|
|
326
|
+
let size = measuredSize(for: child, available: content.size, style: style)
|
|
327
|
+
let width = style.left != nil && style.right != nil ? max(0, content.width - (style.left ?? 0) - (style.right ?? 0)) : size.width
|
|
328
|
+
let height = style.top != nil && style.bottom != nil ? max(0, content.height - (style.top ?? 0) - (style.bottom ?? 0)) : size.height
|
|
329
|
+
let x = style.left ?? (style.right.map { content.width - $0 - width } ?? 0)
|
|
330
|
+
let y = style.top ?? (style.bottom.map { content.height - $0 - height } ?? 0)
|
|
331
|
+
child.frame = CGRect(x: content.minX + x, y: content.minY + y, width: width, height: height).integral
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
198
335
|
private var mainGapValue: CGFloat { axis == .horizontal ? (columnGap ?? gap) : (rowGap ?? gap) }
|
|
199
336
|
private var crossGapValue: CGFloat { axis == .horizontal ? (rowGap ?? 0) : (columnGap ?? 0) }
|
|
200
337
|
|
|
338
|
+
private func mainMargins(_ style: CraftNativeLayoutStyle) -> CGFloat {
|
|
339
|
+
axis == .horizontal ? style.marginLeft + style.marginRight : style.marginTop + style.marginBottom
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private func crossMargins(_ style: CraftNativeLayoutStyle) -> CGFloat {
|
|
343
|
+
axis == .horizontal ? style.marginTop + style.marginBottom : style.marginLeft + style.marginRight
|
|
344
|
+
}
|
|
345
|
+
|
|
201
346
|
private func styleCrossDimension(_ style: CraftNativeLayoutStyle) -> CGFloat? {
|
|
202
347
|
axis == .horizontal ? style.height : style.width
|
|
203
348
|
}
|
|
@@ -206,11 +351,31 @@ private final class CraftNativeFlowView: UIStackView {
|
|
|
206
351
|
let style = style ?? childStyles[ObjectIdentifier(view)] ?? CraftNativeLayoutStyle([:])
|
|
207
352
|
let intrinsic = view.intrinsicContentSize
|
|
208
353
|
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))
|
|
354
|
+
let width = (axis == .horizontal ? style.flexBasis : nil) ?? style.width ?? (intrinsic.width > 0 && intrinsic.width != UIView.noIntrinsicMetric ? intrinsic.width : max(0, fitted.width))
|
|
355
|
+
let height = (axis == .vertical ? style.flexBasis : nil) ?? style.height ?? (intrinsic.height > 0 && intrinsic.height != UIView.noIntrinsicMetric ? intrinsic.height : max(0, fitted.height))
|
|
211
356
|
return CGSize(width: clamp(width, min: style.minWidth, max: style.maxWidth), height: clamp(height, min: style.minHeight, max: style.maxHeight))
|
|
212
357
|
}
|
|
213
358
|
|
|
359
|
+
private func distributeMainAxisSizes(
|
|
360
|
+
_ sizes: [CGSize],
|
|
361
|
+
styles: [CraftNativeLayoutStyle],
|
|
362
|
+
available: CGFloat,
|
|
363
|
+
lineMain: CGFloat
|
|
364
|
+
) -> [CGSize] {
|
|
365
|
+
let free = available - lineMain
|
|
366
|
+
let factors = free >= 0 ? styles.map(\.flexGrow) : styles.map(\.flexShrink)
|
|
367
|
+
let total = factors.reduce(0, +)
|
|
368
|
+
guard total > 0, free != 0 else { return sizes }
|
|
369
|
+
return sizes.enumerated().map { index, size in
|
|
370
|
+
let delta = free * factors[index] / total
|
|
371
|
+
let style = styles[index]
|
|
372
|
+
if axis == .horizontal {
|
|
373
|
+
return CGSize(width: clamp(max(0, size.width + delta), min: style.minWidth, max: style.maxWidth), height: size.height)
|
|
374
|
+
}
|
|
375
|
+
return CGSize(width: size.width, height: clamp(max(0, size.height + delta), min: style.minHeight, max: style.maxHeight))
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
214
379
|
private func clamp(_ value: CGFloat, min lower: CGFloat?, max upper: CGFloat?) -> CGFloat {
|
|
215
380
|
var result = value
|
|
216
381
|
if let lower { result = max(result, lower) }
|
|
@@ -264,7 +429,60 @@ private final class CraftNativeScrollView: UIScrollView {
|
|
|
264
429
|
|
|
265
430
|
private final class CraftNativeFlexSpacer: UIView {}
|
|
266
431
|
|
|
267
|
-
final class
|
|
432
|
+
private final class CraftNativeTextView: UITextView {
|
|
433
|
+
private let placeholderLabel = UILabel()
|
|
434
|
+
var placeholder: String? {
|
|
435
|
+
didSet { refreshPlaceholder() }
|
|
436
|
+
}
|
|
437
|
+
var placeholderColor: UIColor? {
|
|
438
|
+
didSet { placeholderLabel.textColor = placeholderColor ?? .placeholderText }
|
|
439
|
+
}
|
|
440
|
+
var placeholderFont: UIFont? {
|
|
441
|
+
didSet { placeholderLabel.font = placeholderFont ?? .systemFont(ofSize: 16) }
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
override init(frame: CGRect, textContainer: NSTextContainer?) {
|
|
445
|
+
super.init(frame: frame, textContainer: textContainer)
|
|
446
|
+
configurePlaceholder()
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
convenience init() {
|
|
450
|
+
self.init(frame: .zero, textContainer: nil)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
required init?(coder: NSCoder) {
|
|
454
|
+
super.init(coder: coder)
|
|
455
|
+
configurePlaceholder()
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
override func layoutSubviews() {
|
|
459
|
+
super.layoutSubviews()
|
|
460
|
+
let inset = textContainerInset
|
|
461
|
+
let padding = textContainer.lineFragmentPadding
|
|
462
|
+
placeholderLabel.frame = CGRect(
|
|
463
|
+
x: inset.left + padding,
|
|
464
|
+
y: inset.top,
|
|
465
|
+
width: max(0, bounds.width - inset.left - inset.right - padding * 2),
|
|
466
|
+
height: max(0, bounds.height - inset.top - inset.bottom)
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
func refreshPlaceholder() {
|
|
471
|
+
placeholderLabel.text = placeholder
|
|
472
|
+
placeholderLabel.isHidden = !(text?.isEmpty ?? true) || placeholder?.isEmpty != false
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private func configurePlaceholder() {
|
|
476
|
+
placeholderLabel.numberOfLines = 0
|
|
477
|
+
placeholderLabel.font = font
|
|
478
|
+
placeholderLabel.textColor = .placeholderText
|
|
479
|
+
placeholderLabel.isUserInteractionEnabled = false
|
|
480
|
+
addSubview(placeholderLabel)
|
|
481
|
+
refreshPlaceholder()
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate, UITextFieldDelegate, UITextViewDelegate {
|
|
268
486
|
private final class RenderedNode {
|
|
269
487
|
let identity: String
|
|
270
488
|
let type: String
|
|
@@ -290,15 +508,33 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
290
508
|
private var handlers: [ObjectIdentifier: String] = [:]
|
|
291
509
|
private var focusHandlers: [ObjectIdentifier: String] = [:]
|
|
292
510
|
private var blurHandlers: [ObjectIdentifier: String] = [:]
|
|
511
|
+
private var endEditingHandlers: [ObjectIdentifier: String] = [:]
|
|
293
512
|
private var submitHandlers: [ObjectIdentifier: String] = [:]
|
|
513
|
+
private var inputIdentities: [ObjectIdentifier: String] = [:]
|
|
514
|
+
private var inputDrafts: [String: String] = [:]
|
|
515
|
+
private var longPressHandlers: [ObjectIdentifier: String] = [:]
|
|
516
|
+
private var textMaxLengths: [ObjectIdentifier: Int] = [:]
|
|
294
517
|
private var scrollHandlers: [ObjectIdentifier: String] = [:]
|
|
295
518
|
private var scrollBeginHandlers: [ObjectIdentifier: String] = [:]
|
|
296
519
|
private var scrollEndHandlers: [ObjectIdentifier: String] = [:]
|
|
520
|
+
private var scrollMomentumBeginHandlers: [ObjectIdentifier: String] = [:]
|
|
521
|
+
private var scrollMomentumEndHandlers: [ObjectIdentifier: String] = [:]
|
|
522
|
+
private var sliderCompleteHandlers: [ObjectIdentifier: String] = [:]
|
|
523
|
+
private var sliderSteps: [ObjectIdentifier: Float] = [:]
|
|
524
|
+
private var layoutHandlers: [ObjectIdentifier: String] = [:]
|
|
525
|
+
private var lastLayoutFrames: [ObjectIdentifier: CGRect] = [:]
|
|
297
526
|
private var tapRecognizers: [ObjectIdentifier: UITapGestureRecognizer] = [:]
|
|
527
|
+
private var longPressRecognizers: [ObjectIdentifier: UILongPressGestureRecognizer] = [:]
|
|
298
528
|
private var imageSources: [ObjectIdentifier: String] = [:]
|
|
299
529
|
private var imageTasks: [ObjectIdentifier: URLSessionDataTask] = [:]
|
|
530
|
+
private var imageErrors: [ObjectIdentifier: String] = [:]
|
|
531
|
+
private var tintedImages = Set<ObjectIdentifier>()
|
|
532
|
+
private var imageLoadStartHandlers: [ObjectIdentifier: String] = [:]
|
|
533
|
+
private var imageLoadHandlers: [ObjectIdentifier: String] = [:]
|
|
534
|
+
private var imageLoadEndHandlers: [ObjectIdentifier: String] = [:]
|
|
535
|
+
private var imageErrorHandlers: [ObjectIdentifier: String] = [:]
|
|
300
536
|
private var renderedRoot: RenderedNode?
|
|
301
|
-
private weak var lastFocusedInput:
|
|
537
|
+
private weak var lastFocusedInput: UIView?
|
|
302
538
|
private var flatListRows: [ObjectIdentifier: [String: RenderedNode]] = [:]
|
|
303
539
|
private var flatListOwners: [String: String] = [:]
|
|
304
540
|
private let mutationDocument = CraftNativeMutationDocument()
|
|
@@ -346,6 +582,38 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
346
582
|
observeNativeEvents()
|
|
347
583
|
}
|
|
348
584
|
|
|
585
|
+
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
586
|
+
super.traitCollectionDidChange(previousTraitCollection)
|
|
587
|
+
guard previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle else { return }
|
|
588
|
+
refreshTraitDefaults()
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
override func viewWillAppear(_ animated: Bool) {
|
|
592
|
+
super.viewWillAppear(animated)
|
|
593
|
+
refreshTraitDefaults()
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private func refreshTraitDefaults() {
|
|
597
|
+
view.backgroundColor = config.resolvedBackgroundColor
|
|
598
|
+
if let document = mutationDocument.node("root") {
|
|
599
|
+
renderCommitted(document)
|
|
600
|
+
} else {
|
|
601
|
+
rootStack.setNeedsLayout()
|
|
602
|
+
}
|
|
603
|
+
refreshTraitDefaults(in: renderedRoot)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private func refreshTraitDefaults(in node: RenderedNode?) {
|
|
607
|
+
guard let node else { return }
|
|
608
|
+
(node.view as? CraftNativeFlatList)?.refreshThemeDefaults()
|
|
609
|
+
node.children.forEach { refreshTraitDefaults(in: $0) }
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
override func viewDidLayoutSubviews() {
|
|
613
|
+
super.viewDidLayoutSubviews()
|
|
614
|
+
emitLayoutEvents()
|
|
615
|
+
}
|
|
616
|
+
|
|
349
617
|
private func setupJavaScript() {
|
|
350
618
|
if let routeName = routeName {
|
|
351
619
|
jsContext.setObject(routeName, forKeyedSubscript: "__stxNativeRoute" as NSString)
|
|
@@ -610,10 +878,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
610
878
|
let events = node["events"] as? [String: String] ?? [:]
|
|
611
879
|
let children = node["children"] as? [Any] ?? []
|
|
612
880
|
let current: RenderedNode
|
|
613
|
-
|
|
881
|
+
let wantsMultilineTextView = type == "TextInput" && props["multiline"] as? Bool == true
|
|
882
|
+
let canReuseInput = type != "TextInput" || ((previous?.view is UITextView) == wantsMultilineTextView)
|
|
883
|
+
if let previous = previous, previous.identity == identity, previous.type == type, canReuseInput {
|
|
614
884
|
current = previous
|
|
615
885
|
} else {
|
|
616
|
-
current = RenderedNode(identity: identity, type: type, view: makeView(type))
|
|
886
|
+
current = RenderedNode(identity: identity, type: type, view: makeView(type, props: props))
|
|
617
887
|
}
|
|
618
888
|
|
|
619
889
|
let result = current.view
|
|
@@ -624,26 +894,68 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
624
894
|
result.setContentHuggingPriority(.required, for: .vertical)
|
|
625
895
|
}
|
|
626
896
|
applyViewStyle(style, to: result, node: current)
|
|
897
|
+
updateLayoutHandler(events["onLayout"], for: result)
|
|
627
898
|
|
|
628
899
|
switch type {
|
|
629
900
|
case "Text":
|
|
630
901
|
let label = result as! UILabel
|
|
902
|
+
label.numberOfLines = max(0, (props["numberOfLines"] as? NSNumber)?.intValue ?? 0)
|
|
903
|
+
label.lineBreakMode = textLineBreakMode(props["ellipsizeMode"])
|
|
631
904
|
configureText(label, text: children.compactMap { $0 as? String }.joined(), style: style)
|
|
632
|
-
case "Button":
|
|
905
|
+
case "Button", "Link":
|
|
633
906
|
let button = result as! UIButton
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
907
|
+
let title = props["title"] as? String ?? children.compactMap { $0 as? String }.joined()
|
|
908
|
+
let transformedTitle = transformedText(title, style: style)
|
|
909
|
+
let titleColor = color(props["color"]) ?? color(style["color"]) ?? .systemBlue
|
|
910
|
+
let font = textFont(
|
|
637
911
|
style,
|
|
638
|
-
default:
|
|
912
|
+
default: .systemFont(ofSize: 16)
|
|
639
913
|
)
|
|
640
|
-
|
|
914
|
+
button.setAttributedTitle(NSAttributedString(
|
|
915
|
+
string: transformedTitle,
|
|
916
|
+
attributes: buttonTitleAttributes(style, color: titleColor, font: font, forceUnderline: type == "Link")
|
|
917
|
+
), for: .normal)
|
|
918
|
+
button.contentHorizontalAlignment = buttonAlignment(style["textAlign"])
|
|
919
|
+
button.isEnabled = props["disabled"] as? Bool != true
|
|
920
|
+
updateHandler(nonEmptyHandler(events["onPress"]) ?? nonEmptyHandler(events["onClick"]), for: button)
|
|
641
921
|
case "TextInput":
|
|
922
|
+
if let textView = result as? UITextView {
|
|
923
|
+
inputIdentities[ObjectIdentifier(textView)] = current.identity
|
|
924
|
+
configureTextView(
|
|
925
|
+
textView,
|
|
926
|
+
props: props,
|
|
927
|
+
style: style,
|
|
928
|
+
events: events,
|
|
929
|
+
defaultValue: previous?.view === textView
|
|
930
|
+
? nil
|
|
931
|
+
: inputDrafts[current.identity] ?? props["defaultValue"] as? String
|
|
932
|
+
)
|
|
933
|
+
break
|
|
934
|
+
}
|
|
642
935
|
let field = result as! UITextField
|
|
643
936
|
field.placeholder = props["placeholder"] as? String
|
|
937
|
+
if let placeholder = field.placeholder,
|
|
938
|
+
let placeholderColor = color(props["placeholderTextColor"]) {
|
|
939
|
+
field.attributedPlaceholder = NSAttributedString(
|
|
940
|
+
string: placeholder,
|
|
941
|
+
attributes: [.foregroundColor: placeholderColor]
|
|
942
|
+
)
|
|
943
|
+
} else {
|
|
944
|
+
field.attributedPlaceholder = nil
|
|
945
|
+
}
|
|
946
|
+
field.tintColor = color(props["selectionColor"])
|
|
947
|
+
let fieldId = ObjectIdentifier(field)
|
|
948
|
+
inputIdentities[fieldId] = current.identity
|
|
949
|
+
if let maxLength = (props["maxLength"] as? NSNumber)?.intValue, maxLength > 0 {
|
|
950
|
+
textMaxLengths[fieldId] = maxLength
|
|
951
|
+
} else {
|
|
952
|
+
textMaxLengths.removeValue(forKey: fieldId)
|
|
953
|
+
}
|
|
954
|
+
field.delegate = self
|
|
644
955
|
field.textColor = color(style["color"]) ?? .label
|
|
645
|
-
field.font = textFont(style, default:
|
|
956
|
+
field.font = textFont(style, default: .systemFont(ofSize: 16))
|
|
646
957
|
field.textAlignment = textAlignment(style["textAlign"])
|
|
958
|
+
field.defaultTextAttributes = inputTextAttributes(style, font: field.font ?? .systemFont(ofSize: 16), alignment: field.textAlignment)
|
|
647
959
|
let desiredKeyboardType = keyboardType(props["keyboardType"])
|
|
648
960
|
if field.keyboardType != desiredKeyboardType { field.keyboardType = desiredKeyboardType }
|
|
649
961
|
let desiredReturnKeyType = returnKeyType(props["returnKeyType"])
|
|
@@ -656,26 +968,99 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
656
968
|
if field.isSecureTextEntry != desiredSecureEntry { field.isSecureTextEntry = desiredSecureEntry }
|
|
657
969
|
let desiredEnabled = props["editable"] as? Bool != false
|
|
658
970
|
if field.isEnabled != desiredEnabled { field.isEnabled = desiredEnabled }
|
|
659
|
-
|
|
660
|
-
|
|
971
|
+
if let value = props["value"] as? String {
|
|
972
|
+
updateField(field, value: value)
|
|
973
|
+
} else if previous?.view !== field {
|
|
974
|
+
updateField(field, value: inputDrafts[current.identity] ?? props["defaultValue"] as? String)
|
|
975
|
+
}
|
|
976
|
+
updateHandler(nonEmptyHandler(events["onChange"]) ?? nonEmptyHandler(events["onChangeText"]), for: field)
|
|
661
977
|
updateAuxiliaryHandler(events["onFocus"], in: &focusHandlers, for: field)
|
|
662
|
-
updateAuxiliaryHandler(events["onBlur"]
|
|
978
|
+
updateAuxiliaryHandler(events["onBlur"], in: &blurHandlers, for: field)
|
|
979
|
+
updateAuxiliaryHandler(events["onEndEditing"], in: &endEditingHandlers, for: field)
|
|
663
980
|
updateAuxiliaryHandler(events["onSubmitEditing"], in: &submitHandlers, for: field)
|
|
664
981
|
if props["autoFocus"] as? Bool == true, !field.isFirstResponder {
|
|
665
982
|
DispatchQueue.main.async { _ = field.becomeFirstResponder() }
|
|
666
983
|
}
|
|
984
|
+
case "Switch":
|
|
985
|
+
let toggle = result as! UISwitch
|
|
986
|
+
toggle.isOn = props["value"] as? Bool ?? props["checked"] as? Bool ?? false
|
|
987
|
+
toggle.isEnabled = props["disabled"] as? Bool != true
|
|
988
|
+
if let track = props["trackColor"] as? [String: Any] {
|
|
989
|
+
toggle.onTintColor = color(track["true"])
|
|
990
|
+
toggle.tintColor = color(track["false"])
|
|
991
|
+
} else {
|
|
992
|
+
toggle.onTintColor = nil
|
|
993
|
+
toggle.tintColor = nil
|
|
994
|
+
}
|
|
995
|
+
toggle.thumbTintColor = color(props["thumbColor"])
|
|
996
|
+
if let background = color(props["ios_backgroundColor"]) { toggle.tintColor = background }
|
|
997
|
+
updateHandler(nonEmptyHandler(events["onValueChange"]) ?? nonEmptyHandler(events["onChange"]), for: toggle)
|
|
998
|
+
case "Slider":
|
|
999
|
+
let slider = result as! UISlider
|
|
1000
|
+
slider.minimumValue = (props["minimumValue"] as? NSNumber)?.floatValue ?? 0
|
|
1001
|
+
slider.maximumValue = max(slider.minimumValue + .leastNonzeroMagnitude, (props["maximumValue"] as? NSNumber)?.floatValue ?? 1)
|
|
1002
|
+
let value = (props["value"] as? NSNumber)?.floatValue ?? slider.minimumValue
|
|
1003
|
+
if let step = (props["step"] as? NSNumber)?.floatValue, step > 0 { sliderSteps[ObjectIdentifier(slider)] = step }
|
|
1004
|
+
else { sliderSteps.removeValue(forKey: ObjectIdentifier(slider)) }
|
|
1005
|
+
slider.value = snappedSliderValue(value, for: slider)
|
|
1006
|
+
slider.minimumTrackTintColor = color(props["minimumTrackTintColor"])
|
|
1007
|
+
slider.maximumTrackTintColor = color(props["maximumTrackTintColor"])
|
|
1008
|
+
slider.thumbTintColor = color(props["thumbTintColor"])
|
|
1009
|
+
slider.isEnabled = props["disabled"] as? Bool != true
|
|
1010
|
+
updateHandler(nonEmptyHandler(events["onValueChange"]) ?? nonEmptyHandler(events["onChange"]), for: slider)
|
|
1011
|
+
updateAuxiliaryHandler(events["onSlidingComplete"], in: &sliderCompleteHandlers, for: slider)
|
|
1012
|
+
case "ActivityIndicator":
|
|
1013
|
+
let indicator = result as! UIActivityIndicatorView
|
|
1014
|
+
let requestedSize = props["size"]
|
|
1015
|
+
indicator.style = (requestedSize as? String) == "large" ? .large : .medium
|
|
1016
|
+
let scale: CGFloat
|
|
1017
|
+
if let numericSize = requestedSize as? NSNumber {
|
|
1018
|
+
scale = max(0.5, CGFloat(numericSize.doubleValue / 20.0))
|
|
1019
|
+
} else if (requestedSize as? String) == "small" {
|
|
1020
|
+
scale = 0.75
|
|
1021
|
+
} else {
|
|
1022
|
+
scale = 1
|
|
1023
|
+
}
|
|
1024
|
+
indicator.transform = CGAffineTransform(scaleX: scale, y: scale)
|
|
1025
|
+
indicator.color = color(props["color"]) ?? .tintColor
|
|
1026
|
+
indicator.hidesWhenStopped = props["hidesWhenStopped"] as? Bool ?? true
|
|
1027
|
+
if props["animating"] as? Bool == false {
|
|
1028
|
+
indicator.stopAnimating()
|
|
1029
|
+
} else {
|
|
1030
|
+
indicator.startAnimating()
|
|
1031
|
+
}
|
|
667
1032
|
case "Image":
|
|
668
1033
|
let image = result as! UIImageView
|
|
669
|
-
|
|
1034
|
+
let imageID = ObjectIdentifier(image)
|
|
1035
|
+
if let tint = color(style["tintColor"]) {
|
|
1036
|
+
tintedImages.insert(imageID)
|
|
1037
|
+
image.tintColor = tint
|
|
1038
|
+
} else {
|
|
1039
|
+
tintedImages.remove(imageID)
|
|
1040
|
+
image.tintColor = nil
|
|
1041
|
+
}
|
|
1042
|
+
if let current = image.image { image.image = imageForDisplay(current, view: image) }
|
|
1043
|
+
let requestedResizeMode = style["resizeMode"] as? String ?? props["resizeMode"] as? String
|
|
1044
|
+
image.contentMode = imageContentMode(requestedResizeMode)
|
|
670
1045
|
image.clipsToBounds = image.contentMode == .scaleAspectFill
|
|
1046
|
+
updateAuxiliaryHandler(events["onLoadStart"], in: &imageLoadStartHandlers, for: image)
|
|
1047
|
+
updateAuxiliaryHandler(events["onLoad"], in: &imageLoadHandlers, for: image)
|
|
1048
|
+
updateAuxiliaryHandler(events["onLoadEnd"], in: &imageLoadEndHandlers, for: image)
|
|
1049
|
+
updateAuxiliaryHandler(events["onError"], in: &imageErrorHandlers, for: image)
|
|
671
1050
|
updateImage(image, source: props["source"])
|
|
672
1051
|
case "ScrollView":
|
|
673
1052
|
let scroll = result as! CraftNativeScrollView
|
|
674
|
-
let
|
|
1053
|
+
let flexDirection = style["flexDirection"] as? String
|
|
1054
|
+
let direction = (props["horizontal"] as? Bool) == true
|
|
1055
|
+
|| flexDirection == "row"
|
|
1056
|
+
|| flexDirection == "row-reverse"
|
|
675
1057
|
? NSLayoutConstraint.Axis.horizontal : .vertical
|
|
676
1058
|
scroll.setAxis(direction)
|
|
677
1059
|
scroll.delegate = self
|
|
678
1060
|
scroll.isScrollEnabled = props["scrollEnabled"] as? Bool != false
|
|
1061
|
+
scroll.bounces = props["bounces"] as? Bool ?? true
|
|
1062
|
+
scroll.isPagingEnabled = props["pagingEnabled"] as? Bool ?? false
|
|
1063
|
+
scroll.keyboardDismissMode = keyboardDismissMode(props["keyboardDismissMode"])
|
|
679
1064
|
scroll.showsVerticalScrollIndicator = props["showsVerticalScrollIndicator"] as? Bool != false
|
|
680
1065
|
scroll.showsHorizontalScrollIndicator = props["showsHorizontalScrollIndicator"] as? Bool != false
|
|
681
1066
|
scroll.alwaysBounceVertical = props["alwaysBounceVertical"] as? Bool ?? (direction == .vertical)
|
|
@@ -683,6 +1068,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
683
1068
|
updateAuxiliaryHandler(events["onScroll"], in: &scrollHandlers, for: scroll)
|
|
684
1069
|
updateAuxiliaryHandler(events["onScrollBeginDrag"], in: &scrollBeginHandlers, for: scroll)
|
|
685
1070
|
updateAuxiliaryHandler(events["onScrollEndDrag"], in: &scrollEndHandlers, for: scroll)
|
|
1071
|
+
updateAuxiliaryHandler(events["onMomentumScrollBegin"], in: &scrollMomentumBeginHandlers, for: scroll)
|
|
1072
|
+
updateAuxiliaryHandler(events["onMomentumScrollEnd"], in: &scrollMomentumEndHandlers, for: scroll)
|
|
686
1073
|
configureStack(scroll.contentStack, style: style)
|
|
687
1074
|
reconcileChildren(children, in: scroll.contentStack, parent: current, path: path, style: style)
|
|
688
1075
|
case "FlatList":
|
|
@@ -700,24 +1087,30 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
700
1087
|
configureStack(stack, style: style)
|
|
701
1088
|
reconcileChildren(children, in: stack, parent: current, path: path, style: style)
|
|
702
1089
|
}
|
|
703
|
-
|
|
704
|
-
|
|
1090
|
+
updateLongPressHandler(events["onLongPress"], for: result)
|
|
1091
|
+
if type != "Button" && type != "Link" && type != "TextInput" && type != "Switch" && type != "Slider" && type != "ActivityIndicator" {
|
|
1092
|
+
updatePressHandler(nonEmptyHandler(events["onPress"]) ?? nonEmptyHandler(events["onClick"]), for: result)
|
|
705
1093
|
}
|
|
706
1094
|
applyAccessibility(props, type: type, to: result)
|
|
707
1095
|
return current
|
|
708
1096
|
}
|
|
709
1097
|
|
|
710
|
-
private func makeView(_ type: String) -> UIView {
|
|
1098
|
+
private func makeView(_ type: String, props: [String: Any]) -> UIView {
|
|
711
1099
|
switch type {
|
|
712
1100
|
case "Text":
|
|
713
1101
|
let label = UILabel()
|
|
714
1102
|
label.numberOfLines = 0
|
|
715
1103
|
return label
|
|
716
|
-
case "Button":
|
|
1104
|
+
case "Button", "Link":
|
|
717
1105
|
let button = UIButton(type: .system)
|
|
718
1106
|
button.addTarget(self, action: #selector(buttonPressed(_:)), for: .touchUpInside)
|
|
719
1107
|
return button
|
|
720
1108
|
case "TextInput":
|
|
1109
|
+
if props["multiline"] as? Bool == true {
|
|
1110
|
+
let textView = CraftNativeTextView()
|
|
1111
|
+
textView.delegate = self
|
|
1112
|
+
return textView
|
|
1113
|
+
}
|
|
721
1114
|
let field = UITextField()
|
|
722
1115
|
field.borderStyle = .roundedRect
|
|
723
1116
|
field.addTarget(self, action: #selector(textChanged(_:)), for: .editingChanged)
|
|
@@ -725,12 +1118,25 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
725
1118
|
field.addTarget(self, action: #selector(textBlurred(_:)), for: .editingDidEnd)
|
|
726
1119
|
field.addTarget(self, action: #selector(textSubmitted(_:)), for: .editingDidEndOnExit)
|
|
727
1120
|
return field
|
|
1121
|
+
case "Switch":
|
|
1122
|
+
let toggle = UISwitch()
|
|
1123
|
+
toggle.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
|
|
1124
|
+
return toggle
|
|
1125
|
+
case "Slider":
|
|
1126
|
+
let slider = UISlider()
|
|
1127
|
+
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
|
|
1128
|
+
slider.addTarget(self, action: #selector(sliderFinished(_:)), for: [.touchUpInside, .touchUpOutside, .touchCancel])
|
|
1129
|
+
return slider
|
|
1130
|
+
case "ActivityIndicator":
|
|
1131
|
+
return UIActivityIndicatorView(style: .medium)
|
|
728
1132
|
case "Image":
|
|
729
1133
|
return UIImageView()
|
|
730
1134
|
case "ScrollView":
|
|
731
1135
|
return CraftNativeScrollView()
|
|
732
1136
|
case "FlatList":
|
|
733
1137
|
return CraftNativeFlatList()
|
|
1138
|
+
case "View", "SafeAreaView":
|
|
1139
|
+
return CraftNativeFlowView()
|
|
734
1140
|
default:
|
|
735
1141
|
return CraftNativeFlowView()
|
|
736
1142
|
}
|
|
@@ -805,6 +1211,23 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
805
1211
|
let owner = parent.protocolId ?? path
|
|
806
1212
|
flatListOwners = flatListOwners.filter { $0.value != owner }
|
|
807
1213
|
for child in children { registerFlatListOwnership(child, owner: owner) }
|
|
1214
|
+
list.bounces = props["bounces"] as? Bool ?? true
|
|
1215
|
+
list.keyboardDismissMode = keyboardDismissMode(props["keyboardDismissMode"])
|
|
1216
|
+
list.onScrollEvent = nonEmptyHandler(events["onScroll"]).map { handler in
|
|
1217
|
+
{ [weak self] scrollView in self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": self?.scrollEvent(scrollView) ?? [:]]) }
|
|
1218
|
+
}
|
|
1219
|
+
list.onScrollBeginDrag = nonEmptyHandler(events["onScrollBeginDrag"]).map { handler in
|
|
1220
|
+
{ [weak self] scrollView in self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": self?.scrollEvent(scrollView) ?? [:]]) }
|
|
1221
|
+
}
|
|
1222
|
+
list.onScrollEndDrag = nonEmptyHandler(events["onScrollEndDrag"]).map { handler in
|
|
1223
|
+
{ [weak self] scrollView in self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": self?.scrollEvent(scrollView) ?? [:]]) }
|
|
1224
|
+
}
|
|
1225
|
+
list.onMomentumScrollBegin = nonEmptyHandler(events["onMomentumScrollBegin"]).map { handler in
|
|
1226
|
+
{ [weak self] scrollView in self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": self?.scrollEvent(scrollView) ?? [:]]) }
|
|
1227
|
+
}
|
|
1228
|
+
list.onMomentumScrollEnd = nonEmptyHandler(events["onMomentumScrollEnd"]).map { handler in
|
|
1229
|
+
{ [weak self] scrollView in self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": self?.scrollEvent(scrollView) ?? [:]]) }
|
|
1230
|
+
}
|
|
808
1231
|
|
|
809
1232
|
list.apply(
|
|
810
1233
|
nodes: children,
|
|
@@ -823,15 +1246,18 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
823
1246
|
path: "\(path).\(identity)",
|
|
824
1247
|
previous: previous
|
|
825
1248
|
)
|
|
1249
|
+
if let previous, previous.view !== next.view {
|
|
1250
|
+
self.forgetHandlers(previous)
|
|
1251
|
+
}
|
|
826
1252
|
self.flatListRows[listKey, default: [:]][identity] = next
|
|
827
1253
|
return next.view
|
|
828
1254
|
},
|
|
829
1255
|
recycleItem: { [weak self] identity in
|
|
830
1256
|
guard let self = self,
|
|
831
1257
|
let row = self.flatListRows[listKey]?.removeValue(forKey: identity) else { return }
|
|
832
|
-
self.forgetHandlers(row)
|
|
1258
|
+
self.forgetHandlers(row, preservingInputDrafts: true)
|
|
833
1259
|
},
|
|
834
|
-
endReached: events["onEndReached"].map { [weak self] handler in
|
|
1260
|
+
endReached: nonEmptyHandler(events["onEndReached"]).map { [weak self] handler in
|
|
835
1261
|
{ self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
836
1262
|
}
|
|
837
1263
|
)
|
|
@@ -855,7 +1281,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
855
1281
|
node.view.removeFromSuperview()
|
|
856
1282
|
}
|
|
857
1283
|
|
|
858
|
-
private func forgetHandlers(_ node: RenderedNode) {
|
|
1284
|
+
private func forgetHandlers(_ node: RenderedNode, preservingInputDrafts: Bool = false) {
|
|
859
1285
|
if let list = node.view as? CraftNativeFlatList {
|
|
860
1286
|
let listKey = ObjectIdentifier(list)
|
|
861
1287
|
list.discardAll()
|
|
@@ -864,26 +1290,92 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
864
1290
|
}
|
|
865
1291
|
let id = ObjectIdentifier(node.view)
|
|
866
1292
|
handlers.removeValue(forKey: id)
|
|
1293
|
+
layoutHandlers.removeValue(forKey: id)
|
|
1294
|
+
lastLayoutFrames.removeValue(forKey: id)
|
|
867
1295
|
focusHandlers.removeValue(forKey: id)
|
|
868
1296
|
blurHandlers.removeValue(forKey: id)
|
|
1297
|
+
endEditingHandlers.removeValue(forKey: id)
|
|
869
1298
|
submitHandlers.removeValue(forKey: id)
|
|
1299
|
+
inputIdentities.removeValue(forKey: id)
|
|
1300
|
+
if node.type == "TextInput" && !preservingInputDrafts { inputDrafts.removeValue(forKey: node.identity) }
|
|
1301
|
+
longPressHandlers.removeValue(forKey: id)
|
|
1302
|
+
textMaxLengths.removeValue(forKey: id)
|
|
870
1303
|
scrollHandlers.removeValue(forKey: id)
|
|
871
1304
|
scrollBeginHandlers.removeValue(forKey: id)
|
|
872
1305
|
scrollEndHandlers.removeValue(forKey: id)
|
|
1306
|
+
scrollMomentumBeginHandlers.removeValue(forKey: id)
|
|
1307
|
+
scrollMomentumEndHandlers.removeValue(forKey: id)
|
|
1308
|
+
sliderCompleteHandlers.removeValue(forKey: id)
|
|
1309
|
+
sliderSteps.removeValue(forKey: id)
|
|
873
1310
|
if let recognizer = tapRecognizers.removeValue(forKey: id) {
|
|
874
1311
|
node.view.removeGestureRecognizer(recognizer)
|
|
875
1312
|
}
|
|
1313
|
+
if let recognizer = longPressRecognizers.removeValue(forKey: id) {
|
|
1314
|
+
node.view.removeGestureRecognizer(recognizer)
|
|
1315
|
+
}
|
|
876
1316
|
imageSources.removeValue(forKey: id)
|
|
877
1317
|
imageTasks.removeValue(forKey: id)?.cancel()
|
|
878
|
-
|
|
1318
|
+
imageErrors.removeValue(forKey: id)
|
|
1319
|
+
tintedImages.remove(id)
|
|
1320
|
+
imageLoadStartHandlers.removeValue(forKey: id)
|
|
1321
|
+
imageLoadHandlers.removeValue(forKey: id)
|
|
1322
|
+
imageLoadEndHandlers.removeValue(forKey: id)
|
|
1323
|
+
imageErrorHandlers.removeValue(forKey: id)
|
|
1324
|
+
for child in node.children { forgetHandlers(child, preservingInputDrafts: preservingInputDrafts) }
|
|
879
1325
|
}
|
|
880
1326
|
|
|
881
1327
|
private func updateHandler(_ handler: String?, for view: UIView) {
|
|
882
1328
|
let id = ObjectIdentifier(view)
|
|
883
|
-
if let handler
|
|
1329
|
+
if let handler, !handler.isEmpty { handlers[id] = handler }
|
|
884
1330
|
else { handlers.removeValue(forKey: id) }
|
|
885
1331
|
}
|
|
886
1332
|
|
|
1333
|
+
private func snappedSliderValue(_ value: Float, for slider: UISlider) -> Float {
|
|
1334
|
+
let bounded = min(slider.maximumValue, max(slider.minimumValue, value))
|
|
1335
|
+
guard let step = sliderSteps[ObjectIdentifier(slider)], step > 0 else { return bounded }
|
|
1336
|
+
let steps = ((bounded - slider.minimumValue) / step).rounded()
|
|
1337
|
+
return min(slider.maximumValue, max(slider.minimumValue, slider.minimumValue + steps * step))
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
private func updateLayoutHandler(_ handler: String?, for view: UIView) {
|
|
1341
|
+
let id = ObjectIdentifier(view)
|
|
1342
|
+
guard let handler, !handler.isEmpty else {
|
|
1343
|
+
layoutHandlers.removeValue(forKey: id)
|
|
1344
|
+
lastLayoutFrames.removeValue(forKey: id)
|
|
1345
|
+
return
|
|
1346
|
+
}
|
|
1347
|
+
if layoutHandlers[id] != handler { lastLayoutFrames.removeValue(forKey: id) }
|
|
1348
|
+
layoutHandlers[id] = handler
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
private func emitLayoutEvents() {
|
|
1352
|
+
guard let renderedRoot else { return }
|
|
1353
|
+
emitLayoutEvents(for: renderedRoot)
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
private func emitLayoutEvents(for node: RenderedNode) {
|
|
1357
|
+
let id = ObjectIdentifier(node.view)
|
|
1358
|
+
if let handler = layoutHandlers[id], node.view.window != nil {
|
|
1359
|
+
let frame = node.view.frame
|
|
1360
|
+
if lastLayoutFrames[id] != frame {
|
|
1361
|
+
lastLayoutFrames[id] = frame
|
|
1362
|
+
send(type: "EVENT", payload: [
|
|
1363
|
+
"handlerName": handler,
|
|
1364
|
+
"nativeEvent": ["layout": [
|
|
1365
|
+
"x": frame.minX,
|
|
1366
|
+
"y": frame.minY,
|
|
1367
|
+
"width": frame.width,
|
|
1368
|
+
"height": frame.height,
|
|
1369
|
+
]],
|
|
1370
|
+
])
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
for child in node.children { emitLayoutEvents(for: child) }
|
|
1374
|
+
if let list = node.view as? CraftNativeFlatList {
|
|
1375
|
+
flatListRows[ObjectIdentifier(list)]?.values.forEach { emitLayoutEvents(for: $0) }
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
887
1379
|
private func updateAuxiliaryHandler(
|
|
888
1380
|
_ handler: String?,
|
|
889
1381
|
in handlers: inout [ObjectIdentifier: String],
|
|
@@ -894,8 +1386,14 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
894
1386
|
else { handlers.removeValue(forKey: id) }
|
|
895
1387
|
}
|
|
896
1388
|
|
|
1389
|
+
private func nonEmptyHandler(_ handler: String?) -> String? {
|
|
1390
|
+
guard let handler, !handler.isEmpty else { return nil }
|
|
1391
|
+
return handler
|
|
1392
|
+
}
|
|
1393
|
+
|
|
897
1394
|
private func updatePressHandler(_ handler: String?, for view: UIView) {
|
|
898
1395
|
let id = ObjectIdentifier(view)
|
|
1396
|
+
let handler = handler?.isEmpty == false ? handler : nil
|
|
899
1397
|
updateHandler(handler, for: view)
|
|
900
1398
|
if handler != nil, tapRecognizers[id] == nil {
|
|
901
1399
|
let recognizer = UITapGestureRecognizer(target: self, action: #selector(viewPressed(_:)))
|
|
@@ -904,7 +1402,26 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
904
1402
|
} else if handler == nil, let recognizer = tapRecognizers.removeValue(forKey: id) {
|
|
905
1403
|
view.removeGestureRecognizer(recognizer)
|
|
906
1404
|
}
|
|
907
|
-
if view is
|
|
1405
|
+
if !(view is UIScrollView) { view.isUserInteractionEnabled = handler != nil || longPressHandlers[id] != nil }
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
private func updateLongPressHandler(_ handler: String?, for view: UIView) {
|
|
1409
|
+
let id = ObjectIdentifier(view)
|
|
1410
|
+
if let handler, !handler.isEmpty {
|
|
1411
|
+
longPressHandlers[id] = handler
|
|
1412
|
+
if !(view is UIScrollView) { view.isUserInteractionEnabled = true }
|
|
1413
|
+
if longPressRecognizers[id] == nil {
|
|
1414
|
+
let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(viewLongPressed(_:)))
|
|
1415
|
+
longPressRecognizers[id] = recognizer
|
|
1416
|
+
view.addGestureRecognizer(recognizer)
|
|
1417
|
+
}
|
|
1418
|
+
} else {
|
|
1419
|
+
longPressHandlers.removeValue(forKey: id)
|
|
1420
|
+
if let recognizer = longPressRecognizers.removeValue(forKey: id) {
|
|
1421
|
+
view.removeGestureRecognizer(recognizer)
|
|
1422
|
+
}
|
|
1423
|
+
if !(view is UIScrollView) { view.isUserInteractionEnabled = handlers[id] != nil }
|
|
1424
|
+
}
|
|
908
1425
|
}
|
|
909
1426
|
|
|
910
1427
|
private func updateField(_ field: UITextField, value: String?) {
|
|
@@ -920,15 +1437,86 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
920
1437
|
}
|
|
921
1438
|
}
|
|
922
1439
|
|
|
1440
|
+
private func updateTextView(_ textView: UITextView, value: String?) {
|
|
1441
|
+
guard let value = value, textView.text != value, textView.markedTextRange == nil else { return }
|
|
1442
|
+
let selection = textView.selectedTextRange
|
|
1443
|
+
let start = selection.map { textView.offset(from: textView.beginningOfDocument, to: $0.start) }
|
|
1444
|
+
let end = selection.map { textView.offset(from: textView.beginningOfDocument, to: $0.end) }
|
|
1445
|
+
textView.text = value
|
|
1446
|
+
if let start = start, let end = end,
|
|
1447
|
+
let from = textView.position(from: textView.beginningOfDocument, offset: min(start, value.utf16.count)),
|
|
1448
|
+
let to = textView.position(from: textView.beginningOfDocument, offset: min(end, value.utf16.count)) {
|
|
1449
|
+
textView.selectedTextRange = textView.textRange(from: from, to: to)
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
private func configureTextView(
|
|
1454
|
+
_ textView: UITextView,
|
|
1455
|
+
props: [String: Any],
|
|
1456
|
+
style: [String: Any],
|
|
1457
|
+
events: [String: String],
|
|
1458
|
+
defaultValue: String?
|
|
1459
|
+
) {
|
|
1460
|
+
let id = ObjectIdentifier(textView)
|
|
1461
|
+
textView.delegate = self
|
|
1462
|
+
textView.textColor = color(style["color"]) ?? .label
|
|
1463
|
+
textView.font = textFont(style, default: .systemFont(ofSize: 16))
|
|
1464
|
+
textView.textAlignment = textAlignment(style["textAlign"])
|
|
1465
|
+
textView.tintColor = color(props["selectionColor"])
|
|
1466
|
+
textView.keyboardType = keyboardType(props["keyboardType"])
|
|
1467
|
+
textView.returnKeyType = returnKeyType(props["returnKeyType"])
|
|
1468
|
+
textView.autocorrectionType = props["autoCorrect"] as? Bool == false ? .no : .default
|
|
1469
|
+
textView.autocapitalizationType = capitalizationType(props["autoCapitalize"])
|
|
1470
|
+
textView.isSecureTextEntry = props["secureTextEntry"] as? Bool == true
|
|
1471
|
+
textView.isEditable = props["editable"] as? Bool != false
|
|
1472
|
+
textView.isScrollEnabled = false
|
|
1473
|
+
if let numberOfLines = (props["numberOfLines"] as? NSNumber)?.intValue, numberOfLines > 0 {
|
|
1474
|
+
textView.textContainer.maximumNumberOfLines = numberOfLines
|
|
1475
|
+
} else {
|
|
1476
|
+
textView.textContainer.maximumNumberOfLines = 0
|
|
1477
|
+
}
|
|
1478
|
+
if let maxLength = (props["maxLength"] as? NSNumber)?.intValue, maxLength > 0 {
|
|
1479
|
+
textMaxLengths[id] = maxLength
|
|
1480
|
+
} else {
|
|
1481
|
+
textMaxLengths.removeValue(forKey: id)
|
|
1482
|
+
}
|
|
1483
|
+
if let craftTextView = textView as? CraftNativeTextView {
|
|
1484
|
+
craftTextView.placeholder = props["placeholder"] as? String
|
|
1485
|
+
craftTextView.placeholderColor = color(props["placeholderTextColor"])
|
|
1486
|
+
craftTextView.placeholderFont = textView.font
|
|
1487
|
+
}
|
|
1488
|
+
updateTextView(textView, value: (props["value"] as? String) ?? defaultValue)
|
|
1489
|
+
let selectedRange = textView.selectedRange
|
|
1490
|
+
let attributes = inputTextAttributes(style, font: textView.font ?? .systemFont(ofSize: 16), alignment: textView.textAlignment)
|
|
1491
|
+
textView.typingAttributes = attributes
|
|
1492
|
+
if !textView.text.isEmpty {
|
|
1493
|
+
textView.attributedText = NSAttributedString(string: textView.text, attributes: attributes)
|
|
1494
|
+
textView.selectedRange = selectedRange
|
|
1495
|
+
}
|
|
1496
|
+
(textView as? CraftNativeTextView)?.refreshPlaceholder()
|
|
1497
|
+
updateHandler(nonEmptyHandler(events["onChange"]) ?? nonEmptyHandler(events["onChangeText"]), for: textView)
|
|
1498
|
+
updateAuxiliaryHandler(events["onFocus"], in: &focusHandlers, for: textView)
|
|
1499
|
+
updateAuxiliaryHandler(events["onBlur"], in: &blurHandlers, for: textView)
|
|
1500
|
+
updateAuxiliaryHandler(events["onEndEditing"], in: &endEditingHandlers, for: textView)
|
|
1501
|
+
updateAuxiliaryHandler(events["onSubmitEditing"], in: &submitHandlers, for: textView)
|
|
1502
|
+
if props["autoFocus"] as? Bool == true, !textView.isFirstResponder {
|
|
1503
|
+
DispatchQueue.main.async { _ = textView.becomeFirstResponder() }
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
923
1507
|
private func configureStack(_ stack: CraftNativeFlowView, style: [String: Any]) {
|
|
924
1508
|
let direction = style["flexDirection"] as? String
|
|
925
1509
|
stack.axis = direction == "row" || direction == "row-reverse" ? .horizontal : .vertical
|
|
926
1510
|
stack.wrap = style["flexWrap"] as? String == "wrap" || style["flexWrap"] as? Bool == true
|
|
927
1511
|
stack.alignItems = style["alignItems"] as? String ?? "stretch"
|
|
928
1512
|
stack.justifyContent = style["justifyContent"] as? String ?? "flex-start"
|
|
929
|
-
|
|
930
|
-
stack.
|
|
931
|
-
stack.
|
|
1513
|
+
let gap = number(style["gap"]) ?? 0
|
|
1514
|
+
stack.gap = gap
|
|
1515
|
+
stack.rowGap = number(style["rowGap"]) ?? gap
|
|
1516
|
+
stack.columnGap = number(style["columnGap"]) ?? gap
|
|
1517
|
+
stack.grid = style["display"] as? String == "grid"
|
|
1518
|
+
stack.gridColumns = craftNativeGridColumnCount(style["gridTemplateColumns"] ?? style["gridColumns"])
|
|
1519
|
+
stack.gridAutoRows = number(style["gridAutoRows"])
|
|
932
1520
|
let padding = number(style["padding"]) ?? 0
|
|
933
1521
|
let horizontal = number(style["paddingHorizontal"]) ?? padding
|
|
934
1522
|
let vertical = number(style["paddingVertical"]) ?? padding
|
|
@@ -953,19 +1541,19 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
953
1541
|
view.layer.cornerRadius = number(style["borderRadius"]) ?? 0
|
|
954
1542
|
view.layer.borderWidth = number(style["borderWidth"]) ?? 0
|
|
955
1543
|
view.layer.borderColor = (color(style["borderColor"]) ?? .clear).cgColor
|
|
1544
|
+
let elevation = max(0, number(style["elevation"]) ?? 0)
|
|
1545
|
+
view.layer.shadowColor = UIColor.black.cgColor
|
|
1546
|
+
view.layer.shadowOpacity = elevation > 0 ? Float(min(0.28, 0.12 + elevation * 0.02)) : 0
|
|
1547
|
+
view.layer.shadowRadius = elevation * 0.5
|
|
1548
|
+
view.layer.shadowOffset = CGSize(width: 0, height: elevation * 0.25)
|
|
956
1549
|
view.clipsToBounds = style["overflow"] as? String == "hidden"
|
|
1550
|
+
view.layer.masksToBounds = view.clipsToBounds
|
|
957
1551
|
}
|
|
958
1552
|
|
|
959
1553
|
private func configureText(_ label: UILabel, text: String, style: [String: Any]) {
|
|
960
|
-
let transformed:
|
|
961
|
-
switch style["textTransform"] as? String {
|
|
962
|
-
case "uppercase": transformed = text.uppercased()
|
|
963
|
-
case "lowercase": transformed = text.lowercased()
|
|
964
|
-
case "capitalize": transformed = text.capitalized
|
|
965
|
-
default: transformed = text
|
|
966
|
-
}
|
|
1554
|
+
let transformed = transformedText(text, style: style)
|
|
967
1555
|
label.textColor = color(style["color"]) ?? .label
|
|
968
|
-
label.font = textFont(style, default: .systemFont(ofSize:
|
|
1556
|
+
label.font = textFont(style, default: .systemFont(ofSize: 16))
|
|
969
1557
|
label.textAlignment = textAlignment(style["textAlign"])
|
|
970
1558
|
var attributes: [NSAttributedString.Key: Any] = [:]
|
|
971
1559
|
if let spacing = number(style["letterSpacing"]) { attributes[.kern] = spacing }
|
|
@@ -988,6 +1576,85 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
988
1576
|
if attributes.isEmpty { label.text = transformed }
|
|
989
1577
|
}
|
|
990
1578
|
|
|
1579
|
+
private func textLineBreakMode(_ value: Any?) -> NSLineBreakMode {
|
|
1580
|
+
switch value as? String {
|
|
1581
|
+
case "head": return .byTruncatingHead
|
|
1582
|
+
case "middle": return .byTruncatingMiddle
|
|
1583
|
+
case "tail": return .byTruncatingTail
|
|
1584
|
+
case "clip": return .byClipping
|
|
1585
|
+
default: return .byTruncatingTail
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
private func buttonTitleAttributes(
|
|
1590
|
+
_ style: [String: Any],
|
|
1591
|
+
color: UIColor,
|
|
1592
|
+
font: UIFont,
|
|
1593
|
+
forceUnderline: Bool
|
|
1594
|
+
) -> [NSAttributedString.Key: Any] {
|
|
1595
|
+
var attributes: [NSAttributedString.Key: Any] = [
|
|
1596
|
+
.foregroundColor: color,
|
|
1597
|
+
.font: font,
|
|
1598
|
+
]
|
|
1599
|
+
if let spacing = number(style["letterSpacing"]) { attributes[.kern] = spacing }
|
|
1600
|
+
if let lineHeight = number(style["lineHeight"]) {
|
|
1601
|
+
let paragraph = NSMutableParagraphStyle()
|
|
1602
|
+
paragraph.minimumLineHeight = lineHeight
|
|
1603
|
+
paragraph.maximumLineHeight = lineHeight
|
|
1604
|
+
attributes[.paragraphStyle] = paragraph
|
|
1605
|
+
}
|
|
1606
|
+
switch style["textDecorationLine"] as? String {
|
|
1607
|
+
case "underline": attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
|
1608
|
+
case "line-through": attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
1609
|
+
case "underline line-through":
|
|
1610
|
+
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
|
1611
|
+
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
1612
|
+
default: break
|
|
1613
|
+
}
|
|
1614
|
+
if forceUnderline { attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue }
|
|
1615
|
+
return attributes
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
private func inputTextAttributes(
|
|
1619
|
+
_ style: [String: Any],
|
|
1620
|
+
font: UIFont,
|
|
1621
|
+
alignment: NSTextAlignment
|
|
1622
|
+
) -> [NSAttributedString.Key: Any] {
|
|
1623
|
+
var attributes: [NSAttributedString.Key: Any] = [
|
|
1624
|
+
.foregroundColor: color(style["color"]) ?? .label,
|
|
1625
|
+
.font: font,
|
|
1626
|
+
]
|
|
1627
|
+
if let spacing = number(style["letterSpacing"]) { attributes[.kern] = spacing }
|
|
1628
|
+
if let lineHeight = number(style["lineHeight"]) {
|
|
1629
|
+
let paragraph = NSMutableParagraphStyle()
|
|
1630
|
+
paragraph.minimumLineHeight = lineHeight
|
|
1631
|
+
paragraph.maximumLineHeight = lineHeight
|
|
1632
|
+
paragraph.alignment = alignment
|
|
1633
|
+
attributes[.paragraphStyle] = paragraph
|
|
1634
|
+
}
|
|
1635
|
+
switch style["textDecorationLine"] as? String {
|
|
1636
|
+
case "underline": attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
|
1637
|
+
case "line-through": attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
1638
|
+
case "underline line-through":
|
|
1639
|
+
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
|
1640
|
+
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
1641
|
+
default: break
|
|
1642
|
+
}
|
|
1643
|
+
return attributes
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
private func transformedText(_ text: String, style: [String: Any]) -> String {
|
|
1647
|
+
switch style["textTransform"] as? String {
|
|
1648
|
+
case "uppercase": return text.uppercased()
|
|
1649
|
+
case "lowercase": return text.lowercased()
|
|
1650
|
+
case "capitalize": return text.split(separator: " ", omittingEmptySubsequences: false).map { word in
|
|
1651
|
+
guard let first = word.first else { return String(word) }
|
|
1652
|
+
return String(first).uppercased() + String(word.dropFirst())
|
|
1653
|
+
}.joined(separator: " ")
|
|
1654
|
+
default: return text
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
991
1658
|
private func textFont(_ style: [String: Any], default fallback: UIFont) -> UIFont {
|
|
992
1659
|
let size = number(style["fontSize"]) ?? fallback.pointSize
|
|
993
1660
|
let weight: UIFont.Weight
|
|
@@ -1021,6 +1688,15 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
1021
1688
|
}
|
|
1022
1689
|
}
|
|
1023
1690
|
|
|
1691
|
+
private func buttonAlignment(_ value: Any?) -> UIControl.ContentHorizontalAlignment {
|
|
1692
|
+
switch value as? String {
|
|
1693
|
+
case "left": return .left
|
|
1694
|
+
case "right": return .right
|
|
1695
|
+
case "center": return .center
|
|
1696
|
+
default: return .center
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1024
1700
|
private func imageContentMode(_ value: Any?) -> UIView.ContentMode {
|
|
1025
1701
|
switch value as? String {
|
|
1026
1702
|
case "cover": return .scaleAspectFill
|
|
@@ -1030,6 +1706,14 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
1030
1706
|
}
|
|
1031
1707
|
}
|
|
1032
1708
|
|
|
1709
|
+
private func keyboardDismissMode(_ value: Any?) -> UIScrollView.KeyboardDismissMode {
|
|
1710
|
+
switch value as? String {
|
|
1711
|
+
case "on-drag": return .onDrag
|
|
1712
|
+
case "interactive": return .interactive
|
|
1713
|
+
default: return .none
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1033
1717
|
private func updateImage(_ view: UIImageView, source: Any?) {
|
|
1034
1718
|
let uri = (source as? String) ?? (source as? [String: Any])?["uri"] as? String
|
|
1035
1719
|
let id = ObjectIdentifier(view)
|
|
@@ -1042,8 +1726,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
1042
1726
|
guard imageSources[id] != uri else { return }
|
|
1043
1727
|
imageTasks.removeValue(forKey: id)?.cancel()
|
|
1044
1728
|
imageSources[id] = uri
|
|
1729
|
+
imageErrors.removeValue(forKey: id)
|
|
1045
1730
|
view.image = nil
|
|
1046
1731
|
view.accessibilityValue = nil
|
|
1732
|
+
emitImageEvent(view, handler: imageLoadStartHandlers[id])
|
|
1047
1733
|
|
|
1048
1734
|
if uri.hasPrefix("data:image/") {
|
|
1049
1735
|
guard let comma = uri.firstIndex(of: ","),
|
|
@@ -1052,7 +1738,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
1052
1738
|
imageFailure(view, uri: uri, message: "Image data is invalid")
|
|
1053
1739
|
return
|
|
1054
1740
|
}
|
|
1055
|
-
view.image = image
|
|
1741
|
+
view.image = imageForDisplay(image, view: view)
|
|
1742
|
+
emitImageEvent(view, handler: imageLoadHandlers[id])
|
|
1743
|
+
emitImageEvent(view, handler: imageLoadEndHandlers[id])
|
|
1056
1744
|
return
|
|
1057
1745
|
}
|
|
1058
1746
|
if let url = URL(string: uri), url.scheme == "https" {
|
|
@@ -1065,7 +1753,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
1065
1753
|
self.imageFailure(view, uri: uri, message: "Image download failed")
|
|
1066
1754
|
return
|
|
1067
1755
|
}
|
|
1068
|
-
view.image = image
|
|
1756
|
+
view.image = self.imageForDisplay(image, view: view)
|
|
1757
|
+
self.emitImageEvent(view, handler: self.imageLoadHandlers[ObjectIdentifier(view)])
|
|
1758
|
+
self.emitImageEvent(view, handler: self.imageLoadEndHandlers[ObjectIdentifier(view)])
|
|
1069
1759
|
}
|
|
1070
1760
|
}
|
|
1071
1761
|
imageTasks[id] = task
|
|
@@ -1077,26 +1767,48 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
1077
1767
|
imageFailure(view, uri: uri, message: "Bundled image was not found")
|
|
1078
1768
|
return
|
|
1079
1769
|
}
|
|
1080
|
-
view.image = image
|
|
1770
|
+
view.image = imageForDisplay(image, view: view)
|
|
1771
|
+
emitImageEvent(view, handler: imageLoadHandlers[id])
|
|
1772
|
+
emitImageEvent(view, handler: imageLoadEndHandlers[id])
|
|
1081
1773
|
return
|
|
1082
1774
|
}
|
|
1083
1775
|
imageFailure(view, uri: uri, message: "Unsupported image source")
|
|
1084
1776
|
}
|
|
1085
1777
|
|
|
1086
1778
|
private func imageFailure(_ view: UIImageView, uri: String?, message: String) {
|
|
1779
|
+
imageErrors[ObjectIdentifier(view)] = message
|
|
1087
1780
|
view.image = nil
|
|
1088
1781
|
view.accessibilityValue = message
|
|
1782
|
+
let id = ObjectIdentifier(view)
|
|
1783
|
+
emitImageEvent(view, handler: imageErrorHandlers[id], nativeEvent: ["error": ["message": message]])
|
|
1784
|
+
emitImageEvent(view, handler: imageLoadEndHandlers[id])
|
|
1089
1785
|
NSLog("[craft native] %@: %@", message, uri ?? "<missing>")
|
|
1090
1786
|
}
|
|
1091
1787
|
|
|
1788
|
+
private func imageForDisplay(_ image: UIImage, view: UIImageView) -> UIImage {
|
|
1789
|
+
image.withRenderingMode(tintedImages.contains(ObjectIdentifier(view)) ? .alwaysTemplate : .alwaysOriginal)
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
private func emitImageEvent(
|
|
1793
|
+
_ view: UIImageView,
|
|
1794
|
+
handler: String?,
|
|
1795
|
+
nativeEvent: [String: Any] = [:]
|
|
1796
|
+
) {
|
|
1797
|
+
guard let handler, !handler.isEmpty else { return }
|
|
1798
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": nativeEvent])
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1092
1801
|
private func applyAccessibility(_ props: [String: Any], type: String, to view: UIView) {
|
|
1093
|
-
view.accessibilityLabel = props["accessibilityLabel"] as? String
|
|
1802
|
+
view.accessibilityLabel = (props["accessibilityLabel"] as? String)
|
|
1803
|
+
?? (type == "Image" ? props["alt"] as? String : nil)
|
|
1094
1804
|
view.accessibilityHint = props["accessibilityHint"] as? String
|
|
1095
1805
|
if let value = props["accessibilityValue"] {
|
|
1096
1806
|
view.accessibilityValue = (value as? String) ?? (value as? NSNumber)?.stringValue
|
|
1807
|
+
} else if type != "TextInput" {
|
|
1808
|
+
view.accessibilityValue = nil
|
|
1097
1809
|
}
|
|
1098
1810
|
let role = props["accessibilityRole"] as? String
|
|
1099
|
-
view.isAccessibilityElement = role != "none" && (view.accessibilityLabel != nil || role != nil || ["Text", "Button", "Image", "TextInput"].contains(type))
|
|
1811
|
+
view.isAccessibilityElement = role != "none" && (view.accessibilityLabel != nil || role != nil || ["Text", "Button", "Link", "Image", "TextInput", "Switch", "Slider"].contains(type))
|
|
1100
1812
|
var traits: UIAccessibilityTraits = []
|
|
1101
1813
|
switch role ?? type.lowercased() {
|
|
1102
1814
|
case "button": traits.insert(.button)
|
|
@@ -1107,11 +1819,24 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
1107
1819
|
default: break
|
|
1108
1820
|
}
|
|
1109
1821
|
if let state = props["accessibilityState"] as? [String: Any] {
|
|
1110
|
-
if state["disabled"] as? Bool == true {
|
|
1822
|
+
if state["disabled"] as? Bool == true {
|
|
1823
|
+
traits.insert(.notEnabled)
|
|
1824
|
+
if let control = view as? UIControl {
|
|
1825
|
+
control.isEnabled = false
|
|
1826
|
+
} else if handlers[ObjectIdentifier(view)] != nil || longPressHandlers[ObjectIdentifier(view)] != nil {
|
|
1827
|
+
view.isUserInteractionEnabled = false
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1111
1830
|
if state["selected"] as? Bool == true { traits.insert(.selected) }
|
|
1112
1831
|
if state["checked"] as? Bool == true { traits.insert(.selected) }
|
|
1113
1832
|
}
|
|
1114
1833
|
if let control = view as? UIControl, !control.isEnabled { traits.insert(.notEnabled) }
|
|
1834
|
+
if type == "Image", let message = imageErrors[ObjectIdentifier(view)] {
|
|
1835
|
+
let current = view.accessibilityValue
|
|
1836
|
+
if current?.contains(message) != true {
|
|
1837
|
+
view.accessibilityValue = [current, message].compactMap { $0 }.joined(separator: ", ")
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1115
1840
|
view.accessibilityTraits = traits
|
|
1116
1841
|
}
|
|
1117
1842
|
|
|
@@ -1138,10 +1863,55 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
1138
1863
|
}
|
|
1139
1864
|
}
|
|
1140
1865
|
|
|
1866
|
+
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
|
1867
|
+
guard let maxLength = textMaxLengths[ObjectIdentifier(textField)],
|
|
1868
|
+
let current = textField.text,
|
|
1869
|
+
let stringRange = Range(range, in: current) else { return true }
|
|
1870
|
+
return current.replacingCharacters(in: stringRange, with: string).utf16.count <= maxLength
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
|
1874
|
+
if text == "\n", let handler = submitHandlers[ObjectIdentifier(textView)] {
|
|
1875
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["text": textView.text ?? ""]])
|
|
1876
|
+
}
|
|
1877
|
+
guard let maxLength = textMaxLengths[ObjectIdentifier(textView)],
|
|
1878
|
+
let current = textView.text,
|
|
1879
|
+
let stringRange = Range(range, in: current) else { return true }
|
|
1880
|
+
return current.replacingCharacters(in: stringRange, with: text).utf16.count <= maxLength
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
func textViewDidChange(_ textView: UITextView) {
|
|
1884
|
+
lastFocusedInput = textView
|
|
1885
|
+
let id = ObjectIdentifier(textView)
|
|
1886
|
+
inputIdentities[id].map { inputDrafts[$0] = textView.text ?? "" }
|
|
1887
|
+
(textView as? CraftNativeTextView)?.refreshPlaceholder()
|
|
1888
|
+
guard let handler = handlers[id] else { return }
|
|
1889
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["text": textView.text ?? ""]])
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
func textViewDidBeginEditing(_ textView: UITextView) {
|
|
1893
|
+
lastFocusedInput = textView
|
|
1894
|
+
guard let handler = focusHandlers[ObjectIdentifier(textView)] else { return }
|
|
1895
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["text": textView.text ?? ""]])
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
func textViewDidEndEditing(_ textView: UITextView) {
|
|
1899
|
+
let id = ObjectIdentifier(textView)
|
|
1900
|
+
let nativeEvent: [String: Any] = ["text": textView.text ?? ""]
|
|
1901
|
+
if let handler = blurHandlers[id] {
|
|
1902
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": nativeEvent])
|
|
1903
|
+
}
|
|
1904
|
+
if let handler = endEditingHandlers[id] {
|
|
1905
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": nativeEvent])
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1141
1909
|
@objc private func textChanged(_ sender: UITextField) {
|
|
1142
1910
|
lastFocusedInput = sender
|
|
1911
|
+
let id = ObjectIdentifier(sender)
|
|
1912
|
+
inputIdentities[id].map { inputDrafts[$0] = sender.text ?? "" }
|
|
1143
1913
|
let focused = sender.isFirstResponder ? sender : firstResponder(in: rootStack)
|
|
1144
|
-
guard let handler = handlers[
|
|
1914
|
+
guard let handler = handlers[id] else { return }
|
|
1145
1915
|
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["text": sender.text ?? ""]])
|
|
1146
1916
|
restoreFocus(focused)
|
|
1147
1917
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self, weak focused] in
|
|
@@ -1149,15 +1919,38 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
1149
1919
|
}
|
|
1150
1920
|
}
|
|
1151
1921
|
|
|
1922
|
+
@objc private func switchChanged(_ sender: UISwitch) {
|
|
1923
|
+
guard let handler = handlers[ObjectIdentifier(sender)] else { return }
|
|
1924
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["value": sender.isOn]])
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
@objc private func sliderChanged(_ sender: UISlider) {
|
|
1928
|
+
let value = snappedSliderValue(sender.value, for: sender)
|
|
1929
|
+
if sender.value != value { sender.value = value }
|
|
1930
|
+
guard let handler = handlers[ObjectIdentifier(sender)] else { return }
|
|
1931
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["value": value]])
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
@objc private func sliderFinished(_ sender: UISlider) {
|
|
1935
|
+
guard let handler = sliderCompleteHandlers[ObjectIdentifier(sender)] else { return }
|
|
1936
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["value": snappedSliderValue(sender.value, for: sender)]])
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1152
1939
|
@objc private func textFocused(_ sender: UITextField) {
|
|
1153
1940
|
lastFocusedInput = sender
|
|
1154
1941
|
guard let handler = focusHandlers[ObjectIdentifier(sender)] else { return }
|
|
1155
|
-
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]])
|
|
1942
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["text": sender.text ?? ""]])
|
|
1156
1943
|
}
|
|
1157
1944
|
|
|
1158
1945
|
@objc private func textBlurred(_ sender: UITextField) {
|
|
1159
|
-
|
|
1160
|
-
|
|
1946
|
+
let id = ObjectIdentifier(sender)
|
|
1947
|
+
let nativeEvent: [String: Any] = ["text": sender.text ?? ""]
|
|
1948
|
+
if let handler = blurHandlers[id] {
|
|
1949
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": nativeEvent])
|
|
1950
|
+
}
|
|
1951
|
+
if let handler = endEditingHandlers[id] {
|
|
1952
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": nativeEvent])
|
|
1953
|
+
}
|
|
1161
1954
|
}
|
|
1162
1955
|
|
|
1163
1956
|
@objc private func textSubmitted(_ sender: UITextField) {
|
|
@@ -1170,6 +1963,13 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
1170
1963
|
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]])
|
|
1171
1964
|
}
|
|
1172
1965
|
|
|
1966
|
+
@objc private func viewLongPressed(_ sender: UILongPressGestureRecognizer) {
|
|
1967
|
+
guard sender.state == .began,
|
|
1968
|
+
let view = sender.view,
|
|
1969
|
+
let handler = longPressHandlers[ObjectIdentifier(view)] else { return }
|
|
1970
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]])
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1173
1973
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
1174
1974
|
guard let handler = scrollHandlers[ObjectIdentifier(scrollView)] else { return }
|
|
1175
1975
|
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": scrollEvent(scrollView)])
|
|
@@ -1185,6 +1985,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
1185
1985
|
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": scrollEvent(scrollView)])
|
|
1186
1986
|
}
|
|
1187
1987
|
|
|
1988
|
+
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
|
|
1989
|
+
guard let handler = scrollMomentumBeginHandlers[ObjectIdentifier(scrollView)] else { return }
|
|
1990
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": scrollEvent(scrollView)])
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
1994
|
+
guard let handler = scrollMomentumEndHandlers[ObjectIdentifier(scrollView)] else { return }
|
|
1995
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": scrollEvent(scrollView)])
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1188
1998
|
private func scrollEvent(_ scrollView: UIScrollView) -> [String: Any] {
|
|
1189
1999
|
[
|
|
1190
2000
|
"contentOffset": ["x": scrollView.contentOffset.x, "y": scrollView.contentOffset.y],
|
|
@@ -1208,10 +2018,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate
|
|
|
1208
2018
|
private func keyboardType(_ value: Any?) -> UIKeyboardType {
|
|
1209
2019
|
switch value as? String {
|
|
1210
2020
|
case "email-address": return .emailAddress
|
|
1211
|
-
case "numeric": return .numberPad
|
|
2021
|
+
case "numeric", "number-pad": return .numberPad
|
|
1212
2022
|
case "phone-pad": return .phonePad
|
|
1213
2023
|
case "decimal-pad": return .decimalPad
|
|
1214
2024
|
case "url": return .URL
|
|
2025
|
+
case "web-search": return .webSearch
|
|
2026
|
+
case "visible-password": return .asciiCapable
|
|
1215
2027
|
default: return .default
|
|
1216
2028
|
}
|
|
1217
2029
|
}
|