craft-native 0.0.204 → 0.0.206
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.js +90 -0
- package/dist/android/templates/CraftNativeFlatList.kt.template +117 -0
- package/dist/android/templates/CraftNativeRefreshableFlatList.kt.template +7 -0
- package/dist/android/templates/CraftNativeShell.kt.template +42 -10
- package/dist/android/templates/MainActivityNative.kt.template +628 -99
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +122 -74
- 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.js +11 -0
- package/dist/ios/src/testing.d.ts +6 -4
- package/dist/ios/templates/CraftHybrid.swift +17 -3
- package/dist/ios/templates/CraftNativeFlatList.swift +68 -0
- package/dist/ios/templates/CraftNativeScreen.swift +339 -46
- package/dist/ios/templates/testing/DriverTests.swift.template +58 -6
- package/package.json +1 -1
|
@@ -21,6 +21,12 @@ private func number(_ value: Any?) -> CGFloat? {
|
|
|
21
21
|
return value.isFinite ? value : nil
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
private func craftNativePickerValue(_ value: Any?) -> String? {
|
|
25
|
+
if let value = value as? String, !value.isEmpty { return value }
|
|
26
|
+
if let value = value as? NSNumber, value.doubleValue.isFinite { return String(value.doubleValue) }
|
|
27
|
+
return nil
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
private func craftNativeGridColumnCount(_ value: Any?) -> Int {
|
|
25
31
|
if let number = value as? NSNumber { return max(1, number.intValue) }
|
|
26
32
|
guard let text = value as? String else { return 1 }
|
|
@@ -751,6 +757,9 @@ private final class CraftNativeVideoView: UIView, AVPlayerViewControllerDelegate
|
|
|
751
757
|
private var timeObserver: Any?
|
|
752
758
|
private var loops = false
|
|
753
759
|
private var wasPlaying: Bool?
|
|
760
|
+
private var playIntent: Bool?
|
|
761
|
+
private var userPaused = false
|
|
762
|
+
private var ended = false
|
|
754
763
|
/// This video holds a claim on the audio session (`CraftNativeSounds`).
|
|
755
764
|
private var claimedSession = false
|
|
756
765
|
/// The screen's handlers by event name (`onEnd`, `onPlay`, `onPause`,
|
|
@@ -829,9 +838,17 @@ private final class CraftNativeVideoView: UIView, AVPlayerViewControllerDelegate
|
|
|
829
838
|
|
|
830
839
|
func configure(source: Any?, autoplay: Bool, paused: Bool, loop: Bool, muted: Bool, controls: Bool, resizeMode: String?, pictureInPicture: Bool = false) {
|
|
831
840
|
let uri = (source as? String) ?? (source as? [String: Any])?["uri"] as? String ?? ""
|
|
841
|
+
let nextShouldPlay = autoplay && !paused
|
|
842
|
+
if playIntent != nextShouldPlay {
|
|
843
|
+
userPaused = false
|
|
844
|
+
ended = false
|
|
845
|
+
}
|
|
846
|
+
playIntent = nextShouldPlay
|
|
832
847
|
if uri != sourceURI {
|
|
833
848
|
detachPlayer()
|
|
834
849
|
sourceURI = uri
|
|
850
|
+
userPaused = false
|
|
851
|
+
ended = false
|
|
835
852
|
loops = false
|
|
836
853
|
// The copy `craft.media.keep` put on the phone plays in place of
|
|
837
854
|
// the address, so a kept video plays with no signal.
|
|
@@ -871,7 +888,11 @@ private final class CraftNativeVideoView: UIView, AVPlayerViewControllerDelegate
|
|
|
871
888
|
playerController.canStartPictureInPictureAutomaticallyFromInline = floating
|
|
872
889
|
}
|
|
873
890
|
loops = loop
|
|
874
|
-
if
|
|
891
|
+
if nextShouldPlay {
|
|
892
|
+
if !userPaused && !ended { player.play() }
|
|
893
|
+
} else {
|
|
894
|
+
player.pause()
|
|
895
|
+
}
|
|
875
896
|
}
|
|
876
897
|
|
|
877
898
|
// MARK: AVPlayerViewControllerDelegate
|
|
@@ -915,8 +936,12 @@ private final class CraftNativeVideoView: UIView, AVPlayerViewControllerDelegate
|
|
|
915
936
|
queue: .main
|
|
916
937
|
) { [weak self, weak player] _ in
|
|
917
938
|
guard let self else { return }
|
|
939
|
+
self.ended = !self.loops
|
|
918
940
|
self.fire("onEnd")
|
|
919
|
-
if self.loops {
|
|
941
|
+
if self.loops {
|
|
942
|
+
self.ended = false
|
|
943
|
+
player?.seek(to: .zero) { _ in player?.play() }
|
|
944
|
+
}
|
|
920
945
|
}
|
|
921
946
|
}
|
|
922
947
|
playingObservation = player.observe(\.timeControlStatus, options: [.new]) { [weak self] player, _ in
|
|
@@ -927,6 +952,12 @@ private final class CraftNativeVideoView: UIView, AVPlayerViewControllerDelegate
|
|
|
927
952
|
// Paused before it ever played is where it starts, not news.
|
|
928
953
|
if self.wasPlaying == nil && !playing { self.wasPlaying = false; return }
|
|
929
954
|
self.wasPlaying = playing
|
|
955
|
+
if playing {
|
|
956
|
+
self.userPaused = false
|
|
957
|
+
self.ended = false
|
|
958
|
+
} else if self.playIntent == true {
|
|
959
|
+
self.userPaused = true
|
|
960
|
+
}
|
|
930
961
|
self.fire(playing ? "onPlay" : "onPause")
|
|
931
962
|
}
|
|
932
963
|
}
|
|
@@ -940,6 +971,9 @@ private final class CraftNativeVideoView: UIView, AVPlayerViewControllerDelegate
|
|
|
940
971
|
detachPlayer()
|
|
941
972
|
playerController.player = nil
|
|
942
973
|
sourceURI = ""
|
|
974
|
+
playIntent = nil
|
|
975
|
+
userPaused = false
|
|
976
|
+
ended = false
|
|
943
977
|
if playerController.parent != nil {
|
|
944
978
|
playerController.willMove(toParent: nil)
|
|
945
979
|
playerController.view.removeFromSuperview()
|
|
@@ -956,6 +990,8 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
956
990
|
private var overlayCoordinatesByID: [String: [CLLocationCoordinate2D]] = [:]
|
|
957
991
|
private var overlayColors: [ObjectIdentifier: UIColor] = [:]
|
|
958
992
|
private var overlayWidths: [ObjectIdentifier: CGFloat] = [:]
|
|
993
|
+
private var initialCameraKey: String?
|
|
994
|
+
private var pendingFitKey: String?
|
|
959
995
|
private(set) var markerHandlers: [String: String] = [:]
|
|
960
996
|
var onMarkerTap: ((String) -> Void)?
|
|
961
997
|
|
|
@@ -998,7 +1034,8 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
998
1034
|
if type == "mapmarker" || type == "marker" {
|
|
999
1035
|
guard let point = points.first else { continue }
|
|
1000
1036
|
nextMarkers[id] = (point, childProps["title"] as? String)
|
|
1001
|
-
if let handler = childEvents["onPress"]
|
|
1037
|
+
if let handler = nonEmptyHandler(childEvents["onPress"])
|
|
1038
|
+
?? nonEmptyHandler(childEvents["onMarkerPress"]) {
|
|
1002
1039
|
nextHandlers[id] = handler
|
|
1003
1040
|
}
|
|
1004
1041
|
} else if type == "mappolyline" || type == "polyline" {
|
|
@@ -1056,23 +1093,62 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
1056
1093
|
addOverlay(overlay)
|
|
1057
1094
|
}
|
|
1058
1095
|
|
|
1059
|
-
if let region = Self.region(props["region"]
|
|
1096
|
+
if let region = Self.region(props["region"]) {
|
|
1097
|
+
// `region` is controlled: an authored change is allowed to move
|
|
1098
|
+
// the camera, even when the map itself was already mounted.
|
|
1099
|
+
pendingFitKey = nil
|
|
1060
1100
|
setRegion(region, animated: false)
|
|
1101
|
+
initialCameraKey = nil
|
|
1102
|
+
} else if let initial = Self.region(props["initialRegion"]) {
|
|
1103
|
+
pendingFitKey = nil
|
|
1104
|
+
let key = "region:\(Self.regionKey(initial))"
|
|
1105
|
+
if initialCameraKey != key {
|
|
1106
|
+
setRegion(initial, animated: false)
|
|
1107
|
+
initialCameraKey = key
|
|
1108
|
+
}
|
|
1061
1109
|
} else if props["fitToBounds"] as? Bool == true {
|
|
1062
|
-
|
|
1110
|
+
let key = "fit:\(Self.overlayKey(markers: nextMarkers.mapValues { $0.coordinate }, polylines: nextPolylines.mapValues { $0.coordinates }))"
|
|
1111
|
+
if initialCameraKey != key {
|
|
1112
|
+
// A native screen can reconcile before UIKit assigns the map
|
|
1113
|
+
// its final safe-area-constrained bounds. Keep the fit
|
|
1114
|
+
// pending until there is a real viewport, matching Android's
|
|
1115
|
+
// onSizeChanged retry instead of silently accepting a no-op.
|
|
1116
|
+
pendingFitKey = key
|
|
1117
|
+
if fitToOverlays() {
|
|
1118
|
+
initialCameraKey = key
|
|
1119
|
+
pendingFitKey = nil
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
} else {
|
|
1123
|
+
initialCameraKey = nil
|
|
1124
|
+
pendingFitKey = nil
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
override func layoutSubviews() {
|
|
1129
|
+
super.layoutSubviews()
|
|
1130
|
+
guard let key = pendingFitKey else { return }
|
|
1131
|
+
pendingFitKey = nil
|
|
1132
|
+
guard fitToOverlays() else {
|
|
1133
|
+
pendingFitKey = key
|
|
1134
|
+
return
|
|
1063
1135
|
}
|
|
1136
|
+
initialCameraKey = key
|
|
1064
1137
|
}
|
|
1065
1138
|
|
|
1066
|
-
|
|
1139
|
+
@discardableResult
|
|
1140
|
+
private func fitToOverlays() -> Bool {
|
|
1141
|
+
guard bounds.width > 0, bounds.height > 0 else { return false }
|
|
1067
1142
|
var rect = MKMapRect.null
|
|
1068
1143
|
for overlay in overlaysByID.values { rect = rect.union(overlay.boundingMapRect) }
|
|
1069
1144
|
for annotation in annotationsByID.values {
|
|
1070
1145
|
let point = MKMapPoint(annotation.coordinate)
|
|
1071
1146
|
rect = rect.union(MKMapRect(x: point.x, y: point.y, width: 0, height: 0))
|
|
1072
1147
|
}
|
|
1073
|
-
guard !rect.isNull else { return }
|
|
1148
|
+
guard !rect.isNull else { return false }
|
|
1074
1149
|
let inset = UIEdgeInsets(top: 24, left: 24, bottom: 24, right: 24)
|
|
1075
1150
|
setVisibleMapRect(rect, edgePadding: inset, animated: false)
|
|
1151
|
+
return true
|
|
1076
1152
|
}
|
|
1077
1153
|
|
|
1078
1154
|
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
|
|
@@ -1118,6 +1194,25 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
1118
1194
|
}
|
|
1119
1195
|
}
|
|
1120
1196
|
|
|
1197
|
+
private static func regionKey(_ region: MKCoordinateRegion) -> String {
|
|
1198
|
+
"\(region.center.latitude),\(region.center.longitude),\(region.span.latitudeDelta),\(region.span.longitudeDelta)"
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
private static func overlayKey(
|
|
1202
|
+
markers: [String: CLLocationCoordinate2D],
|
|
1203
|
+
polylines: [String: [CLLocationCoordinate2D]]
|
|
1204
|
+
) -> String {
|
|
1205
|
+
let markerKey = markers.keys.sorted().compactMap { id in
|
|
1206
|
+
guard let point = markers[id] else { return nil }
|
|
1207
|
+
return "\(id):\(point.latitude),\(point.longitude)"
|
|
1208
|
+
}.joined(separator: "|")
|
|
1209
|
+
let polylineKey = polylines.keys.sorted().compactMap { id in
|
|
1210
|
+
guard let points = polylines[id] else { return nil }
|
|
1211
|
+
return "\(id):" + points.map { "\($0.latitude),\($0.longitude)" }.joined(separator: ";")
|
|
1212
|
+
}.joined(separator: "|")
|
|
1213
|
+
return "m[\(markerKey)]p[\(polylineKey)]"
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1121
1216
|
private static func region(_ value: Any?) -> MKCoordinateRegion? {
|
|
1122
1217
|
guard let value = value as? [String: Any],
|
|
1123
1218
|
let center = value["center"] as? [String: Any],
|
|
@@ -1140,7 +1235,9 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
1140
1235
|
case "orange": return .systemOrange
|
|
1141
1236
|
case "purple": return .systemPurple
|
|
1142
1237
|
case "yellow": return .systemYellow
|
|
1143
|
-
default:
|
|
1238
|
+
default:
|
|
1239
|
+
guard let value else { return .systemBlue }
|
|
1240
|
+
return UIColor(css: value) ?? UIColor(hex: value) ?? .systemBlue
|
|
1144
1241
|
}
|
|
1145
1242
|
}
|
|
1146
1243
|
|
|
@@ -1371,13 +1468,32 @@ extension UIView {
|
|
|
1371
1468
|
let right = sides[1].frame.width
|
|
1372
1469
|
let bottom = sides[2].frame.height
|
|
1373
1470
|
let left = sides[3].frame.width
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1471
|
+
let frames = [
|
|
1472
|
+
CGRect(x: 0, y: 0, width: bounds.width, height: top),
|
|
1473
|
+
CGRect(x: max(0, bounds.width - right), y: 0, width: right, height: bounds.height),
|
|
1474
|
+
CGRect(x: 0, y: max(0, bounds.height - bottom), width: bounds.width, height: bottom),
|
|
1475
|
+
CGRect(x: 0, y: 0, width: left, height: bounds.height),
|
|
1476
|
+
]
|
|
1477
|
+
for (index, side) in sides.enumerated() {
|
|
1478
|
+
side.frame = frames[index]
|
|
1479
|
+
if let shape = side as? CAShapeLayer {
|
|
1480
|
+
shape.path = craftSideBorderPath(shape.bounds, index: index)
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1378
1483
|
}
|
|
1379
1484
|
}
|
|
1380
1485
|
|
|
1486
|
+
private func craftSideBorderPath(_ bounds: CGRect, index: Int) -> CGPath {
|
|
1487
|
+
let path = UIBezierPath()
|
|
1488
|
+
switch index {
|
|
1489
|
+
case 0: path.move(to: CGPoint(x: bounds.minX, y: bounds.midY)); path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.midY))
|
|
1490
|
+
case 1: path.move(to: CGPoint(x: bounds.midX, y: bounds.minY)); path.addLine(to: CGPoint(x: bounds.midX, y: bounds.maxY))
|
|
1491
|
+
case 2: path.move(to: CGPoint(x: bounds.minX, y: bounds.midY)); path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.midY))
|
|
1492
|
+
default: path.move(to: CGPoint(x: bounds.midX, y: bounds.minY)); path.addLine(to: CGPoint(x: bounds.midX, y: bounds.maxY))
|
|
1493
|
+
}
|
|
1494
|
+
return path.cgPath
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1381
1497
|
fileprivate func craftApplyBorderStyle(_ style: String?, width: CGFloat, color: UIColor) {
|
|
1382
1498
|
guard width > 0, style == "dashed" || style == "dotted" else {
|
|
1383
1499
|
(objc_getAssociatedObject(self, &craftNativeBorderLayerKey) as? CAShapeLayer)?.removeFromSuperlayer()
|
|
@@ -1410,6 +1526,7 @@ extension UIView {
|
|
|
1410
1526
|
(objc_getAssociatedObject(self, &craftNativeBorderLayerKey) as? CAShapeLayer)?.removeFromSuperlayer()
|
|
1411
1527
|
objc_setAssociatedObject(self, &craftNativeBorderLayerKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
1412
1528
|
layer.borderWidth = 0
|
|
1529
|
+
(objc_getAssociatedObject(self, &craftNativeSideBorderLayersKey) as? [CALayer])?.forEach { $0.removeFromSuperlayer() }
|
|
1413
1530
|
let top = max(0, widths.count > 0 ? widths[0] : 0)
|
|
1414
1531
|
let right = max(0, widths.count > 1 ? widths[1] : 0)
|
|
1415
1532
|
let bottom = max(0, widths.count > 2 ? widths[2] : 0)
|
|
@@ -1421,9 +1538,25 @@ extension UIView {
|
|
|
1421
1538
|
CGRect(x: 0, y: 0, width: left, height: bounds.height),
|
|
1422
1539
|
]
|
|
1423
1540
|
let layers = frames.enumerated().map { index, frame in
|
|
1424
|
-
let side
|
|
1541
|
+
let side: CALayer
|
|
1542
|
+
if style == "dashed" || style == "dotted" {
|
|
1543
|
+
let shape = CAShapeLayer()
|
|
1544
|
+
shape.strokeColor = (colors.count > index ? colors[index] : .clear).cgColor
|
|
1545
|
+
shape.fillColor = nil
|
|
1546
|
+
shape.lineWidth = index % 2 == 0 ? frame.height : frame.width
|
|
1547
|
+
shape.lineCap = style == "dotted" ? .round : .butt
|
|
1548
|
+
shape.lineDashPattern = style == "dotted"
|
|
1549
|
+
? [NSNumber(value: Double(shape.lineWidth)), NSNumber(value: Double(shape.lineWidth * 1.5))]
|
|
1550
|
+
: [NSNumber(value: Double(shape.lineWidth * 3)), NSNumber(value: Double(shape.lineWidth * 2))]
|
|
1551
|
+
shape.contentsScale = window?.screen.scale ?? traitCollection.displayScale
|
|
1552
|
+
shape.frame = frame
|
|
1553
|
+
shape.path = craftSideBorderPath(shape.bounds, index: index)
|
|
1554
|
+
side = shape
|
|
1555
|
+
} else {
|
|
1556
|
+
side = CALayer()
|
|
1557
|
+
side.backgroundColor = (colors.count > index ? colors[index] : .clear).cgColor
|
|
1558
|
+
}
|
|
1425
1559
|
side.frame = frame
|
|
1426
|
-
side.backgroundColor = (colors.count > index ? colors[index] : .clear).cgColor
|
|
1427
1560
|
layer.addSublayer(side)
|
|
1428
1561
|
return side
|
|
1429
1562
|
}
|
|
@@ -1523,6 +1656,8 @@ extension UIView {
|
|
|
1523
1656
|
private class CraftNativeFlowView: UIStackView {
|
|
1524
1657
|
var pressActiveOpacity: CGFloat?
|
|
1525
1658
|
var pressBaseOpacity: CGFloat = 1
|
|
1659
|
+
var pressUnderlayColor: UIColor?
|
|
1660
|
+
var pressBaseBackgroundColor: UIColor?
|
|
1526
1661
|
var wrap = false { didSet { if wrap != oldValue { invalidateMeasurements() } } }
|
|
1527
1662
|
var alignItems = "stretch" { didSet { alignment = alignmentValue; if alignItems != oldValue { invalidateMeasurements() } } }
|
|
1528
1663
|
var alignContent = "flex-start" { didSet { if alignContent != oldValue { invalidateMeasurements() } } }
|
|
@@ -3174,10 +3309,17 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3174
3309
|
var strokeWidth: CGFloat = 1
|
|
3175
3310
|
var color: String? = "#000000"
|
|
3176
3311
|
var opacity: CGFloat = 1
|
|
3312
|
+
var fillOpacity: CGFloat = 1
|
|
3313
|
+
var strokeOpacity: CGFloat = 1
|
|
3314
|
+
var strokeDashoffset: CGFloat = 0
|
|
3315
|
+
var strokeMiterlimit: CGFloat = 4
|
|
3316
|
+
var fillRule: String = "nonzero"
|
|
3177
3317
|
var linecap: String?
|
|
3178
3318
|
var linejoin: String?
|
|
3179
3319
|
var dashes: [CGFloat]?
|
|
3180
3320
|
var vectorEffect: String?
|
|
3321
|
+
var fontWeight: String?
|
|
3322
|
+
var dominantBaseline: String?
|
|
3181
3323
|
|
|
3182
3324
|
func merged(_ node: [String: Any]) -> Paint {
|
|
3183
3325
|
var next = self
|
|
@@ -3189,6 +3331,11 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3189
3331
|
let value = CGFloat(truncating: number)
|
|
3190
3332
|
return value.isFinite ? value : nil
|
|
3191
3333
|
}
|
|
3334
|
+
func string(_ raw: Any?) -> String? {
|
|
3335
|
+
if let value = raw as? String { return value }
|
|
3336
|
+
if let number = raw as? NSNumber { return number.stringValue }
|
|
3337
|
+
return nil
|
|
3338
|
+
}
|
|
3192
3339
|
if let color = style["color"] as? String { next.color = color }
|
|
3193
3340
|
if let fill = style["fill"] as? String ?? props["fill"] as? String { next.fill = fill }
|
|
3194
3341
|
if let stroke = style["stroke"] as? String ?? props["stroke"] as? String { next.stroke = stroke }
|
|
@@ -3196,10 +3343,17 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3196
3343
|
// Computed style wins over the component prop, matching Android
|
|
3197
3344
|
// and the browser's cascade when both provide opacity.
|
|
3198
3345
|
if let opacity = finite(style["opacity"] ?? props["opacity"]) { next.opacity *= opacity }
|
|
3346
|
+
if let opacity = finite(value("fillOpacity")) { next.fillOpacity = opacity }
|
|
3347
|
+
if let opacity = finite(value("strokeOpacity")) { next.strokeOpacity = opacity }
|
|
3348
|
+
if let offset = finite(value("strokeDashoffset")) { next.strokeDashoffset = offset }
|
|
3349
|
+
if let limit = finite(value("strokeMiterlimit")), limit > 0 { next.strokeMiterlimit = limit }
|
|
3350
|
+
if let rule = value("fillRule") as? String { next.fillRule = rule }
|
|
3199
3351
|
if let cap = value("strokeLinecap") as? String { next.linecap = cap }
|
|
3200
3352
|
if let join = value("strokeLinejoin") as? String { next.linejoin = join }
|
|
3201
3353
|
if let dashes = value("strokeDasharray") as? [Any] { next.dashes = dashes.compactMap(finite).filter { $0 >= 0 } }
|
|
3202
3354
|
if let vectorEffect = value("vectorEffect") as? String { next.vectorEffect = vectorEffect }
|
|
3355
|
+
if let fontWeight = string(value("fontWeight")) { next.fontWeight = fontWeight }
|
|
3356
|
+
if let baseline = value("dominantBaseline") as? String { next.dominantBaseline = baseline }
|
|
3203
3357
|
return next
|
|
3204
3358
|
}
|
|
3205
3359
|
|
|
@@ -3257,10 +3411,13 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3257
3411
|
default:
|
|
3258
3412
|
guard let path = CraftNativeSvgView.path((shape["props"] as? [String: Any])?["geometry"]) else { continue }
|
|
3259
3413
|
context.saveGState()
|
|
3260
|
-
|
|
3414
|
+
path.usesEvenOddFillRule = own.fillRule == "evenodd"
|
|
3261
3415
|
if let fill = own.resolve(own.fill) {
|
|
3416
|
+
context.saveGState()
|
|
3417
|
+
context.setAlpha(max(0, min(1, own.opacity * own.fillOpacity)))
|
|
3262
3418
|
fill.setFill()
|
|
3263
3419
|
path.fill()
|
|
3420
|
+
context.restoreGState()
|
|
3264
3421
|
}
|
|
3265
3422
|
if let stroke = own.resolve(own.stroke), own.strokeWidth > 0 {
|
|
3266
3423
|
// `vector-effect="non-scaling-stroke"`: the stroke is in the
|
|
@@ -3277,10 +3434,12 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3277
3434
|
line = path
|
|
3278
3435
|
}
|
|
3279
3436
|
stroke.setStroke()
|
|
3437
|
+
context.setAlpha(max(0, min(1, own.opacity * own.strokeOpacity)))
|
|
3280
3438
|
line.lineWidth = own.strokeWidth
|
|
3439
|
+
line.miterLimit = own.strokeMiterlimit
|
|
3281
3440
|
line.lineCapStyle = own.linecap == "round" ? .round : own.linecap == "square" ? .square : .butt
|
|
3282
3441
|
line.lineJoinStyle = own.linejoin == "round" ? .round : own.linejoin == "bevel" ? .bevel : .miter
|
|
3283
|
-
if let dashes = own.dashes, !dashes.isEmpty { line.setLineDash(dashes, count: dashes.count, phase:
|
|
3442
|
+
if let dashes = own.dashes, !dashes.isEmpty { line.setLineDash(dashes, count: dashes.count, phase: own.strokeDashoffset) }
|
|
3284
3443
|
line.stroke()
|
|
3285
3444
|
}
|
|
3286
3445
|
context.restoreGState()
|
|
@@ -3294,10 +3453,21 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3294
3453
|
let props = node["props"] as? [String: Any] ?? [:]
|
|
3295
3454
|
let style = node["style"] as? [String: Any] ?? [:]
|
|
3296
3455
|
let size = number(props["fontSize"]) ?? number(style["fontSize"]) ?? 12
|
|
3297
|
-
let weight: UIFont.Weight =
|
|
3456
|
+
let weight: UIFont.Weight = {
|
|
3457
|
+
switch paint.fontWeight?.lowercased() {
|
|
3458
|
+
case "500": return .medium
|
|
3459
|
+
case "600": return .semibold
|
|
3460
|
+
case "bold", "700", "800", "900": return .bold
|
|
3461
|
+
default: return .regular
|
|
3462
|
+
}
|
|
3463
|
+
}()
|
|
3464
|
+
let font = UIFont.systemFont(ofSize: size, weight: weight)
|
|
3465
|
+
let resolvedTextColor = paint.resolve(paint.fill) ?? UIColor.label
|
|
3466
|
+
let textAlpha = max(0, min(1, paint.opacity * paint.fillOpacity))
|
|
3467
|
+
let textColor = resolvedTextColor.withAlphaComponent(resolvedTextColor.cgColor.alpha * textAlpha)
|
|
3298
3468
|
let string = NSAttributedString(string: text, attributes: [
|
|
3299
|
-
.font:
|
|
3300
|
-
.foregroundColor:
|
|
3469
|
+
.font: font,
|
|
3470
|
+
.foregroundColor: textColor,
|
|
3301
3471
|
])
|
|
3302
3472
|
var x = number(props["x"]) ?? 0
|
|
3303
3473
|
let y = number(props["y"]) ?? 0
|
|
@@ -3306,8 +3476,14 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3306
3476
|
case "end": x -= string.size().width
|
|
3307
3477
|
default: break
|
|
3308
3478
|
}
|
|
3309
|
-
|
|
3310
|
-
|
|
3479
|
+
let baseline: CGFloat
|
|
3480
|
+
switch paint.dominantBaseline {
|
|
3481
|
+
case "middle", "central", "mathematical": baseline = y - (font.ascender + font.descender) / 2
|
|
3482
|
+
case "hanging", "text-before-edge", "before-edge": baseline = y + font.ascender
|
|
3483
|
+
case "text-after-edge", "after-edge": baseline = y + font.descender
|
|
3484
|
+
default: baseline = y
|
|
3485
|
+
}
|
|
3486
|
+
string.draw(at: CGPoint(x: x, y: baseline - font.ascender))
|
|
3311
3487
|
}
|
|
3312
3488
|
|
|
3313
3489
|
static func path(_ value: Any?) -> UIBezierPath? {
|
|
@@ -3505,6 +3681,8 @@ private final class CraftNativeIconView: UIImageView {
|
|
|
3505
3681
|
|
|
3506
3682
|
private final class CraftNativeTextView: UITextView {
|
|
3507
3683
|
private let placeholderLabel = UILabel()
|
|
3684
|
+
private var nativeTextContainerInset: UIEdgeInsets?
|
|
3685
|
+
private var nativeLineFragmentPadding: CGFloat?
|
|
3508
3686
|
var placeholder: String? {
|
|
3509
3687
|
didSet { refreshPlaceholder() }
|
|
3510
3688
|
}
|
|
@@ -3529,6 +3707,20 @@ private final class CraftNativeTextView: UITextView {
|
|
|
3529
3707
|
configurePlaceholder()
|
|
3530
3708
|
}
|
|
3531
3709
|
|
|
3710
|
+
func applyAuthoredPadding(_ padding: UIEdgeInsets) {
|
|
3711
|
+
if nativeTextContainerInset == nil {
|
|
3712
|
+
nativeTextContainerInset = textContainerInset
|
|
3713
|
+
nativeLineFragmentPadding = textContainer.lineFragmentPadding
|
|
3714
|
+
}
|
|
3715
|
+
if padding == .zero {
|
|
3716
|
+
textContainerInset = nativeTextContainerInset ?? .zero
|
|
3717
|
+
textContainer.lineFragmentPadding = nativeLineFragmentPadding ?? 0
|
|
3718
|
+
} else {
|
|
3719
|
+
textContainerInset = padding
|
|
3720
|
+
textContainer.lineFragmentPadding = 0
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3532
3724
|
override func layoutSubviews() {
|
|
3533
3725
|
super.layoutSubviews()
|
|
3534
3726
|
let inset = textContainerInset
|
|
@@ -3582,6 +3774,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3582
3774
|
private var handlers: [ObjectIdentifier: String] = [:]
|
|
3583
3775
|
private var pressInHandlers: [ObjectIdentifier: String] = [:]
|
|
3584
3776
|
private var pressOutHandlers: [ObjectIdentifier: String] = [:]
|
|
3777
|
+
private var underlayShowHandlers: [ObjectIdentifier: String] = [:]
|
|
3778
|
+
private var underlayHideHandlers: [ObjectIdentifier: String] = [:]
|
|
3585
3779
|
private var focusHandlers: [ObjectIdentifier: String] = [:]
|
|
3586
3780
|
private var blurHandlers: [ObjectIdentifier: String] = [:]
|
|
3587
3781
|
private var endEditingHandlers: [ObjectIdentifier: String] = [:]
|
|
@@ -3607,6 +3801,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3607
3801
|
private var imageSources: [ObjectIdentifier: String] = [:]
|
|
3608
3802
|
private var imageTasks: [ObjectIdentifier: URLSessionDataTask] = [:]
|
|
3609
3803
|
private var imageErrors: [ObjectIdentifier: String] = [:]
|
|
3804
|
+
/// Text inputs retain UIKit's generated accessibility value unless the
|
|
3805
|
+
/// bundle authored one; remember that distinction so removing the prop
|
|
3806
|
+
/// restores the native value instead of leaving stale metadata behind.
|
|
3807
|
+
private var authoredAccessibilityValues = Set<ObjectIdentifier>()
|
|
3610
3808
|
private var checkboxIndeterminate: [ObjectIdentifier: Bool] = [:]
|
|
3611
3809
|
private var tintedImages = Set<ObjectIdentifier>()
|
|
3612
3810
|
private var imageLoadStartHandlers: [ObjectIdentifier: String] = [:]
|
|
@@ -5138,6 +5336,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5138
5336
|
// reads the node's new layout style on its next pass.
|
|
5139
5337
|
CraftNativeFlexLayout.invalidateAncestors(of: next.view)
|
|
5140
5338
|
}
|
|
5339
|
+
if !applied.isEmpty {
|
|
5340
|
+
// A targeted mutation can change geometry without causing UIKit
|
|
5341
|
+
// to call viewDidLayoutSubviews again. Keep onLayout delivery
|
|
5342
|
+
// aligned with Android's post-mutation layout pass.
|
|
5343
|
+
scheduleLayoutEvents()
|
|
5344
|
+
}
|
|
5141
5345
|
restoreFocus(focused)
|
|
5142
5346
|
return true
|
|
5143
5347
|
}
|
|
@@ -5406,17 +5610,23 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5406
5610
|
}
|
|
5407
5611
|
case "Picker":
|
|
5408
5612
|
let picker = result as! UIPickerView
|
|
5409
|
-
let
|
|
5410
|
-
guard let
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5613
|
+
let itemOptions = (props["items"] as? [[String: Any]])?.compactMap { item -> (value: String, label: String)? in
|
|
5614
|
+
guard let label = item["label"] as? String, !label.isEmpty else { return nil }
|
|
5615
|
+
return (value: craftNativePickerValue(item["value"]) ?? label, label: label)
|
|
5616
|
+
} ?? []
|
|
5617
|
+
let options = itemOptions.isEmpty
|
|
5618
|
+
? children.compactMap { child -> (value: String, label: String)? in
|
|
5619
|
+
guard let node = child as? [String: Any], node["type"] as? String == "Text" else { return nil }
|
|
5620
|
+
let optionProps = node["props"] as? [String: Any] ?? [:]
|
|
5621
|
+
let label = (node["children"] as? [Any] ?? []).compactMap { $0 as? String }.joined()
|
|
5622
|
+
guard !label.isEmpty else { return nil }
|
|
5623
|
+
return (value: craftNativePickerValue(optionProps["value"]) ?? label, label: label)
|
|
5624
|
+
}
|
|
5625
|
+
: itemOptions
|
|
5416
5626
|
let pickerID = ObjectIdentifier(picker)
|
|
5417
5627
|
pickerOptions[pickerID] = options
|
|
5418
5628
|
picker.reloadAllComponents()
|
|
5419
|
-
if let selected = (props["selectedValue"]
|
|
5629
|
+
if let selected = craftNativePickerValue(props["selectedValue"]) ?? craftNativePickerValue(props["value"]),
|
|
5420
5630
|
let row = options.firstIndex(where: { $0.value == selected }) {
|
|
5421
5631
|
picker.selectRow(row, inComponent: 0, animated: false)
|
|
5422
5632
|
} else if !options.isEmpty {
|
|
@@ -5492,9 +5702,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5492
5702
|
let pressable = result as! CraftNativeFlowView
|
|
5493
5703
|
configureStack(pressable, style: style)
|
|
5494
5704
|
reconcileChildren(children, in: pressable, parent: current, path: path, style: style)
|
|
5495
|
-
|
|
5705
|
+
let disabled = props["disabled"] as? Bool == true
|
|
5706
|
+
pressable.pressActiveOpacity = disabled
|
|
5496
5707
|
? nil : number(props["activeOpacity"]) ?? 0.5
|
|
5497
5708
|
pressable.pressBaseOpacity = number(style["opacity"]) ?? 1
|
|
5709
|
+
pressable.pressUnderlayColor = type == "TouchableHighlight" && !disabled ? color(props["underlayColor"]) : nil
|
|
5710
|
+
pressable.pressBaseBackgroundColor = pressable.backgroundColor
|
|
5498
5711
|
case "Switch":
|
|
5499
5712
|
let toggle = result as! UISwitch
|
|
5500
5713
|
toggle.isOn = props["value"] as? Bool ?? props["checked"] as? Bool ?? false
|
|
@@ -5580,6 +5793,15 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5580
5793
|
updateImage(image, source: props["source"], defaultSource: props["defaultSource"])
|
|
5581
5794
|
case "ImageBackground":
|
|
5582
5795
|
let background = result as! CraftNativeImageBackgroundView
|
|
5796
|
+
let backgroundID = ObjectIdentifier(background)
|
|
5797
|
+
if let tint = color(style["tintColor"]) {
|
|
5798
|
+
tintedImages.insert(backgroundID)
|
|
5799
|
+
background.tintColor = tint
|
|
5800
|
+
} else {
|
|
5801
|
+
tintedImages.remove(backgroundID)
|
|
5802
|
+
background.tintColor = nil
|
|
5803
|
+
}
|
|
5804
|
+
if let current = background.image { background.image = imageForDisplay(current, view: background) }
|
|
5583
5805
|
let requestedResizeMode = style["resizeMode"] as? String ?? props["resizeMode"] as? String
|
|
5584
5806
|
background.contentMode = imageContentMode(requestedResizeMode)
|
|
5585
5807
|
background.clipsToBounds = background.contentMode == .scaleAspectFill
|
|
@@ -5614,8 +5836,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5614
5836
|
)
|
|
5615
5837
|
case "Map":
|
|
5616
5838
|
let map = result as! CraftNativeMapView
|
|
5839
|
+
let mapMarkerHandler = nonEmptyHandler(events["onMarkerPress"])
|
|
5617
5840
|
map.configure(props: props, children: children) { [weak self, weak map] markerID in
|
|
5618
|
-
guard let self, let map,
|
|
5841
|
+
guard let self, let map,
|
|
5842
|
+
let handler = map.markerHandlers[markerID] ?? mapMarkerHandler else { return }
|
|
5619
5843
|
self.send(type: "EVENT", payload: [
|
|
5620
5844
|
"handlerName": handler,
|
|
5621
5845
|
"nativeEvent": ["id": markerID],
|
|
@@ -5649,13 +5873,15 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5649
5873
|
let refreshNode = children.compactMap { $0 as? [String: Any] }.first {
|
|
5650
5874
|
canonicalType($0["type"] as? String ?? "") == "RefreshControl"
|
|
5651
5875
|
}
|
|
5652
|
-
let
|
|
5876
|
+
let propRefresh = props["refreshControl"] as? [String: Any] ?? [:]
|
|
5877
|
+
let refreshProps = refreshNode?["props"] as? [String: Any] ?? propRefresh
|
|
5653
5878
|
let refreshEvents = refreshNode?["events"] as? [String: String] ?? [:]
|
|
5654
5879
|
let refreshHandlerName = nonEmptyHandler(refreshEvents["onRefresh"])
|
|
5655
5880
|
?? nonEmptyHandler(events["onRefresh"])
|
|
5881
|
+
let refreshing = (refreshProps["refreshing"] ?? props["refreshing"]) as? Bool == true
|
|
5656
5882
|
scroll.setRefreshHandler(refreshHandlerName.map { [weak self] handler in
|
|
5657
5883
|
{ self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
5658
|
-
}, refreshing:
|
|
5884
|
+
}, refreshing: refreshing)
|
|
5659
5885
|
scroll.setRefreshAppearance(refreshProps)
|
|
5660
5886
|
var contentStyle = style
|
|
5661
5887
|
if let override = props["contentContainerStyle"] as? [String: Any] {
|
|
@@ -5690,6 +5916,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5690
5916
|
disabled ? nil : nonEmptyHandler(events["onPressOut"]),
|
|
5691
5917
|
for: result
|
|
5692
5918
|
)
|
|
5919
|
+
updateUnderlayHandlers(
|
|
5920
|
+
type == "TouchableHighlight" && !disabled ? nonEmptyHandler(events["onShowUnderlay"]) : nil,
|
|
5921
|
+
type == "TouchableHighlight" && !disabled ? nonEmptyHandler(events["onHideUnderlay"]) : nil,
|
|
5922
|
+
for: result
|
|
5923
|
+
)
|
|
5693
5924
|
if disabled { result.isUserInteractionEnabled = false }
|
|
5694
5925
|
}
|
|
5695
5926
|
applyAccessibility(props, type: type, to: result)
|
|
@@ -5931,6 +6162,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5931
6162
|
let owner = parent.protocolId ?? path
|
|
5932
6163
|
flatListOwners = flatListOwners.filter { $0.value != owner }
|
|
5933
6164
|
for child in children { registerFlatListOwnership(child, owner: owner) }
|
|
6165
|
+
let propRefresh = props["refreshControl"] as? [String: Any] ?? [:]
|
|
6166
|
+
let propRefreshEvents = propRefresh["events"] as? [String: String] ?? [:]
|
|
6167
|
+
let refreshHandler = nonEmptyHandler(propRefreshEvents["onRefresh"])
|
|
6168
|
+
?? nonEmptyHandler(events["onRefresh"])
|
|
6169
|
+
let refreshing = (propRefresh["refreshing"] ?? props["refreshing"]) as? Bool == true
|
|
5934
6170
|
list.bounces = props["bounces"] as? Bool ?? true
|
|
5935
6171
|
list.isScrollEnabled = props["scrollEnabled"] as? Bool != false
|
|
5936
6172
|
list.onFittingHeightChanged = { [weak list] in
|
|
@@ -5939,9 +6175,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5939
6175
|
}
|
|
5940
6176
|
list.setKeyboardShouldPersistTaps(props["keyboardShouldPersistTaps"])
|
|
5941
6177
|
list.setContentContainerStyle(props["contentContainerStyle"] as? [String: Any])
|
|
5942
|
-
list.setRefreshHandler(
|
|
6178
|
+
list.setRefreshHandler(refreshHandler.map { [weak self] handler in
|
|
5943
6179
|
{ self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
5944
|
-
}, refreshing:
|
|
6180
|
+
}, refreshing: refreshing)
|
|
6181
|
+
list.setRefreshAppearance(propRefresh)
|
|
5945
6182
|
list.keyboardDismissMode = keyboardDismissMode(props["keyboardDismissMode"])
|
|
5946
6183
|
list.onScrollEvent = nonEmptyHandler(events["onScroll"]).map { handler in
|
|
5947
6184
|
{ [weak self] scrollView in self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": self?.scrollEvent(scrollView) ?? [:]]) }
|
|
@@ -5993,6 +6230,13 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5993
6230
|
{ self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
5994
6231
|
}
|
|
5995
6232
|
)
|
|
6233
|
+
let horizontal = props["horizontal"] as? Bool == true
|
|
6234
|
+
list.showsVerticalScrollIndicator = props["showsVerticalScrollIndicator"] as? Bool ?? true
|
|
6235
|
+
list.showsHorizontalScrollIndicator = props["showsHorizontalScrollIndicator"] as? Bool ?? true
|
|
6236
|
+
list.isPagingEnabled = props["pagingEnabled"] as? Bool ?? false
|
|
6237
|
+
list.setScrollTarget(props["scrollTarget"])
|
|
6238
|
+
list.alwaysBounceVertical = props["alwaysBounceVertical"] as? Bool ?? !horizontal
|
|
6239
|
+
list.alwaysBounceHorizontal = props["alwaysBounceHorizontal"] as? Bool ?? horizontal
|
|
5996
6240
|
parent.children = []
|
|
5997
6241
|
}
|
|
5998
6242
|
|
|
@@ -6045,6 +6289,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6045
6289
|
transitionIDs.removeValue(forKey: id)
|
|
6046
6290
|
pressInHandlers.removeValue(forKey: id)
|
|
6047
6291
|
pressOutHandlers.removeValue(forKey: id)
|
|
6292
|
+
underlayShowHandlers.removeValue(forKey: id)
|
|
6293
|
+
underlayHideHandlers.removeValue(forKey: id)
|
|
6048
6294
|
layoutHandlers.removeValue(forKey: id)
|
|
6049
6295
|
lastLayoutFrames.removeValue(forKey: id)
|
|
6050
6296
|
focusHandlers.removeValue(forKey: id)
|
|
@@ -6076,6 +6322,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6076
6322
|
imageSources.removeValue(forKey: id)
|
|
6077
6323
|
imageTasks.removeValue(forKey: id)?.cancel()
|
|
6078
6324
|
imageErrors.removeValue(forKey: id)
|
|
6325
|
+
authoredAccessibilityValues.remove(id)
|
|
6079
6326
|
tintedImages.remove(id)
|
|
6080
6327
|
imageLoadStartHandlers.removeValue(forKey: id)
|
|
6081
6328
|
imageLoadHandlers.removeValue(forKey: id)
|
|
@@ -6117,6 +6364,19 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6117
6364
|
|
|
6118
6365
|
private var layoutEventsScheduled = false
|
|
6119
6366
|
|
|
6367
|
+
/// UIView.frame is undefined while a transform is applied. onLayout is
|
|
6368
|
+
/// the authored layout in the parent coordinate space, matching Android's
|
|
6369
|
+
/// left/top/right/bottom values rather than the transformed visual bounds.
|
|
6370
|
+
private func layoutFrame(of view: UIView) -> CGRect {
|
|
6371
|
+
guard !view.transform.isIdentity else { return view.frame }
|
|
6372
|
+
return CGRect(
|
|
6373
|
+
x: view.center.x - view.bounds.width / 2,
|
|
6374
|
+
y: view.center.y - view.bounds.height / 2,
|
|
6375
|
+
width: view.bounds.width,
|
|
6376
|
+
height: view.bounds.height
|
|
6377
|
+
)
|
|
6378
|
+
}
|
|
6379
|
+
|
|
6120
6380
|
/// A view inside the screen moved: report onLayout once the pass that
|
|
6121
6381
|
/// moved it is over, however deep it was.
|
|
6122
6382
|
fileprivate func scheduleLayoutEvents() {
|
|
@@ -6132,7 +6392,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6132
6392
|
private func emitLayoutEvents(for node: RenderedNode) {
|
|
6133
6393
|
let id = ObjectIdentifier(node.view)
|
|
6134
6394
|
if let handler = layoutHandlers[id], isEffectivelyVisible(node.view) {
|
|
6135
|
-
let frame = node.view
|
|
6395
|
+
let frame = layoutFrame(of: node.view)
|
|
6136
6396
|
if lastLayoutFrames[id] != frame {
|
|
6137
6397
|
lastLayoutFrames[id] = frame
|
|
6138
6398
|
send(type: "EVENT", payload: [
|
|
@@ -6205,8 +6465,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6205
6465
|
let id = ObjectIdentifier(view)
|
|
6206
6466
|
if let pressIn, !pressIn.isEmpty { pressInHandlers[id] = pressIn } else { pressInHandlers.removeValue(forKey: id) }
|
|
6207
6467
|
if let pressOut, !pressOut.isEmpty { pressOutHandlers[id] = pressOut } else { pressOutHandlers.removeValue(forKey: id) }
|
|
6208
|
-
|
|
6468
|
+
let hasUnderlay = (view as? CraftNativeFlowView)?.pressUnderlayColor != nil
|
|
6469
|
+
guard pressInHandlers[id] != nil || pressOutHandlers[id] != nil || hasUnderlay else {
|
|
6209
6470
|
if let recognizer = pressFeedbackRecognizers.removeValue(forKey: id) { view.removeGestureRecognizer(recognizer) }
|
|
6471
|
+
if let pressable = view as? CraftNativeFlowView { pressable.backgroundColor = pressable.pressBaseBackgroundColor }
|
|
6210
6472
|
if !ownsTouches(view) { view.isUserInteractionEnabled = handlers[id] != nil || linkTargets[id] != nil || longPressHandlers[id] != nil || view is CraftNativeFlowView }
|
|
6211
6473
|
return
|
|
6212
6474
|
}
|
|
@@ -6222,6 +6484,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6222
6484
|
if !ownsTouches(view) { view.isUserInteractionEnabled = true }
|
|
6223
6485
|
}
|
|
6224
6486
|
|
|
6487
|
+
private func updateUnderlayHandlers(_ show: String?, _ hide: String?, for view: UIView) {
|
|
6488
|
+
let id = ObjectIdentifier(view)
|
|
6489
|
+
if let show, !show.isEmpty { underlayShowHandlers[id] = show } else { underlayShowHandlers.removeValue(forKey: id) }
|
|
6490
|
+
if let hide, !hide.isEmpty { underlayHideHandlers[id] = hide } else { underlayHideHandlers.removeValue(forKey: id) }
|
|
6491
|
+
}
|
|
6492
|
+
|
|
6225
6493
|
private func updateLongPressHandler(_ handler: String?, for view: UIView) {
|
|
6226
6494
|
let id = ObjectIdentifier(view)
|
|
6227
6495
|
if let handler, !handler.isEmpty {
|
|
@@ -6289,7 +6557,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6289
6557
|
applyAutofill(to: textView, props: props)
|
|
6290
6558
|
// The style's padding is where the text starts, as in a web textarea.
|
|
6291
6559
|
let padding = paddingInsets(style)
|
|
6292
|
-
if
|
|
6560
|
+
if let craftTextView = textView as? CraftNativeTextView {
|
|
6561
|
+
craftTextView.applyAuthoredPadding(padding)
|
|
6562
|
+
} else if padding != .zero {
|
|
6293
6563
|
if textView.textContainerInset != padding { textView.textContainerInset = padding }
|
|
6294
6564
|
textView.textContainer.lineFragmentPadding = 0
|
|
6295
6565
|
}
|
|
@@ -6714,7 +6984,14 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6714
6984
|
}
|
|
6715
6985
|
|
|
6716
6986
|
private func updateImage(_ view: UIImageView, source: Any?, defaultSource: Any? = nil) {
|
|
6717
|
-
let
|
|
6987
|
+
let sourceObject = source as? [String: Any]
|
|
6988
|
+
let uri = (source as? String) ?? sourceObject?["uri"] as? String
|
|
6989
|
+
let headers = (sourceObject?["headers"] as? [String: Any])?.reduce(into: [String: String]()) { result, entry in
|
|
6990
|
+
if let value = entry.value as? String { result[entry.key] = value }
|
|
6991
|
+
} ?? [:]
|
|
6992
|
+
let requestKey = uri.map { value in
|
|
6993
|
+
value + "\u{0}" + headers.sorted { $0.key < $1.key }.map { "\($0.key)=\($0.value)" }.joined(separator: "\u{0}")
|
|
6994
|
+
}
|
|
6718
6995
|
let id = ObjectIdentifier(view)
|
|
6719
6996
|
guard let uri, !uri.isEmpty else {
|
|
6720
6997
|
if imageSources[id] == "" { return }
|
|
@@ -6723,9 +7000,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6723
7000
|
imageFailure(view, uri: nil, message: "Image source is missing")
|
|
6724
7001
|
return
|
|
6725
7002
|
}
|
|
6726
|
-
guard imageSources[id] !=
|
|
7003
|
+
guard let requestKey, imageSources[id] != requestKey else { return }
|
|
6727
7004
|
imageTasks.removeValue(forKey: id)?.cancel()
|
|
6728
|
-
imageSources[id] =
|
|
7005
|
+
imageSources[id] = requestKey
|
|
6729
7006
|
imageErrors.removeValue(forKey: id)
|
|
6730
7007
|
view.image = immediateImage(defaultSource).map { imageForDisplay($0, view: view) }
|
|
6731
7008
|
imageDidChange(view)
|
|
@@ -6746,10 +7023,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6746
7023
|
return
|
|
6747
7024
|
}
|
|
6748
7025
|
if let url = URL(string: uri), Self.loadsImage(from: url) {
|
|
6749
|
-
|
|
7026
|
+
var request = URLRequest(url: url)
|
|
7027
|
+
for (name, value) in headers { request.setValue(value, forHTTPHeaderField: name) }
|
|
7028
|
+
let task = URLSession.shared.dataTask(with: request) { [weak self, weak view] data, _, _ in
|
|
6750
7029
|
guard let self, let view else { return }
|
|
6751
7030
|
DispatchQueue.main.async {
|
|
6752
|
-
guard self.imageSources[ObjectIdentifier(view)] ==
|
|
7031
|
+
guard self.imageSources[ObjectIdentifier(view)] == requestKey else { return }
|
|
6753
7032
|
self.imageTasks.removeValue(forKey: ObjectIdentifier(view))
|
|
6754
7033
|
guard let data, let image = UIImage(data: data) else {
|
|
6755
7034
|
self.imageFailure(view, uri: uri, message: "Image download failed")
|
|
@@ -6823,11 +7102,17 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6823
7102
|
}
|
|
6824
7103
|
|
|
6825
7104
|
private func applyAccessibility(_ props: [String: Any], type: String, to view: UIView) {
|
|
7105
|
+
let id = ObjectIdentifier(view)
|
|
6826
7106
|
view.accessibilityLabel = (props["accessibilityLabel"] as? String)
|
|
6827
7107
|
?? ((type == "Image" || type == "ImageBackground") ? props["alt"] as? String : nil)
|
|
6828
7108
|
view.accessibilityHint = props["accessibilityHint"] as? String
|
|
6829
7109
|
if let value = props["accessibilityValue"] {
|
|
6830
7110
|
view.accessibilityValue = (value as? String) ?? (value as? NSNumber)?.stringValue
|
|
7111
|
+
authoredAccessibilityValues.insert(id)
|
|
7112
|
+
} else if authoredAccessibilityValues.remove(id) != nil {
|
|
7113
|
+
// Clearing an authored value lets UIKit expose the field's live
|
|
7114
|
+
// value again (particularly important for TextInput).
|
|
7115
|
+
view.accessibilityValue = nil
|
|
6831
7116
|
} else if type != "TextInput" {
|
|
6832
7117
|
view.accessibilityValue = nil
|
|
6833
7118
|
}
|
|
@@ -7216,13 +7501,21 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7216
7501
|
let id = ObjectIdentifier(view)
|
|
7217
7502
|
switch sender.state {
|
|
7218
7503
|
case .began:
|
|
7219
|
-
if let pressable = view as? CraftNativeFlowView
|
|
7220
|
-
|
|
7504
|
+
if let pressable = view as? CraftNativeFlowView {
|
|
7505
|
+
if let underlayColor = pressable.pressUnderlayColor { pressable.backgroundColor = underlayColor }
|
|
7506
|
+
if let activeOpacity = pressable.pressActiveOpacity {
|
|
7507
|
+
UIView.animate(withDuration: 0.1) { pressable.alpha = pressable.pressBaseOpacity * activeOpacity }
|
|
7508
|
+
}
|
|
7221
7509
|
}
|
|
7222
7510
|
if let handler = pressInHandlers[id] { send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
7511
|
+
if let handler = underlayShowHandlers[id] { send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
7223
7512
|
case .ended, .cancelled, .failed:
|
|
7224
|
-
if let pressable = view as? CraftNativeFlowView {
|
|
7513
|
+
if let pressable = view as? CraftNativeFlowView {
|
|
7514
|
+
pressable.backgroundColor = pressable.pressBaseBackgroundColor
|
|
7515
|
+
UIView.animate(withDuration: 0.1) { pressable.alpha = pressable.pressBaseOpacity }
|
|
7516
|
+
}
|
|
7225
7517
|
if let handler = pressOutHandlers[id] { send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
7518
|
+
if let handler = underlayHideHandlers[id] { send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
7226
7519
|
default:
|
|
7227
7520
|
break
|
|
7228
7521
|
}
|