craft-native 0.0.175 → 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 +986 -304
- 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 +393 -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,6 +611,7 @@ 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] = [:]
|
|
434
616
|
private var overlayWidths: [ObjectIdentifier: CGFloat] = [:]
|
|
435
617
|
private(set) var markerHandlers: [String: String] = [:]
|
|
@@ -479,7 +661,10 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
479
661
|
}
|
|
480
662
|
} else if type == "mappolyline" || type == "polyline" {
|
|
481
663
|
guard points.count > 1 else { continue }
|
|
482
|
-
|
|
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)
|
|
483
668
|
}
|
|
484
669
|
}
|
|
485
670
|
|
|
@@ -499,13 +684,31 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
499
684
|
}
|
|
500
685
|
markerHandlers = nextHandlers
|
|
501
686
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
+
}
|
|
506
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
|
+
}
|
|
507
709
|
let overlay = MKPolyline(coordinates: value.coordinates, count: value.coordinates.count)
|
|
508
710
|
overlaysByID[id] = overlay
|
|
711
|
+
overlayCoordinatesByID[id] = value.coordinates
|
|
509
712
|
overlayColors[ObjectIdentifier(overlay)] = value.color
|
|
510
713
|
overlayWidths[ObjectIdentifier(overlay)] = value.width
|
|
511
714
|
addOverlay(overlay)
|
|
@@ -557,8 +760,19 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
557
760
|
let numbers = (value as? [Any])?.compactMap { ($0 as? NSNumber)?.doubleValue }
|
|
558
761
|
?? (value as? [NSNumber])?.map(\.doubleValue)
|
|
559
762
|
?? []
|
|
560
|
-
return stride(from: 0, to: numbers.count - 1, by: 2).
|
|
561
|
-
|
|
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
|
|
562
776
|
}
|
|
563
777
|
}
|
|
564
778
|
|
|
@@ -569,7 +783,11 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
569
783
|
let latitude = (center["latitude"] as? NSNumber)?.doubleValue,
|
|
570
784
|
let longitude = (center["longitude"] as? NSNumber)?.doubleValue,
|
|
571
785
|
let latitudeDelta = (span["latitudeDelta"] as? NSNumber)?.doubleValue ?? (span["latitude_delta"] as? NSNumber)?.doubleValue,
|
|
572
|
-
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 }
|
|
573
791
|
return MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), span: MKCoordinateSpan(latitudeDelta: latitudeDelta, longitudeDelta: longitudeDelta))
|
|
574
792
|
}
|
|
575
793
|
|
|
@@ -631,15 +849,17 @@ private enum CraftNativeLength: Equatable {
|
|
|
631
849
|
|
|
632
850
|
init?(_ value: Any?) {
|
|
633
851
|
if let number = value as? NSNumber {
|
|
634
|
-
|
|
852
|
+
let points = CGFloat(truncating: number)
|
|
853
|
+
guard points.isFinite else { return nil }
|
|
854
|
+
self = .points(points)
|
|
635
855
|
return
|
|
636
856
|
}
|
|
637
857
|
guard let text = (value as? String)?.trimmingCharacters(in: .whitespaces), !text.isEmpty else { return nil }
|
|
638
|
-
if text.hasSuffix("%"), let fraction = Double(text.dropLast()) {
|
|
858
|
+
if text.hasSuffix("%"), let fraction = Double(text.dropLast()), fraction.isFinite {
|
|
639
859
|
self = .percent(CGFloat(fraction) / 100)
|
|
640
|
-
} 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 {
|
|
641
861
|
self = .points(CGFloat(points))
|
|
642
|
-
} else if let points = Double(text) {
|
|
862
|
+
} else if let points = Double(text), points.isFinite {
|
|
643
863
|
self = .points(CGFloat(points))
|
|
644
864
|
} else {
|
|
645
865
|
return nil
|
|
@@ -648,7 +868,7 @@ private enum CraftNativeLength: Equatable {
|
|
|
648
868
|
|
|
649
869
|
func resolve(_ base: CGFloat?) -> CGFloat? {
|
|
650
870
|
switch self {
|
|
651
|
-
case .points(let value): return value
|
|
871
|
+
case .points(let value): return max(0, value)
|
|
652
872
|
case .percent(let fraction): return base.map { max(0, $0 * fraction) }
|
|
653
873
|
}
|
|
654
874
|
}
|
|
@@ -685,7 +905,11 @@ private struct CraftNativeLayoutStyle {
|
|
|
685
905
|
static let empty = CraftNativeLayoutStyle([:])
|
|
686
906
|
|
|
687
907
|
init(_ raw: [String: Any]) {
|
|
688
|
-
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
|
+
}
|
|
689
913
|
width = CraftNativeLength(raw["width"])
|
|
690
914
|
height = CraftNativeLength(raw["height"])
|
|
691
915
|
let ratio = number(raw["aspectRatio"])
|
|
@@ -987,6 +1211,13 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
987
1211
|
|
|
988
1212
|
private var flowChildren: [UIView] = []
|
|
989
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
|
|
990
1221
|
|
|
991
1222
|
private struct MeasureKey: Hashable {
|
|
992
1223
|
let width: CGFloat?
|
|
@@ -1088,6 +1319,12 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1088
1319
|
invalidateMeasurements()
|
|
1089
1320
|
}
|
|
1090
1321
|
|
|
1322
|
+
func setViewportChild(_ view: UIView?, fillsHeight: Bool) {
|
|
1323
|
+
viewportChild = view
|
|
1324
|
+
viewportChildFillsHeight = fillsHeight
|
|
1325
|
+
invalidateMeasurements()
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1091
1328
|
func removeLayoutStyle(for view: UIView) {
|
|
1092
1329
|
view.craftLayoutStyle = .empty
|
|
1093
1330
|
}
|
|
@@ -1122,7 +1359,12 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1122
1359
|
let scale = window?.screen.scale ?? traitCollection.displayScale
|
|
1123
1360
|
let result = layout(width: bounds.width, height: bounds.height, maxWidth: bounds.width, maxHeight: bounds.height, place: true)
|
|
1124
1361
|
for (child, frame) in result.frames {
|
|
1125
|
-
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)
|
|
1126
1368
|
// Snapping each edge can take a pixel off a label (15.83…33.83
|
|
1127
1369
|
// becomes 16…33.67), and a label a pixel narrower than its text
|
|
1128
1370
|
// truncates it ("13" drew as "…"). Text keeps its whole size.
|
|
@@ -1205,6 +1447,29 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1205
1447
|
/// outer size when its parent fixes it, nil when it sizes to its content
|
|
1206
1448
|
/// within `maxWidth`/`maxHeight`. Returns its outer size and, when
|
|
1207
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
|
+
|
|
1208
1473
|
private func layout(
|
|
1209
1474
|
width: CGFloat?,
|
|
1210
1475
|
height: CGFloat?,
|
|
@@ -1239,6 +1504,9 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1239
1504
|
frames: &frames
|
|
1240
1505
|
)
|
|
1241
1506
|
}
|
|
1507
|
+
if place, flow.count == children.count, let shared = sharedDirectFrames(flow, width: width, height: height) {
|
|
1508
|
+
frames = shared
|
|
1509
|
+
}
|
|
1242
1510
|
let size = CGSize(width: width ?? contentSize.width + paddingWidth, height: height ?? contentSize.height + paddingHeight)
|
|
1243
1511
|
|
|
1244
1512
|
if place {
|
|
@@ -1286,6 +1554,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1286
1554
|
place: Bool,
|
|
1287
1555
|
frames: inout [(UIView, CGRect)]
|
|
1288
1556
|
) -> CGSize {
|
|
1557
|
+
let contentPadding = effectivePadding
|
|
1289
1558
|
let definiteMain = horizontal ? innerWidth : innerHeight
|
|
1290
1559
|
let definiteCross = horizontal ? innerHeight : innerWidth
|
|
1291
1560
|
let roomMain = horizontal ? roomWidth : roomHeight
|
|
@@ -1647,6 +1916,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1647
1916
|
place: Bool,
|
|
1648
1917
|
frames: inout [(UIView, CGRect)]
|
|
1649
1918
|
) -> CGSize {
|
|
1919
|
+
let contentPadding = effectivePadding
|
|
1650
1920
|
let columns = max(1, gridColumns)
|
|
1651
1921
|
let horizontalGap = columnGap ?? gap
|
|
1652
1922
|
let verticalGap = rowGap ?? gap
|
|
@@ -1670,7 +1940,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1670
1940
|
let available = max(0, cellWidth - style.marginLeft - style.marginRight)
|
|
1671
1941
|
let align = style.alignSelf ?? alignItems
|
|
1672
1942
|
var fixedWidth = style.width?.resolve(cellWidth)
|
|
1673
|
-
var fixedHeight = style.height?.resolve(
|
|
1943
|
+
var fixedHeight = style.height?.resolve(innerHeight)
|
|
1674
1944
|
// A cell's item is as wide as its column unless it sets a width
|
|
1675
1945
|
// (or an aspect ratio derives one from its height), whatever its
|
|
1676
1946
|
// height: a fixed-height swatch with no content was drawn 0 wide.
|
|
@@ -1704,7 +1974,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1704
1974
|
let size = sizes[index]
|
|
1705
1975
|
let alignment: UInt32 = align == "center" ? 1 : align == "flex-end" ? 2 : 0
|
|
1706
1976
|
var explicitWidth = style.width?.resolve(cellWidth)
|
|
1707
|
-
var explicitHeight = style.height?.resolve(
|
|
1977
|
+
var explicitHeight = style.height?.resolve(innerHeight)
|
|
1708
1978
|
if explicitWidth == nil && align == "stretch" && (explicitHeight == nil || style.aspectRatio == nil) {
|
|
1709
1979
|
explicitWidth = max(0, cellWidth - style.marginLeft - style.marginRight)
|
|
1710
1980
|
}
|
|
@@ -1713,7 +1983,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1713
1983
|
size.width, size.height,
|
|
1714
1984
|
explicitWidth, explicitHeight,
|
|
1715
1985
|
style.minWidth?.resolve(cellWidth), style.maxWidth?.resolve(cellWidth),
|
|
1716
|
-
style.minHeight?.resolve(
|
|
1986
|
+
style.minHeight?.resolve(innerHeight), style.maxHeight?.resolve(innerHeight),
|
|
1717
1987
|
style.marginLeft, style.marginRight, style.marginTop, style.marginBottom,
|
|
1718
1988
|
align == "stretch", alignment
|
|
1719
1989
|
)
|
|
@@ -1822,7 +2092,7 @@ private final class CraftNativeKeyboardAvoidingView: CraftNativeFlowView {
|
|
|
1822
2092
|
func configureKeyboard(props: [String: Any]) {
|
|
1823
2093
|
let requestedBehavior = (props["behavior"] as? String) ?? "padding"
|
|
1824
2094
|
keyboardBehavior = ["height", "position", "padding"].contains(requestedBehavior) ? requestedBehavior : "padding"
|
|
1825
|
-
keyboardVerticalOffset = (props["keyboardVerticalOffset"]
|
|
2095
|
+
keyboardVerticalOffset = number(props["keyboardVerticalOffset"]) ?? 0
|
|
1826
2096
|
if keyboardBehavior != "height" { appliedHeightInset = 0 }
|
|
1827
2097
|
authoredPadding = padding
|
|
1828
2098
|
applyKeyboardAvoidance()
|
|
@@ -2323,7 +2593,7 @@ private final class CraftNativeSvgView: UIView {
|
|
|
2323
2593
|
let props = drawing["props"] as? [String: Any] ?? [:]
|
|
2324
2594
|
guard let box = props["viewBox"] as? [Any], box.count == 4 else { return nil }
|
|
2325
2595
|
let n = box.compactMap { ($0 as? NSNumber).map { CGFloat(truncating: $0) } }
|
|
2326
|
-
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 }
|
|
2327
2597
|
return CGRect(x: n[0], y: n[1], width: n[2], height: n[3])
|
|
2328
2598
|
}
|
|
2329
2599
|
|
|
@@ -2344,21 +2614,29 @@ private final class CraftNativeSvgView: UIView {
|
|
|
2344
2614
|
var linecap: String?
|
|
2345
2615
|
var linejoin: String?
|
|
2346
2616
|
var dashes: [CGFloat]?
|
|
2617
|
+
var vectorEffect: String?
|
|
2347
2618
|
|
|
2348
2619
|
func merged(_ node: [String: Any]) -> Paint {
|
|
2349
2620
|
var next = self
|
|
2350
2621
|
let props = node["props"] as? [String: Any] ?? [:]
|
|
2351
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
|
+
}
|
|
2352
2629
|
if let color = style["color"] as? String { next.color = color }
|
|
2353
2630
|
if let fill = style["fill"] as? String ?? props["fill"] as? String { next.fill = fill }
|
|
2354
2631
|
if let stroke = style["stroke"] as? String ?? props["stroke"] as? String { next.stroke = stroke }
|
|
2355
|
-
if let width = (style["strokeWidth"]
|
|
2632
|
+
if let width = finite(style["strokeWidth"] ?? props["strokeWidth"]) { next.strokeWidth = width }
|
|
2356
2633
|
// Computed style wins over the component prop, matching Android
|
|
2357
2634
|
// and the browser's cascade when both provide opacity.
|
|
2358
|
-
if let opacity = style["opacity"]
|
|
2359
|
-
if let cap =
|
|
2360
|
-
if let join =
|
|
2361
|
-
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 }
|
|
2362
2640
|
return next
|
|
2363
2641
|
}
|
|
2364
2642
|
|
|
@@ -2425,7 +2703,7 @@ private final class CraftNativeSvgView: UIView {
|
|
|
2425
2703
|
// `vector-effect="non-scaling-stroke"`: the stroke is in the
|
|
2426
2704
|
// view's points, so a stretched chart's line keeps its width
|
|
2427
2705
|
// and a round cap stays a round dot.
|
|
2428
|
-
let nonScaling =
|
|
2706
|
+
let nonScaling = own.vectorEffect == "non-scaling-stroke"
|
|
2429
2707
|
let line: UIBezierPath
|
|
2430
2708
|
if nonScaling, let copy = path.copy() as? UIBezierPath {
|
|
2431
2709
|
copy.apply(viewTransform)
|
|
@@ -2452,14 +2730,14 @@ private final class CraftNativeSvgView: UIView {
|
|
|
2452
2730
|
guard !text.isEmpty else { return }
|
|
2453
2731
|
let props = node["props"] as? [String: Any] ?? [:]
|
|
2454
2732
|
let style = node["style"] as? [String: Any] ?? [:]
|
|
2455
|
-
let size =
|
|
2733
|
+
let size = number(props["fontSize"]) ?? number(style["fontSize"]) ?? 12
|
|
2456
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
|
|
2457
2735
|
let string = NSAttributedString(string: text, attributes: [
|
|
2458
2736
|
.font: UIFont.systemFont(ofSize: size, weight: weight),
|
|
2459
2737
|
.foregroundColor: paint.resolve(paint.fill) ?? UIColor.label,
|
|
2460
2738
|
])
|
|
2461
|
-
var x =
|
|
2462
|
-
let y =
|
|
2739
|
+
var x = number(props["x"]) ?? 0
|
|
2740
|
+
let y = number(props["y"]) ?? 0
|
|
2463
2741
|
switch props["textAnchor"] as? String {
|
|
2464
2742
|
case "middle": x -= string.size().width / 2
|
|
2465
2743
|
case "end": x -= string.size().width
|
|
@@ -2472,7 +2750,7 @@ private final class CraftNativeSvgView: UIView {
|
|
|
2472
2750
|
static func path(_ value: Any?) -> UIBezierPath? {
|
|
2473
2751
|
guard let list = value as? [Any] else { return nil }
|
|
2474
2752
|
let n = list.compactMap { ($0 as? NSNumber).map { CGFloat(truncating: $0) } }
|
|
2475
|
-
guard !n.isEmpty, n.count == list.count else { return nil }
|
|
2753
|
+
guard !n.isEmpty, n.count == list.count, n.allSatisfy(\.isFinite) else { return nil }
|
|
2476
2754
|
let path = UIBezierPath()
|
|
2477
2755
|
var i = 0
|
|
2478
2756
|
func point(_ at: Int) -> CGPoint { CGPoint(x: n[at], y: n[at + 1]) }
|
|
@@ -2909,6 +3187,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2909
3187
|
override func viewDidAppear(_ animated: Bool) {
|
|
2910
3188
|
super.viewDidAppear(animated)
|
|
2911
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
|
+
}
|
|
2912
3195
|
}
|
|
2913
3196
|
|
|
2914
3197
|
override func viewDidDisappear(_ animated: Bool) {
|
|
@@ -3004,7 +3287,18 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3004
3287
|
?? Self.safeJavaScriptString(exception)
|
|
3005
3288
|
?? "unknown"
|
|
3006
3289
|
let stack = Self.safeJavaScriptString(exception?.objectForKeyedSubscript("stack")) ?? ""
|
|
3007
|
-
|
|
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
|
+
)
|
|
3008
3302
|
self?.hybridFailed(message)
|
|
3009
3303
|
}
|
|
3010
3304
|
let postMessage: @convention(block) (String) -> Void = { [weak self] json in
|
|
@@ -4232,6 +4526,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4232
4526
|
var rootStyle = next.style
|
|
4233
4527
|
if rootStyle["flex"] == nil, rootStyle["height"] == nil { rootStyle["flex"] = 1 }
|
|
4234
4528
|
rootStack.setLayoutStyle(rootStyle, for: next.view)
|
|
4529
|
+
rootStack.setViewportChild(next.view, fillsHeight: rootStyle["height"] == nil)
|
|
4235
4530
|
rootStack.invalidateMeasurements()
|
|
4236
4531
|
renderedRoot = next
|
|
4237
4532
|
rootIsSafeAreaView = next.type == "SafeAreaView"
|
|
@@ -4246,6 +4541,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4246
4541
|
if let root = renderedRoot { detach(root, from: rootStack) }
|
|
4247
4542
|
renderedRoot = nil
|
|
4248
4543
|
rootIsSafeAreaView = false
|
|
4544
|
+
rootStack.setViewportChild(nil, fillsHeight: false)
|
|
4249
4545
|
rootTopToSafeArea?.isActive = true
|
|
4250
4546
|
rootTopToView?.isActive = false
|
|
4251
4547
|
rootBottomToSafeArea?.isActive = true
|
|
@@ -4537,10 +4833,13 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4537
4833
|
updateHandler(nonEmptyHandler(events["onValueChange"]) ?? nonEmptyHandler(events["onChange"]), for: toggle)
|
|
4538
4834
|
case "Slider":
|
|
4539
4835
|
let slider = result as! UISlider
|
|
4540
|
-
|
|
4541
|
-
slider.
|
|
4542
|
-
let
|
|
4543
|
-
|
|
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 }
|
|
4544
4843
|
else { sliderSteps.removeValue(forKey: ObjectIdentifier(slider)) }
|
|
4545
4844
|
slider.value = snappedSliderValue(value, for: slider)
|
|
4546
4845
|
slider.minimumTrackTintColor = color(props["minimumTrackTintColor"])
|
|
@@ -4555,7 +4854,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4555
4854
|
indicator.style = (requestedSize as? String) == "large" ? .large : .medium
|
|
4556
4855
|
let scale: CGFloat
|
|
4557
4856
|
if let numericSize = requestedSize as? NSNumber {
|
|
4558
|
-
|
|
4857
|
+
let value = CGFloat(numericSize.doubleValue / 20.0)
|
|
4858
|
+
scale = value.isFinite ? max(0.5, value) : 1
|
|
4559
4859
|
} else if (requestedSize as? String) == "small" {
|
|
4560
4860
|
scale = 0.75
|
|
4561
4861
|
} else {
|
|
@@ -4811,6 +5111,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4811
5111
|
case "checkbox": return "Checkbox"
|
|
4812
5112
|
case "textinput", "input": return "TextInput"
|
|
4813
5113
|
case "picker", "select": return "Picker"
|
|
5114
|
+
case "datepicker", "date-picker", "date": return "DatePicker"
|
|
4814
5115
|
case "modal": return "Modal"
|
|
4815
5116
|
case "switch": return "Switch"
|
|
4816
5117
|
case "slider": return "Slider"
|
|
@@ -4950,14 +5251,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4950
5251
|
{ [weak self] scrollView in self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": self?.scrollEvent(scrollView) ?? [:]]) }
|
|
4951
5252
|
}
|
|
4952
5253
|
|
|
5254
|
+
let threshold = max(
|
|
5255
|
+
0,
|
|
5256
|
+
Double(number(props["onEndReachedThreshold"]) ?? number(props["threshold"]) ?? 0.1)
|
|
5257
|
+
)
|
|
4953
5258
|
list.apply(
|
|
4954
5259
|
nodes: children,
|
|
4955
5260
|
horizontal: props["horizontal"] as? Bool == true,
|
|
4956
5261
|
columns: (props["numColumns"] as? NSNumber)?.intValue ?? 1,
|
|
4957
5262
|
inverted: props["inverted"] as? Bool == true,
|
|
4958
|
-
endReachedThreshold:
|
|
4959
|
-
?? (props["threshold"] as? NSNumber)?.doubleValue
|
|
4960
|
-
?? 0.1,
|
|
5263
|
+
endReachedThreshold: threshold,
|
|
4961
5264
|
renderItem: { [weak self] node, identity, _ in
|
|
4962
5265
|
guard let self = self else { return UIView() }
|
|
4963
5266
|
let previous = self.flatListRows[listKey]?[identity]
|
|
@@ -5100,7 +5403,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5100
5403
|
|
|
5101
5404
|
private func emitLayoutEvents(for node: RenderedNode) {
|
|
5102
5405
|
let id = ObjectIdentifier(node.view)
|
|
5103
|
-
if let handler = layoutHandlers[id], node.view
|
|
5406
|
+
if let handler = layoutHandlers[id], isEffectivelyVisible(node.view) {
|
|
5104
5407
|
let frame = node.view.frame
|
|
5105
5408
|
if lastLayoutFrames[id] != frame {
|
|
5106
5409
|
lastLayoutFrames[id] = frame
|
|
@@ -5121,6 +5424,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5121
5424
|
}
|
|
5122
5425
|
}
|
|
5123
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
|
+
|
|
5124
5437
|
private func updateAuxiliaryHandler(
|
|
5125
5438
|
_ handler: String?,
|
|
5126
5439
|
in handlers: inout [ObjectIdentifier: String],
|
|
@@ -5327,10 +5640,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5327
5640
|
stack.alignItems = style["alignItems"] as? String ?? "stretch"
|
|
5328
5641
|
stack.alignContent = style["alignContent"] as? String ?? "flex-start"
|
|
5329
5642
|
stack.justifyContent = style["justifyContent"] as? String ?? "flex-start"
|
|
5330
|
-
let gap = number(style["gap"]) ?? 0
|
|
5643
|
+
let gap = max(0, number(style["gap"]) ?? 0)
|
|
5331
5644
|
stack.gap = gap
|
|
5332
|
-
stack.rowGap = number(style["rowGap"]) ?? gap
|
|
5333
|
-
stack.columnGap = number(style["columnGap"]) ?? gap
|
|
5645
|
+
stack.rowGap = max(0, number(style["rowGap"]) ?? gap)
|
|
5646
|
+
stack.columnGap = max(0, number(style["columnGap"]) ?? gap)
|
|
5334
5647
|
stack.grid = style["display"] as? String == "grid"
|
|
5335
5648
|
stack.gridColumns = craftNativeGridColumnCount(style["gridTemplateColumns"] ?? style["gridColumns"])
|
|
5336
5649
|
stack.gridAutoRows = CraftNativeLength(style["gridAutoRows"])
|
|
@@ -5405,14 +5718,18 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5405
5718
|
}
|
|
5406
5719
|
|
|
5407
5720
|
private func paddingInsets(_ style: [String: Any]) -> UIEdgeInsets {
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
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
|
|
5411
5728
|
return UIEdgeInsets(
|
|
5412
|
-
top:
|
|
5413
|
-
left:
|
|
5414
|
-
bottom:
|
|
5415
|
-
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
|
|
5416
5733
|
)
|
|
5417
5734
|
}
|
|
5418
5735
|
|
|
@@ -5672,6 +5989,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5672
5989
|
imageSources[id] = uri
|
|
5673
5990
|
imageErrors.removeValue(forKey: id)
|
|
5674
5991
|
view.image = immediateImage(defaultSource).map { imageForDisplay($0, view: view) }
|
|
5992
|
+
imageDidChange(view)
|
|
5675
5993
|
view.accessibilityValue = nil
|
|
5676
5994
|
emitImageEvent(view, handler: imageLoadStartHandlers[id])
|
|
5677
5995
|
|
|
@@ -5683,6 +6001,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5683
6001
|
return
|
|
5684
6002
|
}
|
|
5685
6003
|
view.image = imageForDisplay(image, view: view)
|
|
6004
|
+
imageDidChange(view)
|
|
5686
6005
|
emitImageEvent(view, handler: imageLoadHandlers[id])
|
|
5687
6006
|
emitImageEvent(view, handler: imageLoadEndHandlers[id])
|
|
5688
6007
|
return
|
|
@@ -5698,6 +6017,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5698
6017
|
return
|
|
5699
6018
|
}
|
|
5700
6019
|
view.image = self.imageForDisplay(image, view: view)
|
|
6020
|
+
self.imageDidChange(view)
|
|
5701
6021
|
self.emitImageEvent(view, handler: self.imageLoadHandlers[ObjectIdentifier(view)])
|
|
5702
6022
|
self.emitImageEvent(view, handler: self.imageLoadEndHandlers[ObjectIdentifier(view)])
|
|
5703
6023
|
}
|
|
@@ -5712,6 +6032,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5712
6032
|
return
|
|
5713
6033
|
}
|
|
5714
6034
|
view.image = imageForDisplay(image, view: view)
|
|
6035
|
+
imageDidChange(view)
|
|
5715
6036
|
emitImageEvent(view, handler: imageLoadHandlers[id])
|
|
5716
6037
|
emitImageEvent(view, handler: imageLoadEndHandlers[id])
|
|
5717
6038
|
return
|
|
@@ -5736,6 +6057,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5736
6057
|
private func imageFailure(_ view: UIImageView, uri: String?, message: String) {
|
|
5737
6058
|
imageErrors[ObjectIdentifier(view)] = message
|
|
5738
6059
|
view.image = nil
|
|
6060
|
+
imageDidChange(view)
|
|
5739
6061
|
view.accessibilityValue = message
|
|
5740
6062
|
let id = ObjectIdentifier(view)
|
|
5741
6063
|
emitImageEvent(view, handler: imageErrorHandlers[id], nativeEvent: ["error": ["message": message]])
|
|
@@ -5743,6 +6065,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5743
6065
|
NSLog("[craft native] %@: %@", message, uri ?? "<missing>")
|
|
5744
6066
|
}
|
|
5745
6067
|
|
|
6068
|
+
private func imageDidChange(_ view: UIImageView) {
|
|
6069
|
+
view.invalidateIntrinsicContentSize()
|
|
6070
|
+
CraftNativeFlexLayout.invalidateAncestors(of: view)
|
|
6071
|
+
}
|
|
6072
|
+
|
|
5746
6073
|
private func imageForDisplay(_ image: UIImage, view: UIImageView) -> UIImage {
|
|
5747
6074
|
image.withRenderingMode(tintedImages.contains(ObjectIdentifier(view)) ? .alwaysTemplate : .alwaysOriginal)
|
|
5748
6075
|
}
|
|
@@ -5766,7 +6093,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5766
6093
|
view.accessibilityValue = nil
|
|
5767
6094
|
}
|
|
5768
6095
|
let role = props["accessibilityRole"] as? String
|
|
5769
|
-
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))
|
|
5770
6097
|
var traits: UIAccessibilityTraits = []
|
|
5771
6098
|
switch role ?? type.lowercased() {
|
|
5772
6099
|
case "button", "checkbox": traits.insert(.button)
|
|
@@ -6000,7 +6327,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6000
6327
|
/// Countdown values use seconds when numeric, or `HH:mm[:ss]` text.
|
|
6001
6328
|
static func countdownDuration(_ value: Any?) -> TimeInterval? {
|
|
6002
6329
|
let maximum: TimeInterval = 23 * 3600 + 59 * 60 + 59
|
|
6003
|
-
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
|
+
}
|
|
6004
6335
|
guard let text = (value as? String)?.trimmingCharacters(in: .whitespaces), !text.isEmpty else { return nil }
|
|
6005
6336
|
if let seconds = Double(text), seconds.isFinite { return min(maximum, max(0, seconds)) }
|
|
6006
6337
|
let parts = text.split(separator: ":")
|
|
@@ -6008,7 +6339,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6008
6339
|
let hours = Double(parts[0]),
|
|
6009
6340
|
let minutes = Double(parts[1]) else { return nil }
|
|
6010
6341
|
let seconds = parts.count == 3 ? (Double(parts[2]) ?? -1) : 0
|
|
6011
|
-
guard hours
|
|
6342
|
+
guard hours.isFinite, minutes.isFinite, seconds.isFinite,
|
|
6343
|
+
hours >= 0, minutes >= 0, minutes < 60, seconds >= 0, seconds < 60 else { return nil }
|
|
6012
6344
|
return min(maximum, hours * 3600 + minutes * 60 + seconds)
|
|
6013
6345
|
}
|
|
6014
6346
|
|
|
@@ -6016,7 +6348,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6016
6348
|
/// time), `2026-10-12T07:30` (local), a full ISO 8601 string, or
|
|
6017
6349
|
/// milliseconds since 1970. Nil for anything else.
|
|
6018
6350
|
static func pickerDate(_ value: Any?) -> Date? {
|
|
6019
|
-
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
|
+
}
|
|
6020
6356
|
guard let text = (value as? String)?.trimmingCharacters(in: .whitespaces), !text.isEmpty else { return nil }
|
|
6021
6357
|
let local = DateFormatter()
|
|
6022
6358
|
local.locale = Locale(identifier: "en_US_POSIX")
|
|
@@ -6174,7 +6510,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6174
6510
|
}
|
|
6175
6511
|
|
|
6176
6512
|
private func number(_ value: Any?) -> CGFloat? {
|
|
6177
|
-
|
|
6513
|
+
guard let number = value as? NSNumber else { return nil }
|
|
6514
|
+
let value = CGFloat(truncating: number)
|
|
6515
|
+
return value.isFinite ? value : nil
|
|
6178
6516
|
}
|
|
6179
6517
|
|
|
6180
6518
|
private func keyboardType(_ value: Any?) -> UIKeyboardType {
|