craft-native 0.0.174 → 0.0.176
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/src/index.d.ts +19 -0
- package/dist/android/src/index.js +19 -2
- package/dist/android/templates/AndroidManifest.xml.template +1 -0
- package/dist/android/templates/CraftBridge.kt.template +51 -0
- package/dist/android/templates/CraftCrashReporter.kt.template +544 -0
- package/dist/android/templates/CraftNative.kt.template +17 -0
- package/dist/android/templates/CraftNativeFlatList.kt.template +20 -9
- package/dist/android/templates/CraftNativeMutation.kt.template +4 -1
- package/dist/android/templates/CraftNativeServices.kt.template +31 -1
- package/dist/android/templates/MainActivityNative.kt.template +998 -303
- package/dist/android/templates/build.gradle.kts.app.template +4 -2
- package/dist/api/mobile.d.ts +31 -0
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +203 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +19 -0
- package/dist/ios/src/index.js +7 -1
- package/dist/ios/templates/CraftApp.swift +51 -1
- package/dist/ios/templates/CraftCrashReporter.swift +1010 -0
- package/dist/ios/templates/CraftHybrid.swift +10 -0
- package/dist/ios/templates/CraftNativeActions.swift +49 -1
- package/dist/ios/templates/CraftNativeFlatList.swift +13 -21
- package/dist/ios/templates/CraftNativeMutation.swift +1 -1
- package/dist/ios/templates/CraftNativeScreen.swift +410 -55
- package/dist/ios/templates/project.yml.template +1 -0
- package/dist/macos/templates/CraftNativeMutation.swift +1 -1
- package/dist/macos/templates/native_layout.zig +389 -17
- package/dist/mobile.js +66 -0
- package/package.json +1 -1
|
@@ -12,6 +12,15 @@ import UIKit
|
|
|
12
12
|
|
|
13
13
|
private let craftNativeCapabilityTimeoutMilliseconds = 30_000
|
|
14
14
|
|
|
15
|
+
/// A finite number from a prop, for the views outside the screen controller,
|
|
16
|
+
/// which has its own: the keyboard-avoiding view and SVG text read theirs
|
|
17
|
+
/// with it, and without it the template did not compile.
|
|
18
|
+
private func number(_ value: Any?) -> CGFloat? {
|
|
19
|
+
guard let number = value as? NSNumber else { return nil }
|
|
20
|
+
let value = CGFloat(truncating: number)
|
|
21
|
+
return value.isFinite ? value : nil
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
private func craftNativeGridColumnCount(_ value: Any?) -> Int {
|
|
16
25
|
if let number = value as? NSNumber { return max(1, number.intValue) }
|
|
17
26
|
guard let text = value as? String else { return 1 }
|
|
@@ -81,6 +90,178 @@ private func craftNativeJustifyCode(_ value: String) -> UInt32 {
|
|
|
81
90
|
}
|
|
82
91
|
}
|
|
83
92
|
|
|
93
|
+
/// Optional bridge to the host-neutral tree layout ABI. The host still owns
|
|
94
|
+
/// intrinsic measurement and keeps its local implementation for unsupported
|
|
95
|
+
/// trees or runtimes built before this symbol was exported.
|
|
96
|
+
private enum CraftNativeTreeLayoutBridge {
|
|
97
|
+
private struct StyleInput {
|
|
98
|
+
var direction: UInt32 = 0
|
|
99
|
+
var width: Float = .nan
|
|
100
|
+
var height: Float = .nan
|
|
101
|
+
var intrinsicWidth: Float = .nan
|
|
102
|
+
var intrinsicHeight: Float = .nan
|
|
103
|
+
var baseline: Float = .nan
|
|
104
|
+
var aspectRatio: Float = .nan
|
|
105
|
+
var flexBasis: Float = .nan
|
|
106
|
+
var flexGrow: Float = 0
|
|
107
|
+
var flexShrink: Float = 0
|
|
108
|
+
var minWidth: Float = .nan
|
|
109
|
+
var maxWidth: Float = .nan
|
|
110
|
+
var minHeight: Float = .nan
|
|
111
|
+
var maxHeight: Float = .nan
|
|
112
|
+
var gap: Float = 0
|
|
113
|
+
var justifyContent: UInt32 = 0
|
|
114
|
+
var alignItems: UInt32 = 3
|
|
115
|
+
var alignSelf: UInt32 = 0
|
|
116
|
+
var flexWrap: UInt32 = 0
|
|
117
|
+
var alignContent: UInt32 = 6
|
|
118
|
+
var marginTop: Float = 0
|
|
119
|
+
var marginRight: Float = 0
|
|
120
|
+
var marginBottom: Float = 0
|
|
121
|
+
var marginLeft: Float = 0
|
|
122
|
+
var paddingTop: Float = 0
|
|
123
|
+
var paddingRight: Float = 0
|
|
124
|
+
var paddingBottom: Float = 0
|
|
125
|
+
var paddingLeft: Float = 0
|
|
126
|
+
var grid: UInt32 = 0
|
|
127
|
+
var gridColumns: UInt32 = 1
|
|
128
|
+
var rowGap: Float = 0
|
|
129
|
+
var columnGap: Float = 0
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private struct NodeInput {
|
|
133
|
+
var style: StyleInput
|
|
134
|
+
var firstChild: UInt32
|
|
135
|
+
var childCount: UInt32
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private struct Frame {
|
|
139
|
+
var x: Float
|
|
140
|
+
var y: Float
|
|
141
|
+
var width: Float
|
|
142
|
+
var height: Float
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private typealias Function = @convention(c) (
|
|
146
|
+
Float, Float, UnsafeRawPointer, Int, UnsafeMutableRawPointer, Int
|
|
147
|
+
) -> Int32
|
|
148
|
+
|
|
149
|
+
private static let function: Function? = {
|
|
150
|
+
guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "craft_native_layout_tree") else { return nil }
|
|
151
|
+
return unsafeBitCast(symbol, to: Function.self)
|
|
152
|
+
}()
|
|
153
|
+
|
|
154
|
+
private static func finite(_ value: CGFloat?) -> Float {
|
|
155
|
+
guard let value, value.isFinite else { return .nan }
|
|
156
|
+
return Float(value)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private static func length(_ value: CraftNativeLength?, base: CGFloat?) -> Float {
|
|
160
|
+
guard let value else { return .nan }
|
|
161
|
+
return finite(value.resolve(base))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private static func alignment(_ value: String?) -> UInt32 {
|
|
165
|
+
switch value {
|
|
166
|
+
case "center": return 1
|
|
167
|
+
case "flex-end": return 2
|
|
168
|
+
case "stretch": return 3
|
|
169
|
+
case "baseline": return 4
|
|
170
|
+
default: return 0
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private static func alignSelf(_ value: String?) -> UInt32 {
|
|
175
|
+
switch value {
|
|
176
|
+
case "flex-start": return 1
|
|
177
|
+
case "center": return 2
|
|
178
|
+
case "flex-end": return 3
|
|
179
|
+
case "stretch": return 4
|
|
180
|
+
case "baseline": return 5
|
|
181
|
+
default: return 0
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private static func justify(_ value: String) -> UInt32 {
|
|
186
|
+
craftNativeJustifyCode(value)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static func frames(
|
|
190
|
+
width: CGFloat,
|
|
191
|
+
height: CGFloat,
|
|
192
|
+
direction: NSLayoutConstraint.Axis,
|
|
193
|
+
gap: CGFloat,
|
|
194
|
+
rowGap: CGFloat?,
|
|
195
|
+
columnGap: CGFloat?,
|
|
196
|
+
wrap: Bool,
|
|
197
|
+
justifyContent: String,
|
|
198
|
+
alignItems: String,
|
|
199
|
+
padding: UIEdgeInsets,
|
|
200
|
+
children: [(UIView, CraftNativeLayoutStyle, CGSize, CGFloat)]
|
|
201
|
+
) -> [(UIView, CGRect)]? {
|
|
202
|
+
guard let function, width.isFinite, height.isFinite, width >= 0, height >= 0, !children.isEmpty else { return nil }
|
|
203
|
+
let mainGap = direction == .horizontal ? (columnGap ?? gap) : (rowGap ?? gap)
|
|
204
|
+
let crossGap = direction == .horizontal ? (rowGap ?? gap) : (columnGap ?? gap)
|
|
205
|
+
var root = StyleInput(
|
|
206
|
+
direction: direction == .horizontal ? 0 : 1,
|
|
207
|
+
width: Float(width), height: Float(height), gap: Float(max(0, mainGap)),
|
|
208
|
+
justifyContent: justify(justifyContent), alignItems: alignment(alignItems), flexWrap: wrap ? 1 : 0,
|
|
209
|
+
paddingTop: Float(max(0, padding.top)), paddingRight: Float(max(0, padding.right)),
|
|
210
|
+
paddingBottom: Float(max(0, padding.bottom)), paddingLeft: Float(max(0, padding.left))
|
|
211
|
+
)
|
|
212
|
+
root.rowGap = Float(max(0, rowGap ?? 0))
|
|
213
|
+
root.columnGap = Float(max(0, columnGap ?? 0))
|
|
214
|
+
let contentWidth = max(0, width - padding.left - padding.right)
|
|
215
|
+
let contentHeight = max(0, height - padding.top - padding.bottom)
|
|
216
|
+
let mainSize = direction == .horizontal ? contentWidth : contentHeight
|
|
217
|
+
var nodes = [NodeInput(style: root, firstChild: 1, childCount: UInt32(children.count))]
|
|
218
|
+
for (view, style, intrinsic, baseline) in children {
|
|
219
|
+
let shrink: CGFloat
|
|
220
|
+
if let authored = style.flexShrink {
|
|
221
|
+
shrink = authored
|
|
222
|
+
} else if view is UIScrollView {
|
|
223
|
+
shrink = 1
|
|
224
|
+
} else if view is UILabel, direction == .horizontal {
|
|
225
|
+
shrink = 1
|
|
226
|
+
} else {
|
|
227
|
+
shrink = 0
|
|
228
|
+
}
|
|
229
|
+
var input = StyleInput(
|
|
230
|
+
width: length(style.width, base: contentWidth), height: length(style.height, base: contentHeight),
|
|
231
|
+
intrinsicWidth: Float(max(0, intrinsic.width)), intrinsicHeight: Float(max(0, intrinsic.height)),
|
|
232
|
+
baseline: Float(max(0, baseline)), aspectRatio: finite(style.aspectRatio),
|
|
233
|
+
flexBasis: length(style.flexBasis, base: mainSize), flexGrow: Float(max(0, style.flexGrow)), flexShrink: Float(max(0, shrink)),
|
|
234
|
+
minWidth: length(style.minWidth, base: contentWidth), maxWidth: length(style.maxWidth, base: contentWidth),
|
|
235
|
+
minHeight: length(style.minHeight, base: contentHeight), maxHeight: length(style.maxHeight, base: contentHeight),
|
|
236
|
+
alignSelf: alignSelf(style.alignSelf),
|
|
237
|
+
marginTop: Float(max(0, style.marginTop)), marginRight: Float(max(0, style.marginRight)),
|
|
238
|
+
marginBottom: Float(max(0, style.marginBottom)), marginLeft: Float(max(0, style.marginLeft))
|
|
239
|
+
)
|
|
240
|
+
input.direction = 0
|
|
241
|
+
nodes.append(NodeInput(style: input, firstChild: 0, childCount: 0))
|
|
242
|
+
}
|
|
243
|
+
var output = [Frame](repeating: Frame(x: 0, y: 0, width: 0, height: 0), count: nodes.count)
|
|
244
|
+
let frameCount = output.count
|
|
245
|
+
let status = nodes.withUnsafeBufferPointer { nodeBuffer in
|
|
246
|
+
output.withUnsafeMutableBufferPointer { frameBuffer in
|
|
247
|
+
function(
|
|
248
|
+
Float(width), Float(height), UnsafeRawPointer(nodeBuffer.baseAddress!), nodes.count,
|
|
249
|
+
UnsafeMutableRawPointer(frameBuffer.baseAddress!), frameCount
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
guard status == 0 else { return nil }
|
|
254
|
+
let childFrames = output.dropFirst()
|
|
255
|
+
guard childFrames.allSatisfy({ frame in
|
|
256
|
+
frame.x.isFinite && frame.y.isFinite && frame.width.isFinite && frame.height.isFinite &&
|
|
257
|
+
frame.width >= 0 && frame.height >= 0
|
|
258
|
+
}) else { return nil }
|
|
259
|
+
return zip(children, childFrames).map { child, frame in
|
|
260
|
+
(child.0, CGRect(x: CGFloat(frame.x), y: CGFloat(frame.y), width: CGFloat(frame.width), height: CGFloat(frame.height)))
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
84
265
|
private enum CraftNativeFlexLineBridge {
|
|
85
266
|
private struct Item {
|
|
86
267
|
var basis: Float
|
|
@@ -430,7 +611,9 @@ private final class CraftNativeVideoView: UIView {
|
|
|
430
611
|
private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
431
612
|
private var annotationsByID: [String: MKPointAnnotation] = [:]
|
|
432
613
|
private var overlaysByID: [String: MKPolyline] = [:]
|
|
614
|
+
private var overlayCoordinatesByID: [String: [CLLocationCoordinate2D]] = [:]
|
|
433
615
|
private var overlayColors: [ObjectIdentifier: UIColor] = [:]
|
|
616
|
+
private var overlayWidths: [ObjectIdentifier: CGFloat] = [:]
|
|
434
617
|
private(set) var markerHandlers: [String: String] = [:]
|
|
435
618
|
var onMarkerTap: ((String) -> Void)?
|
|
436
619
|
|
|
@@ -453,6 +636,10 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
453
636
|
isZoomEnabled = props["zoomEnabled"] as? Bool ?? true
|
|
454
637
|
isRotateEnabled = props["rotateEnabled"] as? Bool ?? false
|
|
455
638
|
mapType = Self.mapType(props["mapType"] as? String)
|
|
639
|
+
// Showing the user dot must not trigger a permission prompt from a
|
|
640
|
+
// renderer pass. The app's existing location capability owns that
|
|
641
|
+
// prompt; a map only opts in after authorization is already granted.
|
|
642
|
+
showsUserLocation = props["showsUserLocation"] as? Bool == true && Self.locationPermissionGranted()
|
|
456
643
|
|
|
457
644
|
var nextMarkers: [String: (coordinate: CLLocationCoordinate2D, title: String?)] = [:]
|
|
458
645
|
var nextHandlers: [String: String] = [:]
|
|
@@ -474,7 +661,10 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
474
661
|
}
|
|
475
662
|
} else if type == "mappolyline" || type == "polyline" {
|
|
476
663
|
guard points.count > 1 else { continue }
|
|
477
|
-
|
|
664
|
+
let authoredWidth = (childProps["strokeWidth"] as? NSNumber)?.doubleValue ?? 3
|
|
665
|
+
let width = CGFloat(authoredWidth)
|
|
666
|
+
guard authoredWidth.isFinite, width.isFinite, width > 0 else { continue }
|
|
667
|
+
nextPolylines[id] = (points, Self.color(childProps["strokeColor"] as? String), width)
|
|
478
668
|
}
|
|
479
669
|
}
|
|
480
670
|
|
|
@@ -494,13 +684,33 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
494
684
|
}
|
|
495
685
|
markerHandlers = nextHandlers
|
|
496
686
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
687
|
+
let staleOverlays = overlaysByID.keys.filter { nextPolylines[$0] == nil }
|
|
688
|
+
for id in staleOverlays {
|
|
689
|
+
if let overlay = overlaysByID.removeValue(forKey: id) {
|
|
690
|
+
removeOverlay(overlay)
|
|
691
|
+
overlayColors.removeValue(forKey: ObjectIdentifier(overlay))
|
|
692
|
+
overlayWidths.removeValue(forKey: ObjectIdentifier(overlay))
|
|
693
|
+
}
|
|
694
|
+
overlayCoordinatesByID.removeValue(forKey: id)
|
|
695
|
+
}
|
|
500
696
|
for (id, value) in nextPolylines {
|
|
697
|
+
if let overlay = overlaysByID[id],
|
|
698
|
+
let coordinates = overlayCoordinatesByID[id],
|
|
699
|
+
Self.coordinatesEqual(coordinates, value.coordinates),
|
|
700
|
+
overlayColors[ObjectIdentifier(overlay)]?.isEqual(value.color) == true,
|
|
701
|
+
overlayWidths[ObjectIdentifier(overlay)] == value.width {
|
|
702
|
+
continue
|
|
703
|
+
}
|
|
704
|
+
if let previous = overlaysByID.removeValue(forKey: id) {
|
|
705
|
+
removeOverlay(previous)
|
|
706
|
+
overlayColors.removeValue(forKey: ObjectIdentifier(previous))
|
|
707
|
+
overlayWidths.removeValue(forKey: ObjectIdentifier(previous))
|
|
708
|
+
}
|
|
501
709
|
let overlay = MKPolyline(coordinates: value.coordinates, count: value.coordinates.count)
|
|
502
710
|
overlaysByID[id] = overlay
|
|
711
|
+
overlayCoordinatesByID[id] = value.coordinates
|
|
503
712
|
overlayColors[ObjectIdentifier(overlay)] = value.color
|
|
713
|
+
overlayWidths[ObjectIdentifier(overlay)] = value.width
|
|
504
714
|
addOverlay(overlay)
|
|
505
715
|
}
|
|
506
716
|
|
|
@@ -542,7 +752,7 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
542
752
|
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
|
|
543
753
|
let renderer = MKPolylineRenderer(overlay: overlay)
|
|
544
754
|
renderer.strokeColor = overlayColors[ObjectIdentifier(overlay)] ?? .systemBlue
|
|
545
|
-
renderer.lineWidth = 3
|
|
755
|
+
renderer.lineWidth = overlayWidths[ObjectIdentifier(overlay)] ?? 3
|
|
546
756
|
return renderer
|
|
547
757
|
}
|
|
548
758
|
|
|
@@ -550,8 +760,19 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
550
760
|
let numbers = (value as? [Any])?.compactMap { ($0 as? NSNumber)?.doubleValue }
|
|
551
761
|
?? (value as? [NSNumber])?.map(\.doubleValue)
|
|
552
762
|
?? []
|
|
553
|
-
return stride(from: 0, to: numbers.count - 1, by: 2).
|
|
554
|
-
|
|
763
|
+
return stride(from: 0, to: numbers.count - 1, by: 2).compactMap {
|
|
764
|
+
let latitude = numbers[$0]
|
|
765
|
+
let longitude = numbers[$0 + 1]
|
|
766
|
+
guard latitude.isFinite, longitude.isFinite,
|
|
767
|
+
(-90...90).contains(latitude), (-180...180).contains(longitude) else { return nil }
|
|
768
|
+
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
private static func coordinatesEqual(_ lhs: [CLLocationCoordinate2D], _ rhs: [CLLocationCoordinate2D]) -> Bool {
|
|
773
|
+
guard lhs.count == rhs.count else { return false }
|
|
774
|
+
return zip(lhs, rhs).allSatisfy { left, right in
|
|
775
|
+
left.latitude == right.latitude && left.longitude == right.longitude
|
|
555
776
|
}
|
|
556
777
|
}
|
|
557
778
|
|
|
@@ -562,7 +783,11 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
562
783
|
let latitude = (center["latitude"] as? NSNumber)?.doubleValue,
|
|
563
784
|
let longitude = (center["longitude"] as? NSNumber)?.doubleValue,
|
|
564
785
|
let latitudeDelta = (span["latitudeDelta"] as? NSNumber)?.doubleValue ?? (span["latitude_delta"] as? NSNumber)?.doubleValue,
|
|
565
|
-
let longitudeDelta = (span["longitudeDelta"] as? NSNumber)?.doubleValue ?? (span["longitude_delta"] as? NSNumber)?.doubleValue
|
|
786
|
+
let longitudeDelta = (span["longitudeDelta"] as? NSNumber)?.doubleValue ?? (span["longitude_delta"] as? NSNumber)?.doubleValue,
|
|
787
|
+
latitude.isFinite, longitude.isFinite, latitudeDelta.isFinite, longitudeDelta.isFinite,
|
|
788
|
+
(-90...90).contains(latitude), (-180...180).contains(longitude),
|
|
789
|
+
latitudeDelta > 0, latitudeDelta <= 180, longitudeDelta > 0, longitudeDelta <= 360,
|
|
790
|
+
latitude - latitudeDelta / 2 >= -90, latitude + latitudeDelta / 2 <= 90 else { return nil }
|
|
566
791
|
return MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), span: MKCoordinateSpan(latitudeDelta: latitudeDelta, longitudeDelta: longitudeDelta))
|
|
567
792
|
}
|
|
568
793
|
|
|
@@ -585,6 +810,16 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
585
810
|
default: return .standard
|
|
586
811
|
}
|
|
587
812
|
}
|
|
813
|
+
|
|
814
|
+
private static func locationPermissionGranted() -> Bool {
|
|
815
|
+
let status: CLAuthorizationStatus
|
|
816
|
+
if #available(iOS 14.0, *) {
|
|
817
|
+
status = CLLocationManager().authorizationStatus
|
|
818
|
+
} else {
|
|
819
|
+
status = CLLocationManager.authorizationStatus()
|
|
820
|
+
}
|
|
821
|
+
return status == .authorizedAlways || status == .authorizedWhenInUse
|
|
822
|
+
}
|
|
588
823
|
}
|
|
589
824
|
|
|
590
825
|
/// A WebView-free host for the first stx native vertical slice. The bundled
|
|
@@ -614,15 +849,17 @@ private enum CraftNativeLength: Equatable {
|
|
|
614
849
|
|
|
615
850
|
init?(_ value: Any?) {
|
|
616
851
|
if let number = value as? NSNumber {
|
|
617
|
-
|
|
852
|
+
let points = CGFloat(truncating: number)
|
|
853
|
+
guard points.isFinite else { return nil }
|
|
854
|
+
self = .points(points)
|
|
618
855
|
return
|
|
619
856
|
}
|
|
620
857
|
guard let text = (value as? String)?.trimmingCharacters(in: .whitespaces), !text.isEmpty else { return nil }
|
|
621
|
-
if text.hasSuffix("%"), let fraction = Double(text.dropLast()) {
|
|
858
|
+
if text.hasSuffix("%"), let fraction = Double(text.dropLast()), fraction.isFinite {
|
|
622
859
|
self = .percent(CGFloat(fraction) / 100)
|
|
623
|
-
} else if text.hasSuffix("px"), let points = Double(text.dropLast(2)) {
|
|
860
|
+
} else if text.hasSuffix("px"), let points = Double(text.dropLast(2)), points.isFinite {
|
|
624
861
|
self = .points(CGFloat(points))
|
|
625
|
-
} else if let points = Double(text) {
|
|
862
|
+
} else if let points = Double(text), points.isFinite {
|
|
626
863
|
self = .points(CGFloat(points))
|
|
627
864
|
} else {
|
|
628
865
|
return nil
|
|
@@ -631,7 +868,7 @@ private enum CraftNativeLength: Equatable {
|
|
|
631
868
|
|
|
632
869
|
func resolve(_ base: CGFloat?) -> CGFloat? {
|
|
633
870
|
switch self {
|
|
634
|
-
case .points(let value): return value
|
|
871
|
+
case .points(let value): return max(0, value)
|
|
635
872
|
case .percent(let fraction): return base.map { max(0, $0 * fraction) }
|
|
636
873
|
}
|
|
637
874
|
}
|
|
@@ -668,7 +905,11 @@ private struct CraftNativeLayoutStyle {
|
|
|
668
905
|
static let empty = CraftNativeLayoutStyle([:])
|
|
669
906
|
|
|
670
907
|
init(_ raw: [String: Any]) {
|
|
671
|
-
func number(_ value: Any?) -> CGFloat? {
|
|
908
|
+
func number(_ value: Any?) -> CGFloat? {
|
|
909
|
+
guard let number = value as? NSNumber else { return nil }
|
|
910
|
+
let value = CGFloat(truncating: number)
|
|
911
|
+
return value.isFinite ? value : nil
|
|
912
|
+
}
|
|
672
913
|
width = CraftNativeLength(raw["width"])
|
|
673
914
|
height = CraftNativeLength(raw["height"])
|
|
674
915
|
let ratio = number(raw["aspectRatio"])
|
|
@@ -970,6 +1211,13 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
970
1211
|
|
|
971
1212
|
private var flowChildren: [UIView] = []
|
|
972
1213
|
private var measurements: [MeasureKey: CGSize] = [:]
|
|
1214
|
+
/// The screen's rendered root gets a definite viewport from the host. A
|
|
1215
|
+
/// root with `flex: 1` must keep that height even when its first child is
|
|
1216
|
+
/// a ScrollView whose content is taller than the screen; otherwise an
|
|
1217
|
+
/// absolute sibling is positioned against the scroll content and moves
|
|
1218
|
+
/// with it.
|
|
1219
|
+
private weak var viewportChild: UIView?
|
|
1220
|
+
private var viewportChildFillsHeight = false
|
|
973
1221
|
|
|
974
1222
|
private struct MeasureKey: Hashable {
|
|
975
1223
|
let width: CGFloat?
|
|
@@ -1071,6 +1319,12 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1071
1319
|
invalidateMeasurements()
|
|
1072
1320
|
}
|
|
1073
1321
|
|
|
1322
|
+
func setViewportChild(_ view: UIView?, fillsHeight: Bool) {
|
|
1323
|
+
viewportChild = view
|
|
1324
|
+
viewportChildFillsHeight = fillsHeight
|
|
1325
|
+
invalidateMeasurements()
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1074
1328
|
func removeLayoutStyle(for view: UIView) {
|
|
1075
1329
|
view.craftLayoutStyle = .empty
|
|
1076
1330
|
}
|
|
@@ -1105,7 +1359,12 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1105
1359
|
let scale = window?.screen.scale ?? traitCollection.displayScale
|
|
1106
1360
|
let result = layout(width: bounds.width, height: bounds.height, maxWidth: bounds.width, maxHeight: bounds.height, place: true)
|
|
1107
1361
|
for (child, frame) in result.frames {
|
|
1108
|
-
var
|
|
1362
|
+
var viewportFrame = frame
|
|
1363
|
+
if child === viewportChild, viewportChildFillsHeight {
|
|
1364
|
+
viewportFrame.origin.y = 0
|
|
1365
|
+
viewportFrame.size.height = bounds.height
|
|
1366
|
+
}
|
|
1367
|
+
var snapped = CraftNativeFlexLayout.snapped(viewportFrame, scale: scale)
|
|
1109
1368
|
// Snapping each edge can take a pixel off a label (15.83…33.83
|
|
1110
1369
|
// becomes 16…33.67), and a label a pixel narrower than its text
|
|
1111
1370
|
// truncates it ("13" drew as "…"). Text keeps its whole size.
|
|
@@ -1188,6 +1447,29 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1188
1447
|
/// outer size when its parent fixes it, nil when it sizes to its content
|
|
1189
1448
|
/// within `maxWidth`/`maxHeight`. Returns its outer size and, when
|
|
1190
1449
|
/// placing, every child's frame in its own coordinates.
|
|
1450
|
+
private func sharedDirectFrames(_ children: [UIView], width: CGFloat?, height: CGFloat?) -> [(UIView, CGRect)]? {
|
|
1451
|
+
guard let width, let height, !grid, !reverse, !wrapReverse,
|
|
1452
|
+
width.isFinite, height.isFinite, width >= 0, height >= 0,
|
|
1453
|
+
children.allSatisfy({ child in
|
|
1454
|
+
let style = child.craftLayoutStyle
|
|
1455
|
+
return style.position != "absolute" && style.display != "none"
|
|
1456
|
+
}) else { return nil }
|
|
1457
|
+
|
|
1458
|
+
let intrinsic = children.map { child in
|
|
1459
|
+
CraftNativeFlexLayout.measure(child, width: nil, height: nil, maxWidth: width, maxHeight: height)
|
|
1460
|
+
}
|
|
1461
|
+
let baselines = children.enumerated().map { index, child in
|
|
1462
|
+
CraftNativeFlowView.firstBaseline(child, cross: intrinsic[index].height)
|
|
1463
|
+
}
|
|
1464
|
+
let payload = zip(children, zip(intrinsic, baselines)).map { child, measured in
|
|
1465
|
+
(child, child.craftLayoutStyle, measured.0, measured.1)
|
|
1466
|
+
}
|
|
1467
|
+
return CraftNativeTreeLayoutBridge.frames(
|
|
1468
|
+
width: width, height: height, direction: axis, gap: gap, rowGap: rowGap, columnGap: columnGap, wrap: wrap,
|
|
1469
|
+
justifyContent: justifyContent, alignItems: alignItems, padding: effectivePadding, children: payload
|
|
1470
|
+
)
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1191
1473
|
private func layout(
|
|
1192
1474
|
width: CGFloat?,
|
|
1193
1475
|
height: CGFloat?,
|
|
@@ -1222,6 +1504,9 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1222
1504
|
frames: &frames
|
|
1223
1505
|
)
|
|
1224
1506
|
}
|
|
1507
|
+
if place, flow.count == children.count, let shared = sharedDirectFrames(flow, width: width, height: height) {
|
|
1508
|
+
frames = shared
|
|
1509
|
+
}
|
|
1225
1510
|
let size = CGSize(width: width ?? contentSize.width + paddingWidth, height: height ?? contentSize.height + paddingHeight)
|
|
1226
1511
|
|
|
1227
1512
|
if place {
|
|
@@ -1269,6 +1554,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1269
1554
|
place: Bool,
|
|
1270
1555
|
frames: inout [(UIView, CGRect)]
|
|
1271
1556
|
) -> CGSize {
|
|
1557
|
+
let contentPadding = effectivePadding
|
|
1272
1558
|
let definiteMain = horizontal ? innerWidth : innerHeight
|
|
1273
1559
|
let definiteCross = horizontal ? innerHeight : innerWidth
|
|
1274
1560
|
let roomMain = horizontal ? roomWidth : roomHeight
|
|
@@ -1630,6 +1916,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1630
1916
|
place: Bool,
|
|
1631
1917
|
frames: inout [(UIView, CGRect)]
|
|
1632
1918
|
) -> CGSize {
|
|
1919
|
+
let contentPadding = effectivePadding
|
|
1633
1920
|
let columns = max(1, gridColumns)
|
|
1634
1921
|
let horizontalGap = columnGap ?? gap
|
|
1635
1922
|
let verticalGap = rowGap ?? gap
|
|
@@ -1653,7 +1940,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1653
1940
|
let available = max(0, cellWidth - style.marginLeft - style.marginRight)
|
|
1654
1941
|
let align = style.alignSelf ?? alignItems
|
|
1655
1942
|
var fixedWidth = style.width?.resolve(cellWidth)
|
|
1656
|
-
var fixedHeight = style.height?.resolve(
|
|
1943
|
+
var fixedHeight = style.height?.resolve(innerHeight)
|
|
1657
1944
|
// A cell's item is as wide as its column unless it sets a width
|
|
1658
1945
|
// (or an aspect ratio derives one from its height), whatever its
|
|
1659
1946
|
// height: a fixed-height swatch with no content was drawn 0 wide.
|
|
@@ -1687,7 +1974,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1687
1974
|
let size = sizes[index]
|
|
1688
1975
|
let alignment: UInt32 = align == "center" ? 1 : align == "flex-end" ? 2 : 0
|
|
1689
1976
|
var explicitWidth = style.width?.resolve(cellWidth)
|
|
1690
|
-
var explicitHeight = style.height?.resolve(
|
|
1977
|
+
var explicitHeight = style.height?.resolve(innerHeight)
|
|
1691
1978
|
if explicitWidth == nil && align == "stretch" && (explicitHeight == nil || style.aspectRatio == nil) {
|
|
1692
1979
|
explicitWidth = max(0, cellWidth - style.marginLeft - style.marginRight)
|
|
1693
1980
|
}
|
|
@@ -1696,7 +1983,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1696
1983
|
size.width, size.height,
|
|
1697
1984
|
explicitWidth, explicitHeight,
|
|
1698
1985
|
style.minWidth?.resolve(cellWidth), style.maxWidth?.resolve(cellWidth),
|
|
1699
|
-
style.minHeight?.resolve(
|
|
1986
|
+
style.minHeight?.resolve(innerHeight), style.maxHeight?.resolve(innerHeight),
|
|
1700
1987
|
style.marginLeft, style.marginRight, style.marginTop, style.marginBottom,
|
|
1701
1988
|
align == "stretch", alignment
|
|
1702
1989
|
)
|
|
@@ -1805,7 +2092,7 @@ private final class CraftNativeKeyboardAvoidingView: CraftNativeFlowView {
|
|
|
1805
2092
|
func configureKeyboard(props: [String: Any]) {
|
|
1806
2093
|
let requestedBehavior = (props["behavior"] as? String) ?? "padding"
|
|
1807
2094
|
keyboardBehavior = ["height", "position", "padding"].contains(requestedBehavior) ? requestedBehavior : "padding"
|
|
1808
|
-
keyboardVerticalOffset = (props["keyboardVerticalOffset"]
|
|
2095
|
+
keyboardVerticalOffset = number(props["keyboardVerticalOffset"]) ?? 0
|
|
1809
2096
|
if keyboardBehavior != "height" { appliedHeightInset = 0 }
|
|
1810
2097
|
authoredPadding = padding
|
|
1811
2098
|
applyKeyboardAvoidance()
|
|
@@ -2306,7 +2593,7 @@ private final class CraftNativeSvgView: UIView {
|
|
|
2306
2593
|
let props = drawing["props"] as? [String: Any] ?? [:]
|
|
2307
2594
|
guard let box = props["viewBox"] as? [Any], box.count == 4 else { return nil }
|
|
2308
2595
|
let n = box.compactMap { ($0 as? NSNumber).map { CGFloat(truncating: $0) } }
|
|
2309
|
-
guard n.count == 4, n[2] > 0, n[3] > 0 else { return nil }
|
|
2596
|
+
guard n.count == 4, n.allSatisfy(\.isFinite), n[2] > 0, n[3] > 0 else { return nil }
|
|
2310
2597
|
return CGRect(x: n[0], y: n[1], width: n[2], height: n[3])
|
|
2311
2598
|
}
|
|
2312
2599
|
|
|
@@ -2327,21 +2614,29 @@ private final class CraftNativeSvgView: UIView {
|
|
|
2327
2614
|
var linecap: String?
|
|
2328
2615
|
var linejoin: String?
|
|
2329
2616
|
var dashes: [CGFloat]?
|
|
2617
|
+
var vectorEffect: String?
|
|
2330
2618
|
|
|
2331
2619
|
func merged(_ node: [String: Any]) -> Paint {
|
|
2332
2620
|
var next = self
|
|
2333
2621
|
let props = node["props"] as? [String: Any] ?? [:]
|
|
2334
2622
|
let style = node["style"] as? [String: Any] ?? [:]
|
|
2623
|
+
func value(_ key: String) -> Any? { style[key] ?? props[key] }
|
|
2624
|
+
func finite(_ raw: Any?) -> CGFloat? {
|
|
2625
|
+
guard let number = raw as? NSNumber else { return nil }
|
|
2626
|
+
let value = CGFloat(truncating: number)
|
|
2627
|
+
return value.isFinite ? value : nil
|
|
2628
|
+
}
|
|
2335
2629
|
if let color = style["color"] as? String { next.color = color }
|
|
2336
2630
|
if let fill = style["fill"] as? String ?? props["fill"] as? String { next.fill = fill }
|
|
2337
2631
|
if let stroke = style["stroke"] as? String ?? props["stroke"] as? String { next.stroke = stroke }
|
|
2338
|
-
if let width = (style["strokeWidth"]
|
|
2632
|
+
if let width = finite(style["strokeWidth"] ?? props["strokeWidth"]) { next.strokeWidth = width }
|
|
2339
2633
|
// Computed style wins over the component prop, matching Android
|
|
2340
2634
|
// and the browser's cascade when both provide opacity.
|
|
2341
|
-
if let opacity = style["opacity"]
|
|
2342
|
-
if let cap =
|
|
2343
|
-
if let join =
|
|
2344
|
-
if let dashes =
|
|
2635
|
+
if let opacity = finite(style["opacity"] ?? props["opacity"]) { next.opacity *= opacity }
|
|
2636
|
+
if let cap = value("strokeLinecap") as? String { next.linecap = cap }
|
|
2637
|
+
if let join = value("strokeLinejoin") as? String { next.linejoin = join }
|
|
2638
|
+
if let dashes = value("strokeDasharray") as? [Any] { next.dashes = dashes.compactMap(finite).filter { $0 >= 0 } }
|
|
2639
|
+
if let vectorEffect = value("vectorEffect") as? String { next.vectorEffect = vectorEffect }
|
|
2345
2640
|
return next
|
|
2346
2641
|
}
|
|
2347
2642
|
|
|
@@ -2408,7 +2703,7 @@ private final class CraftNativeSvgView: UIView {
|
|
|
2408
2703
|
// `vector-effect="non-scaling-stroke"`: the stroke is in the
|
|
2409
2704
|
// view's points, so a stretched chart's line keeps its width
|
|
2410
2705
|
// and a round cap stays a round dot.
|
|
2411
|
-
let nonScaling =
|
|
2706
|
+
let nonScaling = own.vectorEffect == "non-scaling-stroke"
|
|
2412
2707
|
let line: UIBezierPath
|
|
2413
2708
|
if nonScaling, let copy = path.copy() as? UIBezierPath {
|
|
2414
2709
|
copy.apply(viewTransform)
|
|
@@ -2435,14 +2730,14 @@ private final class CraftNativeSvgView: UIView {
|
|
|
2435
2730
|
guard !text.isEmpty else { return }
|
|
2436
2731
|
let props = node["props"] as? [String: Any] ?? [:]
|
|
2437
2732
|
let style = node["style"] as? [String: Any] ?? [:]
|
|
2438
|
-
let size =
|
|
2733
|
+
let size = number(props["fontSize"]) ?? number(style["fontSize"]) ?? 12
|
|
2439
2734
|
let weight: UIFont.Weight = (props["fontWeight"] as? String ?? style["fontWeight"] as? String).map { ["500": .medium, "600": .semibold, "700": .bold, "bold": .bold][$0] ?? .regular } ?? .regular
|
|
2440
2735
|
let string = NSAttributedString(string: text, attributes: [
|
|
2441
2736
|
.font: UIFont.systemFont(ofSize: size, weight: weight),
|
|
2442
2737
|
.foregroundColor: paint.resolve(paint.fill) ?? UIColor.label,
|
|
2443
2738
|
])
|
|
2444
|
-
var x =
|
|
2445
|
-
let y =
|
|
2739
|
+
var x = number(props["x"]) ?? 0
|
|
2740
|
+
let y = number(props["y"]) ?? 0
|
|
2446
2741
|
switch props["textAnchor"] as? String {
|
|
2447
2742
|
case "middle": x -= string.size().width / 2
|
|
2448
2743
|
case "end": x -= string.size().width
|
|
@@ -2455,7 +2750,7 @@ private final class CraftNativeSvgView: UIView {
|
|
|
2455
2750
|
static func path(_ value: Any?) -> UIBezierPath? {
|
|
2456
2751
|
guard let list = value as? [Any] else { return nil }
|
|
2457
2752
|
let n = list.compactMap { ($0 as? NSNumber).map { CGFloat(truncating: $0) } }
|
|
2458
|
-
guard !n.isEmpty, n.count == list.count else { return nil }
|
|
2753
|
+
guard !n.isEmpty, n.count == list.count, n.allSatisfy(\.isFinite) else { return nil }
|
|
2459
2754
|
let path = UIBezierPath()
|
|
2460
2755
|
var i = 0
|
|
2461
2756
|
func point(_ at: Int) -> CGPoint { CGPoint(x: n[at], y: n[at + 1]) }
|
|
@@ -2892,6 +3187,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2892
3187
|
override func viewDidAppear(_ animated: Bool) {
|
|
2893
3188
|
super.viewDidAppear(animated)
|
|
2894
3189
|
screenVisibilityChanged(true)
|
|
3190
|
+
// A navigation breadcrumb for the crash reporter: the trail a crash
|
|
3191
|
+
// report carries is the screens that led to it.
|
|
3192
|
+
if !isAppScript, let routeName, !routeName.isEmpty {
|
|
3193
|
+
CraftCrashReporter.shared.screenAppeared(routeName)
|
|
3194
|
+
}
|
|
2895
3195
|
}
|
|
2896
3196
|
|
|
2897
3197
|
override func viewDidDisappear(_ animated: Bool) {
|
|
@@ -2987,7 +3287,18 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2987
3287
|
?? Self.safeJavaScriptString(exception)
|
|
2988
3288
|
?? "unknown"
|
|
2989
3289
|
let stack = Self.safeJavaScriptString(exception?.objectForKeyedSubscript("stack")) ?? ""
|
|
2990
|
-
|
|
3290
|
+
let screen = self?.screenName(in: context) ?? "screen"
|
|
3291
|
+
CraftNativeConsole.write("error", category: screen, message: stack.isEmpty || stack == "undefined" ? "Uncaught \(message)" : "Uncaught \(message)\n\(stack)")
|
|
3292
|
+
// To the app's crash reporter too (bughq, when configured): an
|
|
3293
|
+
// uncaught error in a shipped screen is otherwise only in a log
|
|
3294
|
+
// nobody reads on someone else's phone.
|
|
3295
|
+
CraftCrashReporter.shared.captureJavaScript(
|
|
3296
|
+
name: Self.safeJavaScriptString(exception?.objectForKeyedSubscript("name")),
|
|
3297
|
+
message: message,
|
|
3298
|
+
stack: stack,
|
|
3299
|
+
route: screen,
|
|
3300
|
+
isAppScript: self?.isAppScript ?? false
|
|
3301
|
+
)
|
|
2991
3302
|
self?.hybridFailed(message)
|
|
2992
3303
|
}
|
|
2993
3304
|
let postMessage: @convention(block) (String) -> Void = { [weak self] json in
|
|
@@ -4215,6 +4526,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4215
4526
|
var rootStyle = next.style
|
|
4216
4527
|
if rootStyle["flex"] == nil, rootStyle["height"] == nil { rootStyle["flex"] = 1 }
|
|
4217
4528
|
rootStack.setLayoutStyle(rootStyle, for: next.view)
|
|
4529
|
+
rootStack.setViewportChild(next.view, fillsHeight: rootStyle["height"] == nil)
|
|
4218
4530
|
rootStack.invalidateMeasurements()
|
|
4219
4531
|
renderedRoot = next
|
|
4220
4532
|
rootIsSafeAreaView = next.type == "SafeAreaView"
|
|
@@ -4229,6 +4541,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4229
4541
|
if let root = renderedRoot { detach(root, from: rootStack) }
|
|
4230
4542
|
renderedRoot = nil
|
|
4231
4543
|
rootIsSafeAreaView = false
|
|
4544
|
+
rootStack.setViewportChild(nil, fillsHeight: false)
|
|
4232
4545
|
rootTopToSafeArea?.isActive = true
|
|
4233
4546
|
rootTopToView?.isActive = false
|
|
4234
4547
|
rootBottomToSafeArea?.isActive = true
|
|
@@ -4520,10 +4833,13 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4520
4833
|
updateHandler(nonEmptyHandler(events["onValueChange"]) ?? nonEmptyHandler(events["onChange"]), for: toggle)
|
|
4521
4834
|
case "Slider":
|
|
4522
4835
|
let slider = result as! UISlider
|
|
4523
|
-
|
|
4524
|
-
slider.
|
|
4525
|
-
let
|
|
4526
|
-
|
|
4836
|
+
let minimum = (props["minimumValue"] as? NSNumber)?.floatValue ?? 0
|
|
4837
|
+
slider.minimumValue = minimum.isFinite ? minimum : 0
|
|
4838
|
+
let maximum = (props["maximumValue"] as? NSNumber)?.floatValue ?? 1
|
|
4839
|
+
slider.maximumValue = max(slider.minimumValue + .leastNonzeroMagnitude, maximum.isFinite ? maximum : 1)
|
|
4840
|
+
let authoredValue = (props["value"] as? NSNumber)?.floatValue ?? slider.minimumValue
|
|
4841
|
+
let value = authoredValue.isFinite ? authoredValue : slider.minimumValue
|
|
4842
|
+
if let step = (props["step"] as? NSNumber)?.floatValue, step.isFinite, step > 0 { sliderSteps[ObjectIdentifier(slider)] = step }
|
|
4527
4843
|
else { sliderSteps.removeValue(forKey: ObjectIdentifier(slider)) }
|
|
4528
4844
|
slider.value = snappedSliderValue(value, for: slider)
|
|
4529
4845
|
slider.minimumTrackTintColor = color(props["minimumTrackTintColor"])
|
|
@@ -4538,7 +4854,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4538
4854
|
indicator.style = (requestedSize as? String) == "large" ? .large : .medium
|
|
4539
4855
|
let scale: CGFloat
|
|
4540
4856
|
if let numericSize = requestedSize as? NSNumber {
|
|
4541
|
-
|
|
4857
|
+
let value = CGFloat(numericSize.doubleValue / 20.0)
|
|
4858
|
+
scale = value.isFinite ? max(0.5, value) : 1
|
|
4542
4859
|
} else if (requestedSize as? String) == "small" {
|
|
4543
4860
|
scale = 0.75
|
|
4544
4861
|
} else {
|
|
@@ -4794,6 +5111,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4794
5111
|
case "checkbox": return "Checkbox"
|
|
4795
5112
|
case "textinput", "input": return "TextInput"
|
|
4796
5113
|
case "picker", "select": return "Picker"
|
|
5114
|
+
case "datepicker", "date-picker", "date": return "DatePicker"
|
|
4797
5115
|
case "modal": return "Modal"
|
|
4798
5116
|
case "switch": return "Switch"
|
|
4799
5117
|
case "slider": return "Slider"
|
|
@@ -4933,14 +5251,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4933
5251
|
{ [weak self] scrollView in self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": self?.scrollEvent(scrollView) ?? [:]]) }
|
|
4934
5252
|
}
|
|
4935
5253
|
|
|
5254
|
+
let threshold = max(
|
|
5255
|
+
0,
|
|
5256
|
+
Double(number(props["onEndReachedThreshold"]) ?? number(props["threshold"]) ?? 0.1)
|
|
5257
|
+
)
|
|
4936
5258
|
list.apply(
|
|
4937
5259
|
nodes: children,
|
|
4938
5260
|
horizontal: props["horizontal"] as? Bool == true,
|
|
4939
5261
|
columns: (props["numColumns"] as? NSNumber)?.intValue ?? 1,
|
|
4940
5262
|
inverted: props["inverted"] as? Bool == true,
|
|
4941
|
-
endReachedThreshold:
|
|
4942
|
-
?? (props["threshold"] as? NSNumber)?.doubleValue
|
|
4943
|
-
?? 0.1,
|
|
5263
|
+
endReachedThreshold: threshold,
|
|
4944
5264
|
renderItem: { [weak self] node, identity, _ in
|
|
4945
5265
|
guard let self = self else { return UIView() }
|
|
4946
5266
|
let previous = self.flatListRows[listKey]?[identity]
|
|
@@ -5083,7 +5403,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5083
5403
|
|
|
5084
5404
|
private func emitLayoutEvents(for node: RenderedNode) {
|
|
5085
5405
|
let id = ObjectIdentifier(node.view)
|
|
5086
|
-
if let handler = layoutHandlers[id], node.view
|
|
5406
|
+
if let handler = layoutHandlers[id], isEffectivelyVisible(node.view) {
|
|
5087
5407
|
let frame = node.view.frame
|
|
5088
5408
|
if lastLayoutFrames[id] != frame {
|
|
5089
5409
|
lastLayoutFrames[id] = frame
|
|
@@ -5104,6 +5424,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5104
5424
|
}
|
|
5105
5425
|
}
|
|
5106
5426
|
|
|
5427
|
+
private func isEffectivelyVisible(_ view: UIView) -> Bool {
|
|
5428
|
+
guard view.window != nil, !view.isHidden else { return false }
|
|
5429
|
+
var ancestor = view.superview
|
|
5430
|
+
while let current = ancestor {
|
|
5431
|
+
if current.isHidden { return false }
|
|
5432
|
+
ancestor = current.superview
|
|
5433
|
+
}
|
|
5434
|
+
return true
|
|
5435
|
+
}
|
|
5436
|
+
|
|
5107
5437
|
private func updateAuxiliaryHandler(
|
|
5108
5438
|
_ handler: String?,
|
|
5109
5439
|
in handlers: inout [ObjectIdentifier: String],
|
|
@@ -5310,10 +5640,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5310
5640
|
stack.alignItems = style["alignItems"] as? String ?? "stretch"
|
|
5311
5641
|
stack.alignContent = style["alignContent"] as? String ?? "flex-start"
|
|
5312
5642
|
stack.justifyContent = style["justifyContent"] as? String ?? "flex-start"
|
|
5313
|
-
let gap = number(style["gap"]) ?? 0
|
|
5643
|
+
let gap = max(0, number(style["gap"]) ?? 0)
|
|
5314
5644
|
stack.gap = gap
|
|
5315
|
-
stack.rowGap = number(style["rowGap"]) ?? gap
|
|
5316
|
-
stack.columnGap = number(style["columnGap"]) ?? gap
|
|
5645
|
+
stack.rowGap = max(0, number(style["rowGap"]) ?? gap)
|
|
5646
|
+
stack.columnGap = max(0, number(style["columnGap"]) ?? gap)
|
|
5317
5647
|
stack.grid = style["display"] as? String == "grid"
|
|
5318
5648
|
stack.gridColumns = craftNativeGridColumnCount(style["gridTemplateColumns"] ?? style["gridColumns"])
|
|
5319
5649
|
stack.gridAutoRows = CraftNativeLength(style["gridAutoRows"])
|
|
@@ -5388,14 +5718,18 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5388
5718
|
}
|
|
5389
5719
|
|
|
5390
5720
|
private func paddingInsets(_ style: [String: Any]) -> UIEdgeInsets {
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5721
|
+
func nonNegative(_ value: Any?) -> CGFloat? {
|
|
5722
|
+
guard let value = number(value), value >= 0 else { return nil }
|
|
5723
|
+
return value
|
|
5724
|
+
}
|
|
5725
|
+
let padding = nonNegative(style["padding"]) ?? 0
|
|
5726
|
+
let horizontal = nonNegative(style["paddingHorizontal"]) ?? padding
|
|
5727
|
+
let vertical = nonNegative(style["paddingVertical"]) ?? padding
|
|
5394
5728
|
return UIEdgeInsets(
|
|
5395
|
-
top:
|
|
5396
|
-
left:
|
|
5397
|
-
bottom:
|
|
5398
|
-
right:
|
|
5729
|
+
top: nonNegative(style["paddingTop"]) ?? vertical,
|
|
5730
|
+
left: nonNegative(style["paddingLeft"]) ?? horizontal,
|
|
5731
|
+
bottom: nonNegative(style["paddingBottom"]) ?? vertical,
|
|
5732
|
+
right: nonNegative(style["paddingRight"]) ?? horizontal
|
|
5399
5733
|
)
|
|
5400
5734
|
}
|
|
5401
5735
|
|
|
@@ -5655,6 +5989,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5655
5989
|
imageSources[id] = uri
|
|
5656
5990
|
imageErrors.removeValue(forKey: id)
|
|
5657
5991
|
view.image = immediateImage(defaultSource).map { imageForDisplay($0, view: view) }
|
|
5992
|
+
imageDidChange(view)
|
|
5658
5993
|
view.accessibilityValue = nil
|
|
5659
5994
|
emitImageEvent(view, handler: imageLoadStartHandlers[id])
|
|
5660
5995
|
|
|
@@ -5666,6 +6001,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5666
6001
|
return
|
|
5667
6002
|
}
|
|
5668
6003
|
view.image = imageForDisplay(image, view: view)
|
|
6004
|
+
imageDidChange(view)
|
|
5669
6005
|
emitImageEvent(view, handler: imageLoadHandlers[id])
|
|
5670
6006
|
emitImageEvent(view, handler: imageLoadEndHandlers[id])
|
|
5671
6007
|
return
|
|
@@ -5681,6 +6017,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5681
6017
|
return
|
|
5682
6018
|
}
|
|
5683
6019
|
view.image = self.imageForDisplay(image, view: view)
|
|
6020
|
+
self.imageDidChange(view)
|
|
5684
6021
|
self.emitImageEvent(view, handler: self.imageLoadHandlers[ObjectIdentifier(view)])
|
|
5685
6022
|
self.emitImageEvent(view, handler: self.imageLoadEndHandlers[ObjectIdentifier(view)])
|
|
5686
6023
|
}
|
|
@@ -5695,6 +6032,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5695
6032
|
return
|
|
5696
6033
|
}
|
|
5697
6034
|
view.image = imageForDisplay(image, view: view)
|
|
6035
|
+
imageDidChange(view)
|
|
5698
6036
|
emitImageEvent(view, handler: imageLoadHandlers[id])
|
|
5699
6037
|
emitImageEvent(view, handler: imageLoadEndHandlers[id])
|
|
5700
6038
|
return
|
|
@@ -5719,6 +6057,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5719
6057
|
private func imageFailure(_ view: UIImageView, uri: String?, message: String) {
|
|
5720
6058
|
imageErrors[ObjectIdentifier(view)] = message
|
|
5721
6059
|
view.image = nil
|
|
6060
|
+
imageDidChange(view)
|
|
5722
6061
|
view.accessibilityValue = message
|
|
5723
6062
|
let id = ObjectIdentifier(view)
|
|
5724
6063
|
emitImageEvent(view, handler: imageErrorHandlers[id], nativeEvent: ["error": ["message": message]])
|
|
@@ -5726,6 +6065,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5726
6065
|
NSLog("[craft native] %@: %@", message, uri ?? "<missing>")
|
|
5727
6066
|
}
|
|
5728
6067
|
|
|
6068
|
+
private func imageDidChange(_ view: UIImageView) {
|
|
6069
|
+
view.invalidateIntrinsicContentSize()
|
|
6070
|
+
CraftNativeFlexLayout.invalidateAncestors(of: view)
|
|
6071
|
+
}
|
|
6072
|
+
|
|
5729
6073
|
private func imageForDisplay(_ image: UIImage, view: UIImageView) -> UIImage {
|
|
5730
6074
|
image.withRenderingMode(tintedImages.contains(ObjectIdentifier(view)) ? .alwaysTemplate : .alwaysOriginal)
|
|
5731
6075
|
}
|
|
@@ -5749,7 +6093,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5749
6093
|
view.accessibilityValue = nil
|
|
5750
6094
|
}
|
|
5751
6095
|
let role = props["accessibilityRole"] as? String
|
|
5752
|
-
view.isAccessibilityElement = role != "none" && (view.accessibilityLabel != nil || role != nil || ["Text", "Button", "Link", "Checkbox", "Image", "ImageBackground", "Video", "TextInput", "Picker", "Switch", "Slider"].contains(type))
|
|
6096
|
+
view.isAccessibilityElement = role != "none" && (view.accessibilityLabel != nil || role != nil || ["Text", "Button", "Link", "Checkbox", "Image", "ImageBackground", "Video", "TextInput", "Picker", "DatePicker", "Switch", "Slider"].contains(type))
|
|
5753
6097
|
var traits: UIAccessibilityTraits = []
|
|
5754
6098
|
switch role ?? type.lowercased() {
|
|
5755
6099
|
case "button", "checkbox": traits.insert(.button)
|
|
@@ -5983,7 +6327,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5983
6327
|
/// Countdown values use seconds when numeric, or `HH:mm[:ss]` text.
|
|
5984
6328
|
static func countdownDuration(_ value: Any?) -> TimeInterval? {
|
|
5985
6329
|
let maximum: TimeInterval = 23 * 3600 + 59 * 60 + 59
|
|
5986
|
-
if let number = value as? NSNumber, !(value is Bool) {
|
|
6330
|
+
if let number = value as? NSNumber, !(value is Bool) {
|
|
6331
|
+
let seconds = number.doubleValue
|
|
6332
|
+
guard seconds.isFinite else { return nil }
|
|
6333
|
+
return min(maximum, max(0, seconds))
|
|
6334
|
+
}
|
|
5987
6335
|
guard let text = (value as? String)?.trimmingCharacters(in: .whitespaces), !text.isEmpty else { return nil }
|
|
5988
6336
|
if let seconds = Double(text), seconds.isFinite { return min(maximum, max(0, seconds)) }
|
|
5989
6337
|
let parts = text.split(separator: ":")
|
|
@@ -5991,7 +6339,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5991
6339
|
let hours = Double(parts[0]),
|
|
5992
6340
|
let minutes = Double(parts[1]) else { return nil }
|
|
5993
6341
|
let seconds = parts.count == 3 ? (Double(parts[2]) ?? -1) : 0
|
|
5994
|
-
guard hours
|
|
6342
|
+
guard hours.isFinite, minutes.isFinite, seconds.isFinite,
|
|
6343
|
+
hours >= 0, minutes >= 0, minutes < 60, seconds >= 0, seconds < 60 else { return nil }
|
|
5995
6344
|
return min(maximum, hours * 3600 + minutes * 60 + seconds)
|
|
5996
6345
|
}
|
|
5997
6346
|
|
|
@@ -5999,7 +6348,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5999
6348
|
/// time), `2026-10-12T07:30` (local), a full ISO 8601 string, or
|
|
6000
6349
|
/// milliseconds since 1970. Nil for anything else.
|
|
6001
6350
|
static func pickerDate(_ value: Any?) -> Date? {
|
|
6002
|
-
if let number = value as? NSNumber, !(value is Bool) {
|
|
6351
|
+
if let number = value as? NSNumber, !(value is Bool) {
|
|
6352
|
+
let milliseconds = number.doubleValue
|
|
6353
|
+
guard milliseconds.isFinite else { return nil }
|
|
6354
|
+
return Date(timeIntervalSince1970: milliseconds / 1000)
|
|
6355
|
+
}
|
|
6003
6356
|
guard let text = (value as? String)?.trimmingCharacters(in: .whitespaces), !text.isEmpty else { return nil }
|
|
6004
6357
|
let local = DateFormatter()
|
|
6005
6358
|
local.locale = Locale(identifier: "en_US_POSIX")
|
|
@@ -6157,7 +6510,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6157
6510
|
}
|
|
6158
6511
|
|
|
6159
6512
|
private func number(_ value: Any?) -> CGFloat? {
|
|
6160
|
-
|
|
6513
|
+
guard let number = value as? NSNumber else { return nil }
|
|
6514
|
+
let value = CGFloat(truncating: number)
|
|
6515
|
+
return value.isFinite ? value : nil
|
|
6161
6516
|
}
|
|
6162
6517
|
|
|
6163
6518
|
private func keyboardType(_ value: Any?) -> UIKeyboardType {
|