craft-native 0.0.203 → 0.0.205
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 +1 -0
- package/dist/android/src/index.js +90 -0
- package/dist/android/templates/CraftAppLinks.kt.template +9 -2
- package/dist/android/templates/CraftNativeFlatList.kt.template +117 -0
- package/dist/android/templates/CraftNativeShell.kt.template +42 -10
- package/dist/android/templates/MainActivityNative.kt.template +527 -50
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +118 -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/testing.d.ts +4 -4
- package/dist/ios/templates/CraftHybrid.swift +17 -3
- package/dist/ios/templates/CraftNativeFlatList.swift +64 -0
- package/dist/ios/templates/CraftNativeScreen.swift +307 -43
- package/dist/ios/templates/testing/DriverTests.swift.template +32 -3
- 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
|
|
1063
1125
|
}
|
|
1064
1126
|
}
|
|
1065
1127
|
|
|
1066
|
-
|
|
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
|
|
1135
|
+
}
|
|
1136
|
+
initialCameraKey = key
|
|
1137
|
+
}
|
|
1138
|
+
|
|
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,16 @@ 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 dominantBaseline: String?
|
|
3181
3322
|
|
|
3182
3323
|
func merged(_ node: [String: Any]) -> Paint {
|
|
3183
3324
|
var next = self
|
|
@@ -3196,10 +3337,16 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3196
3337
|
// Computed style wins over the component prop, matching Android
|
|
3197
3338
|
// and the browser's cascade when both provide opacity.
|
|
3198
3339
|
if let opacity = finite(style["opacity"] ?? props["opacity"]) { next.opacity *= opacity }
|
|
3340
|
+
if let opacity = finite(value("fillOpacity")) { next.fillOpacity = opacity }
|
|
3341
|
+
if let opacity = finite(value("strokeOpacity")) { next.strokeOpacity = opacity }
|
|
3342
|
+
if let offset = finite(value("strokeDashoffset")) { next.strokeDashoffset = offset }
|
|
3343
|
+
if let limit = finite(value("strokeMiterlimit")), limit > 0 { next.strokeMiterlimit = limit }
|
|
3344
|
+
if let rule = value("fillRule") as? String { next.fillRule = rule }
|
|
3199
3345
|
if let cap = value("strokeLinecap") as? String { next.linecap = cap }
|
|
3200
3346
|
if let join = value("strokeLinejoin") as? String { next.linejoin = join }
|
|
3201
3347
|
if let dashes = value("strokeDasharray") as? [Any] { next.dashes = dashes.compactMap(finite).filter { $0 >= 0 } }
|
|
3202
3348
|
if let vectorEffect = value("vectorEffect") as? String { next.vectorEffect = vectorEffect }
|
|
3349
|
+
if let baseline = value("dominantBaseline") as? String { next.dominantBaseline = baseline }
|
|
3203
3350
|
return next
|
|
3204
3351
|
}
|
|
3205
3352
|
|
|
@@ -3257,10 +3404,13 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3257
3404
|
default:
|
|
3258
3405
|
guard let path = CraftNativeSvgView.path((shape["props"] as? [String: Any])?["geometry"]) else { continue }
|
|
3259
3406
|
context.saveGState()
|
|
3260
|
-
|
|
3407
|
+
path.usesEvenOddFillRule = own.fillRule == "evenodd"
|
|
3261
3408
|
if let fill = own.resolve(own.fill) {
|
|
3409
|
+
context.saveGState()
|
|
3410
|
+
context.setAlpha(max(0, min(1, own.opacity * own.fillOpacity)))
|
|
3262
3411
|
fill.setFill()
|
|
3263
3412
|
path.fill()
|
|
3413
|
+
context.restoreGState()
|
|
3264
3414
|
}
|
|
3265
3415
|
if let stroke = own.resolve(own.stroke), own.strokeWidth > 0 {
|
|
3266
3416
|
// `vector-effect="non-scaling-stroke"`: the stroke is in the
|
|
@@ -3277,10 +3427,12 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3277
3427
|
line = path
|
|
3278
3428
|
}
|
|
3279
3429
|
stroke.setStroke()
|
|
3430
|
+
context.setAlpha(max(0, min(1, own.opacity * own.strokeOpacity)))
|
|
3280
3431
|
line.lineWidth = own.strokeWidth
|
|
3432
|
+
line.miterLimit = own.strokeMiterlimit
|
|
3281
3433
|
line.lineCapStyle = own.linecap == "round" ? .round : own.linecap == "square" ? .square : .butt
|
|
3282
3434
|
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:
|
|
3435
|
+
if let dashes = own.dashes, !dashes.isEmpty { line.setLineDash(dashes, count: dashes.count, phase: own.strokeDashoffset) }
|
|
3284
3436
|
line.stroke()
|
|
3285
3437
|
}
|
|
3286
3438
|
context.restoreGState()
|
|
@@ -3295,9 +3447,13 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3295
3447
|
let style = node["style"] as? [String: Any] ?? [:]
|
|
3296
3448
|
let size = number(props["fontSize"]) ?? number(style["fontSize"]) ?? 12
|
|
3297
3449
|
let weight: UIFont.Weight = (props["fontWeight"] as? String ?? style["fontWeight"] as? String).map { ["500": .medium, "600": .semibold, "700": .bold, "bold": .bold][$0] ?? .regular } ?? .regular
|
|
3450
|
+
let font = UIFont.systemFont(ofSize: size, weight: weight)
|
|
3451
|
+
let resolvedTextColor = paint.resolve(paint.fill) ?? UIColor.label
|
|
3452
|
+
let textAlpha = max(0, min(1, paint.opacity * paint.fillOpacity))
|
|
3453
|
+
let textColor = resolvedTextColor.withAlphaComponent(resolvedTextColor.cgColor.alpha * textAlpha)
|
|
3298
3454
|
let string = NSAttributedString(string: text, attributes: [
|
|
3299
|
-
.font:
|
|
3300
|
-
.foregroundColor:
|
|
3455
|
+
.font: font,
|
|
3456
|
+
.foregroundColor: textColor,
|
|
3301
3457
|
])
|
|
3302
3458
|
var x = number(props["x"]) ?? 0
|
|
3303
3459
|
let y = number(props["y"]) ?? 0
|
|
@@ -3306,8 +3462,14 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3306
3462
|
case "end": x -= string.size().width
|
|
3307
3463
|
default: break
|
|
3308
3464
|
}
|
|
3309
|
-
|
|
3310
|
-
|
|
3465
|
+
let baseline: CGFloat
|
|
3466
|
+
switch paint.dominantBaseline {
|
|
3467
|
+
case "middle", "central", "mathematical": baseline = y - (font.ascender + font.descender) / 2
|
|
3468
|
+
case "hanging", "text-before-edge", "before-edge": baseline = y + font.ascender
|
|
3469
|
+
case "text-after-edge", "after-edge": baseline = y + font.descender
|
|
3470
|
+
default: baseline = y
|
|
3471
|
+
}
|
|
3472
|
+
string.draw(at: CGPoint(x: x, y: baseline - font.ascender))
|
|
3311
3473
|
}
|
|
3312
3474
|
|
|
3313
3475
|
static func path(_ value: Any?) -> UIBezierPath? {
|
|
@@ -3505,6 +3667,8 @@ private final class CraftNativeIconView: UIImageView {
|
|
|
3505
3667
|
|
|
3506
3668
|
private final class CraftNativeTextView: UITextView {
|
|
3507
3669
|
private let placeholderLabel = UILabel()
|
|
3670
|
+
private var nativeTextContainerInset: UIEdgeInsets?
|
|
3671
|
+
private var nativeLineFragmentPadding: CGFloat?
|
|
3508
3672
|
var placeholder: String? {
|
|
3509
3673
|
didSet { refreshPlaceholder() }
|
|
3510
3674
|
}
|
|
@@ -3529,6 +3693,20 @@ private final class CraftNativeTextView: UITextView {
|
|
|
3529
3693
|
configurePlaceholder()
|
|
3530
3694
|
}
|
|
3531
3695
|
|
|
3696
|
+
func applyAuthoredPadding(_ padding: UIEdgeInsets) {
|
|
3697
|
+
if nativeTextContainerInset == nil {
|
|
3698
|
+
nativeTextContainerInset = textContainerInset
|
|
3699
|
+
nativeLineFragmentPadding = textContainer.lineFragmentPadding
|
|
3700
|
+
}
|
|
3701
|
+
if padding == .zero {
|
|
3702
|
+
textContainerInset = nativeTextContainerInset ?? .zero
|
|
3703
|
+
textContainer.lineFragmentPadding = nativeLineFragmentPadding ?? 0
|
|
3704
|
+
} else {
|
|
3705
|
+
textContainerInset = padding
|
|
3706
|
+
textContainer.lineFragmentPadding = 0
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3532
3710
|
override func layoutSubviews() {
|
|
3533
3711
|
super.layoutSubviews()
|
|
3534
3712
|
let inset = textContainerInset
|
|
@@ -3582,6 +3760,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3582
3760
|
private var handlers: [ObjectIdentifier: String] = [:]
|
|
3583
3761
|
private var pressInHandlers: [ObjectIdentifier: String] = [:]
|
|
3584
3762
|
private var pressOutHandlers: [ObjectIdentifier: String] = [:]
|
|
3763
|
+
private var underlayShowHandlers: [ObjectIdentifier: String] = [:]
|
|
3764
|
+
private var underlayHideHandlers: [ObjectIdentifier: String] = [:]
|
|
3585
3765
|
private var focusHandlers: [ObjectIdentifier: String] = [:]
|
|
3586
3766
|
private var blurHandlers: [ObjectIdentifier: String] = [:]
|
|
3587
3767
|
private var endEditingHandlers: [ObjectIdentifier: String] = [:]
|
|
@@ -3607,6 +3787,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3607
3787
|
private var imageSources: [ObjectIdentifier: String] = [:]
|
|
3608
3788
|
private var imageTasks: [ObjectIdentifier: URLSessionDataTask] = [:]
|
|
3609
3789
|
private var imageErrors: [ObjectIdentifier: String] = [:]
|
|
3790
|
+
/// Text inputs retain UIKit's generated accessibility value unless the
|
|
3791
|
+
/// bundle authored one; remember that distinction so removing the prop
|
|
3792
|
+
/// restores the native value instead of leaving stale metadata behind.
|
|
3793
|
+
private var authoredAccessibilityValues = Set<ObjectIdentifier>()
|
|
3610
3794
|
private var checkboxIndeterminate: [ObjectIdentifier: Bool] = [:]
|
|
3611
3795
|
private var tintedImages = Set<ObjectIdentifier>()
|
|
3612
3796
|
private var imageLoadStartHandlers: [ObjectIdentifier: String] = [:]
|
|
@@ -5138,6 +5322,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5138
5322
|
// reads the node's new layout style on its next pass.
|
|
5139
5323
|
CraftNativeFlexLayout.invalidateAncestors(of: next.view)
|
|
5140
5324
|
}
|
|
5325
|
+
if !applied.isEmpty {
|
|
5326
|
+
// A targeted mutation can change geometry without causing UIKit
|
|
5327
|
+
// to call viewDidLayoutSubviews again. Keep onLayout delivery
|
|
5328
|
+
// aligned with Android's post-mutation layout pass.
|
|
5329
|
+
scheduleLayoutEvents()
|
|
5330
|
+
}
|
|
5141
5331
|
restoreFocus(focused)
|
|
5142
5332
|
return true
|
|
5143
5333
|
}
|
|
@@ -5406,17 +5596,23 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5406
5596
|
}
|
|
5407
5597
|
case "Picker":
|
|
5408
5598
|
let picker = result as! UIPickerView
|
|
5409
|
-
let
|
|
5410
|
-
guard let
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5599
|
+
let itemOptions = (props["items"] as? [[String: Any]])?.compactMap { item -> (value: String, label: String)? in
|
|
5600
|
+
guard let label = item["label"] as? String, !label.isEmpty else { return nil }
|
|
5601
|
+
return (value: craftNativePickerValue(item["value"]) ?? label, label: label)
|
|
5602
|
+
} ?? []
|
|
5603
|
+
let options = itemOptions.isEmpty
|
|
5604
|
+
? children.compactMap { child -> (value: String, label: String)? in
|
|
5605
|
+
guard let node = child as? [String: Any], node["type"] as? String == "Text" else { return nil }
|
|
5606
|
+
let optionProps = node["props"] as? [String: Any] ?? [:]
|
|
5607
|
+
let label = (node["children"] as? [Any] ?? []).compactMap { $0 as? String }.joined()
|
|
5608
|
+
guard !label.isEmpty else { return nil }
|
|
5609
|
+
return (value: craftNativePickerValue(optionProps["value"]) ?? label, label: label)
|
|
5610
|
+
}
|
|
5611
|
+
: itemOptions
|
|
5416
5612
|
let pickerID = ObjectIdentifier(picker)
|
|
5417
5613
|
pickerOptions[pickerID] = options
|
|
5418
5614
|
picker.reloadAllComponents()
|
|
5419
|
-
if let selected = (props["selectedValue"]
|
|
5615
|
+
if let selected = craftNativePickerValue(props["selectedValue"]) ?? craftNativePickerValue(props["value"]),
|
|
5420
5616
|
let row = options.firstIndex(where: { $0.value == selected }) {
|
|
5421
5617
|
picker.selectRow(row, inComponent: 0, animated: false)
|
|
5422
5618
|
} else if !options.isEmpty {
|
|
@@ -5492,9 +5688,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5492
5688
|
let pressable = result as! CraftNativeFlowView
|
|
5493
5689
|
configureStack(pressable, style: style)
|
|
5494
5690
|
reconcileChildren(children, in: pressable, parent: current, path: path, style: style)
|
|
5495
|
-
|
|
5691
|
+
let disabled = props["disabled"] as? Bool == true
|
|
5692
|
+
pressable.pressActiveOpacity = disabled
|
|
5496
5693
|
? nil : number(props["activeOpacity"]) ?? 0.5
|
|
5497
5694
|
pressable.pressBaseOpacity = number(style["opacity"]) ?? 1
|
|
5695
|
+
pressable.pressUnderlayColor = type == "TouchableHighlight" && !disabled ? color(props["underlayColor"]) : nil
|
|
5696
|
+
pressable.pressBaseBackgroundColor = pressable.backgroundColor
|
|
5498
5697
|
case "Switch":
|
|
5499
5698
|
let toggle = result as! UISwitch
|
|
5500
5699
|
toggle.isOn = props["value"] as? Bool ?? props["checked"] as? Bool ?? false
|
|
@@ -5614,8 +5813,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5614
5813
|
)
|
|
5615
5814
|
case "Map":
|
|
5616
5815
|
let map = result as! CraftNativeMapView
|
|
5816
|
+
let mapMarkerHandler = nonEmptyHandler(events["onMarkerPress"])
|
|
5617
5817
|
map.configure(props: props, children: children) { [weak self, weak map] markerID in
|
|
5618
|
-
guard let self, let map,
|
|
5818
|
+
guard let self, let map,
|
|
5819
|
+
let handler = map.markerHandlers[markerID] ?? mapMarkerHandler else { return }
|
|
5619
5820
|
self.send(type: "EVENT", payload: [
|
|
5620
5821
|
"handlerName": handler,
|
|
5621
5822
|
"nativeEvent": ["id": markerID],
|
|
@@ -5649,13 +5850,15 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5649
5850
|
let refreshNode = children.compactMap { $0 as? [String: Any] }.first {
|
|
5650
5851
|
canonicalType($0["type"] as? String ?? "") == "RefreshControl"
|
|
5651
5852
|
}
|
|
5652
|
-
let
|
|
5853
|
+
let propRefresh = props["refreshControl"] as? [String: Any] ?? [:]
|
|
5854
|
+
let refreshProps = refreshNode?["props"] as? [String: Any] ?? propRefresh
|
|
5653
5855
|
let refreshEvents = refreshNode?["events"] as? [String: String] ?? [:]
|
|
5654
5856
|
let refreshHandlerName = nonEmptyHandler(refreshEvents["onRefresh"])
|
|
5655
5857
|
?? nonEmptyHandler(events["onRefresh"])
|
|
5858
|
+
let refreshing = (refreshProps["refreshing"] ?? props["refreshing"]) as? Bool == true
|
|
5656
5859
|
scroll.setRefreshHandler(refreshHandlerName.map { [weak self] handler in
|
|
5657
5860
|
{ self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
5658
|
-
}, refreshing:
|
|
5861
|
+
}, refreshing: refreshing)
|
|
5659
5862
|
scroll.setRefreshAppearance(refreshProps)
|
|
5660
5863
|
var contentStyle = style
|
|
5661
5864
|
if let override = props["contentContainerStyle"] as? [String: Any] {
|
|
@@ -5690,6 +5893,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5690
5893
|
disabled ? nil : nonEmptyHandler(events["onPressOut"]),
|
|
5691
5894
|
for: result
|
|
5692
5895
|
)
|
|
5896
|
+
updateUnderlayHandlers(
|
|
5897
|
+
type == "TouchableHighlight" && !disabled ? nonEmptyHandler(events["onShowUnderlay"]) : nil,
|
|
5898
|
+
type == "TouchableHighlight" && !disabled ? nonEmptyHandler(events["onHideUnderlay"]) : nil,
|
|
5899
|
+
for: result
|
|
5900
|
+
)
|
|
5693
5901
|
if disabled { result.isUserInteractionEnabled = false }
|
|
5694
5902
|
}
|
|
5695
5903
|
applyAccessibility(props, type: type, to: result)
|
|
@@ -5993,6 +6201,13 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5993
6201
|
{ self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
5994
6202
|
}
|
|
5995
6203
|
)
|
|
6204
|
+
let horizontal = props["horizontal"] as? Bool == true
|
|
6205
|
+
list.showsVerticalScrollIndicator = props["showsVerticalScrollIndicator"] as? Bool ?? true
|
|
6206
|
+
list.showsHorizontalScrollIndicator = props["showsHorizontalScrollIndicator"] as? Bool ?? true
|
|
6207
|
+
list.isPagingEnabled = props["pagingEnabled"] as? Bool ?? false
|
|
6208
|
+
list.setScrollTarget(props["scrollTarget"])
|
|
6209
|
+
list.alwaysBounceVertical = props["alwaysBounceVertical"] as? Bool ?? !horizontal
|
|
6210
|
+
list.alwaysBounceHorizontal = props["alwaysBounceHorizontal"] as? Bool ?? horizontal
|
|
5996
6211
|
parent.children = []
|
|
5997
6212
|
}
|
|
5998
6213
|
|
|
@@ -6045,6 +6260,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6045
6260
|
transitionIDs.removeValue(forKey: id)
|
|
6046
6261
|
pressInHandlers.removeValue(forKey: id)
|
|
6047
6262
|
pressOutHandlers.removeValue(forKey: id)
|
|
6263
|
+
underlayShowHandlers.removeValue(forKey: id)
|
|
6264
|
+
underlayHideHandlers.removeValue(forKey: id)
|
|
6048
6265
|
layoutHandlers.removeValue(forKey: id)
|
|
6049
6266
|
lastLayoutFrames.removeValue(forKey: id)
|
|
6050
6267
|
focusHandlers.removeValue(forKey: id)
|
|
@@ -6076,6 +6293,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6076
6293
|
imageSources.removeValue(forKey: id)
|
|
6077
6294
|
imageTasks.removeValue(forKey: id)?.cancel()
|
|
6078
6295
|
imageErrors.removeValue(forKey: id)
|
|
6296
|
+
authoredAccessibilityValues.remove(id)
|
|
6079
6297
|
tintedImages.remove(id)
|
|
6080
6298
|
imageLoadStartHandlers.removeValue(forKey: id)
|
|
6081
6299
|
imageLoadHandlers.removeValue(forKey: id)
|
|
@@ -6117,6 +6335,19 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6117
6335
|
|
|
6118
6336
|
private var layoutEventsScheduled = false
|
|
6119
6337
|
|
|
6338
|
+
/// UIView.frame is undefined while a transform is applied. onLayout is
|
|
6339
|
+
/// the authored layout in the parent coordinate space, matching Android's
|
|
6340
|
+
/// left/top/right/bottom values rather than the transformed visual bounds.
|
|
6341
|
+
private func layoutFrame(of view: UIView) -> CGRect {
|
|
6342
|
+
guard !view.transform.isIdentity else { return view.frame }
|
|
6343
|
+
return CGRect(
|
|
6344
|
+
x: view.center.x - view.bounds.width / 2,
|
|
6345
|
+
y: view.center.y - view.bounds.height / 2,
|
|
6346
|
+
width: view.bounds.width,
|
|
6347
|
+
height: view.bounds.height
|
|
6348
|
+
)
|
|
6349
|
+
}
|
|
6350
|
+
|
|
6120
6351
|
/// A view inside the screen moved: report onLayout once the pass that
|
|
6121
6352
|
/// moved it is over, however deep it was.
|
|
6122
6353
|
fileprivate func scheduleLayoutEvents() {
|
|
@@ -6132,7 +6363,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6132
6363
|
private func emitLayoutEvents(for node: RenderedNode) {
|
|
6133
6364
|
let id = ObjectIdentifier(node.view)
|
|
6134
6365
|
if let handler = layoutHandlers[id], isEffectivelyVisible(node.view) {
|
|
6135
|
-
let frame = node.view
|
|
6366
|
+
let frame = layoutFrame(of: node.view)
|
|
6136
6367
|
if lastLayoutFrames[id] != frame {
|
|
6137
6368
|
lastLayoutFrames[id] = frame
|
|
6138
6369
|
send(type: "EVENT", payload: [
|
|
@@ -6205,8 +6436,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6205
6436
|
let id = ObjectIdentifier(view)
|
|
6206
6437
|
if let pressIn, !pressIn.isEmpty { pressInHandlers[id] = pressIn } else { pressInHandlers.removeValue(forKey: id) }
|
|
6207
6438
|
if let pressOut, !pressOut.isEmpty { pressOutHandlers[id] = pressOut } else { pressOutHandlers.removeValue(forKey: id) }
|
|
6208
|
-
|
|
6439
|
+
let hasUnderlay = (view as? CraftNativeFlowView)?.pressUnderlayColor != nil
|
|
6440
|
+
guard pressInHandlers[id] != nil || pressOutHandlers[id] != nil || hasUnderlay else {
|
|
6209
6441
|
if let recognizer = pressFeedbackRecognizers.removeValue(forKey: id) { view.removeGestureRecognizer(recognizer) }
|
|
6442
|
+
if let pressable = view as? CraftNativeFlowView { pressable.backgroundColor = pressable.pressBaseBackgroundColor }
|
|
6210
6443
|
if !ownsTouches(view) { view.isUserInteractionEnabled = handlers[id] != nil || linkTargets[id] != nil || longPressHandlers[id] != nil || view is CraftNativeFlowView }
|
|
6211
6444
|
return
|
|
6212
6445
|
}
|
|
@@ -6222,6 +6455,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6222
6455
|
if !ownsTouches(view) { view.isUserInteractionEnabled = true }
|
|
6223
6456
|
}
|
|
6224
6457
|
|
|
6458
|
+
private func updateUnderlayHandlers(_ show: String?, _ hide: String?, for view: UIView) {
|
|
6459
|
+
let id = ObjectIdentifier(view)
|
|
6460
|
+
if let show, !show.isEmpty { underlayShowHandlers[id] = show } else { underlayShowHandlers.removeValue(forKey: id) }
|
|
6461
|
+
if let hide, !hide.isEmpty { underlayHideHandlers[id] = hide } else { underlayHideHandlers.removeValue(forKey: id) }
|
|
6462
|
+
}
|
|
6463
|
+
|
|
6225
6464
|
private func updateLongPressHandler(_ handler: String?, for view: UIView) {
|
|
6226
6465
|
let id = ObjectIdentifier(view)
|
|
6227
6466
|
if let handler, !handler.isEmpty {
|
|
@@ -6289,7 +6528,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6289
6528
|
applyAutofill(to: textView, props: props)
|
|
6290
6529
|
// The style's padding is where the text starts, as in a web textarea.
|
|
6291
6530
|
let padding = paddingInsets(style)
|
|
6292
|
-
if
|
|
6531
|
+
if let craftTextView = textView as? CraftNativeTextView {
|
|
6532
|
+
craftTextView.applyAuthoredPadding(padding)
|
|
6533
|
+
} else if padding != .zero {
|
|
6293
6534
|
if textView.textContainerInset != padding { textView.textContainerInset = padding }
|
|
6294
6535
|
textView.textContainer.lineFragmentPadding = 0
|
|
6295
6536
|
}
|
|
@@ -6714,7 +6955,14 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6714
6955
|
}
|
|
6715
6956
|
|
|
6716
6957
|
private func updateImage(_ view: UIImageView, source: Any?, defaultSource: Any? = nil) {
|
|
6717
|
-
let
|
|
6958
|
+
let sourceObject = source as? [String: Any]
|
|
6959
|
+
let uri = (source as? String) ?? sourceObject?["uri"] as? String
|
|
6960
|
+
let headers = (sourceObject?["headers"] as? [String: Any])?.reduce(into: [String: String]()) { result, entry in
|
|
6961
|
+
if let value = entry.value as? String { result[entry.key] = value }
|
|
6962
|
+
} ?? [:]
|
|
6963
|
+
let requestKey = uri.map { value in
|
|
6964
|
+
value + "\u{0}" + headers.sorted { $0.key < $1.key }.map { "\($0.key)=\($0.value)" }.joined(separator: "\u{0}")
|
|
6965
|
+
}
|
|
6718
6966
|
let id = ObjectIdentifier(view)
|
|
6719
6967
|
guard let uri, !uri.isEmpty else {
|
|
6720
6968
|
if imageSources[id] == "" { return }
|
|
@@ -6723,9 +6971,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6723
6971
|
imageFailure(view, uri: nil, message: "Image source is missing")
|
|
6724
6972
|
return
|
|
6725
6973
|
}
|
|
6726
|
-
guard imageSources[id] !=
|
|
6974
|
+
guard let requestKey, imageSources[id] != requestKey else { return }
|
|
6727
6975
|
imageTasks.removeValue(forKey: id)?.cancel()
|
|
6728
|
-
imageSources[id] =
|
|
6976
|
+
imageSources[id] = requestKey
|
|
6729
6977
|
imageErrors.removeValue(forKey: id)
|
|
6730
6978
|
view.image = immediateImage(defaultSource).map { imageForDisplay($0, view: view) }
|
|
6731
6979
|
imageDidChange(view)
|
|
@@ -6746,10 +6994,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6746
6994
|
return
|
|
6747
6995
|
}
|
|
6748
6996
|
if let url = URL(string: uri), Self.loadsImage(from: url) {
|
|
6749
|
-
|
|
6997
|
+
var request = URLRequest(url: url)
|
|
6998
|
+
for (name, value) in headers { request.setValue(value, forHTTPHeaderField: name) }
|
|
6999
|
+
let task = URLSession.shared.dataTask(with: request) { [weak self, weak view] data, _, _ in
|
|
6750
7000
|
guard let self, let view else { return }
|
|
6751
7001
|
DispatchQueue.main.async {
|
|
6752
|
-
guard self.imageSources[ObjectIdentifier(view)] ==
|
|
7002
|
+
guard self.imageSources[ObjectIdentifier(view)] == requestKey else { return }
|
|
6753
7003
|
self.imageTasks.removeValue(forKey: ObjectIdentifier(view))
|
|
6754
7004
|
guard let data, let image = UIImage(data: data) else {
|
|
6755
7005
|
self.imageFailure(view, uri: uri, message: "Image download failed")
|
|
@@ -6823,11 +7073,17 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6823
7073
|
}
|
|
6824
7074
|
|
|
6825
7075
|
private func applyAccessibility(_ props: [String: Any], type: String, to view: UIView) {
|
|
7076
|
+
let id = ObjectIdentifier(view)
|
|
6826
7077
|
view.accessibilityLabel = (props["accessibilityLabel"] as? String)
|
|
6827
7078
|
?? ((type == "Image" || type == "ImageBackground") ? props["alt"] as? String : nil)
|
|
6828
7079
|
view.accessibilityHint = props["accessibilityHint"] as? String
|
|
6829
7080
|
if let value = props["accessibilityValue"] {
|
|
6830
7081
|
view.accessibilityValue = (value as? String) ?? (value as? NSNumber)?.stringValue
|
|
7082
|
+
authoredAccessibilityValues.insert(id)
|
|
7083
|
+
} else if authoredAccessibilityValues.remove(id) != nil {
|
|
7084
|
+
// Clearing an authored value lets UIKit expose the field's live
|
|
7085
|
+
// value again (particularly important for TextInput).
|
|
7086
|
+
view.accessibilityValue = nil
|
|
6831
7087
|
} else if type != "TextInput" {
|
|
6832
7088
|
view.accessibilityValue = nil
|
|
6833
7089
|
}
|
|
@@ -7216,13 +7472,21 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7216
7472
|
let id = ObjectIdentifier(view)
|
|
7217
7473
|
switch sender.state {
|
|
7218
7474
|
case .began:
|
|
7219
|
-
if let pressable = view as? CraftNativeFlowView
|
|
7220
|
-
|
|
7475
|
+
if let pressable = view as? CraftNativeFlowView {
|
|
7476
|
+
if let underlayColor = pressable.pressUnderlayColor { pressable.backgroundColor = underlayColor }
|
|
7477
|
+
if let activeOpacity = pressable.pressActiveOpacity {
|
|
7478
|
+
UIView.animate(withDuration: 0.1) { pressable.alpha = pressable.pressBaseOpacity * activeOpacity }
|
|
7479
|
+
}
|
|
7221
7480
|
}
|
|
7222
7481
|
if let handler = pressInHandlers[id] { send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
7482
|
+
if let handler = underlayShowHandlers[id] { send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
7223
7483
|
case .ended, .cancelled, .failed:
|
|
7224
|
-
if let pressable = view as? CraftNativeFlowView {
|
|
7484
|
+
if let pressable = view as? CraftNativeFlowView {
|
|
7485
|
+
pressable.backgroundColor = pressable.pressBaseBackgroundColor
|
|
7486
|
+
UIView.animate(withDuration: 0.1) { pressable.alpha = pressable.pressBaseOpacity }
|
|
7487
|
+
}
|
|
7225
7488
|
if let handler = pressOutHandlers[id] { send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
7489
|
+
if let handler = underlayHideHandlers[id] { send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
7226
7490
|
default:
|
|
7227
7491
|
break
|
|
7228
7492
|
}
|