craft-native 0.0.173 → 0.0.174
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 +2 -0
- package/dist/android/src/index.js +4 -1
- package/dist/android/templates/AndroidManifest.xml.template +2 -0
- package/dist/android/templates/CraftNativeShell.kt.template +50 -0
- package/dist/android/templates/MainActivityNative.kt.template +441 -27
- package/dist/android/templates/build.gradle.kts.app.template +3 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +2 -0
- package/dist/ios/src/index.js +1 -0
- package/dist/ios/templates/CraftApp.swift +5 -0
- package/dist/ios/templates/CraftNativeActions.swift +174 -0
- package/dist/ios/templates/CraftNativeFlatList.swift +1 -1
- package/dist/ios/templates/CraftNativeScreen.swift +437 -38
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import AVKit
|
|
|
4
4
|
import CryptoKit
|
|
5
5
|
import Darwin
|
|
6
6
|
import JavaScriptCore
|
|
7
|
+
import MapKit
|
|
7
8
|
import OSLog
|
|
8
9
|
import Security
|
|
9
10
|
import SwiftUI
|
|
@@ -426,6 +427,166 @@ private final class CraftNativeVideoView: UIView {
|
|
|
426
427
|
deinit { stop() }
|
|
427
428
|
}
|
|
428
429
|
|
|
430
|
+
private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
431
|
+
private var annotationsByID: [String: MKPointAnnotation] = [:]
|
|
432
|
+
private var overlaysByID: [String: MKPolyline] = [:]
|
|
433
|
+
private var overlayColors: [ObjectIdentifier: UIColor] = [:]
|
|
434
|
+
private(set) var markerHandlers: [String: String] = [:]
|
|
435
|
+
var onMarkerTap: ((String) -> Void)?
|
|
436
|
+
|
|
437
|
+
override init(frame: CGRect) {
|
|
438
|
+
super.init(frame: frame)
|
|
439
|
+
delegate = self
|
|
440
|
+
showsCompass = false
|
|
441
|
+
showsScale = false
|
|
442
|
+
isRotateEnabled = false
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
required init?(coder: NSCoder) {
|
|
446
|
+
super.init(coder: coder)
|
|
447
|
+
delegate = self
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
func configure(props: [String: Any], children: [Any], markerHandler: @escaping (String) -> Void) {
|
|
451
|
+
onMarkerTap = markerHandler
|
|
452
|
+
isScrollEnabled = props["scrollEnabled"] as? Bool ?? true
|
|
453
|
+
isZoomEnabled = props["zoomEnabled"] as? Bool ?? true
|
|
454
|
+
isRotateEnabled = props["rotateEnabled"] as? Bool ?? false
|
|
455
|
+
mapType = Self.mapType(props["mapType"] as? String)
|
|
456
|
+
|
|
457
|
+
var nextMarkers: [String: (coordinate: CLLocationCoordinate2D, title: String?)] = [:]
|
|
458
|
+
var nextHandlers: [String: String] = [:]
|
|
459
|
+
var nextPolylines: [String: (coordinates: [CLLocationCoordinate2D], color: UIColor, width: CGFloat)] = [:]
|
|
460
|
+
for (index, child) in children.enumerated() {
|
|
461
|
+
guard let node = child as? [String: Any], let rawType = node["type"] as? String else { continue }
|
|
462
|
+
let type = rawType.lowercased()
|
|
463
|
+
let childProps = node["props"] as? [String: Any] ?? [:]
|
|
464
|
+
let childEvents = node["events"] as? [String: String] ?? [:]
|
|
465
|
+
let id = (childProps["id"] as? CustomStringConvertible)?.description
|
|
466
|
+
?? (node["id"] as? String)
|
|
467
|
+
?? "overlay-\(index)"
|
|
468
|
+
let points = Self.coordinates(childProps["geometry"])
|
|
469
|
+
if type == "mapmarker" || type == "marker" {
|
|
470
|
+
guard let point = points.first else { continue }
|
|
471
|
+
nextMarkers[id] = (point, childProps["title"] as? String)
|
|
472
|
+
if let handler = childEvents["onPress"] ?? childEvents["onMarkerPress"], !handler.isEmpty {
|
|
473
|
+
nextHandlers[id] = handler
|
|
474
|
+
}
|
|
475
|
+
} else if type == "mappolyline" || type == "polyline" {
|
|
476
|
+
guard points.count > 1 else { continue }
|
|
477
|
+
nextPolylines[id] = (points, Self.color(childProps["strokeColor"] as? String), CGFloat((childProps["strokeWidth"] as? NSNumber)?.doubleValue ?? 3))
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
let staleMarkers = annotationsByID.keys.filter { nextMarkers[$0] == nil }
|
|
482
|
+
removeAnnotations(staleMarkers.compactMap { annotationsByID.removeValue(forKey: $0) })
|
|
483
|
+
for (id, value) in nextMarkers {
|
|
484
|
+
if let annotation = annotationsByID[id] {
|
|
485
|
+
annotation.coordinate = value.coordinate
|
|
486
|
+
annotation.title = value.title
|
|
487
|
+
} else {
|
|
488
|
+
let annotation = MKPointAnnotation()
|
|
489
|
+
annotation.coordinate = value.coordinate
|
|
490
|
+
annotation.title = value.title
|
|
491
|
+
annotationsByID[id] = annotation
|
|
492
|
+
addAnnotation(annotation)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
markerHandlers = nextHandlers
|
|
496
|
+
|
|
497
|
+
removeOverlays(Array(overlaysByID.values))
|
|
498
|
+
overlaysByID.removeAll(keepingCapacity: true)
|
|
499
|
+
overlayColors.removeAll(keepingCapacity: true)
|
|
500
|
+
for (id, value) in nextPolylines {
|
|
501
|
+
let overlay = MKPolyline(coordinates: value.coordinates, count: value.coordinates.count)
|
|
502
|
+
overlaysByID[id] = overlay
|
|
503
|
+
overlayColors[ObjectIdentifier(overlay)] = value.color
|
|
504
|
+
addOverlay(overlay)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if let region = Self.region(props["region"] ?? props["initialRegion"]) {
|
|
508
|
+
setRegion(region, animated: false)
|
|
509
|
+
} else if props["fitToBounds"] as? Bool == true {
|
|
510
|
+
fitToOverlays()
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private func fitToOverlays() {
|
|
515
|
+
var rect = MKMapRect.null
|
|
516
|
+
for overlay in overlaysByID.values { rect = rect.union(overlay.boundingMapRect) }
|
|
517
|
+
for annotation in annotationsByID.values {
|
|
518
|
+
let point = MKMapPoint(annotation.coordinate)
|
|
519
|
+
rect = rect.union(MKMapRect(x: point.x, y: point.y, width: 0, height: 0))
|
|
520
|
+
}
|
|
521
|
+
guard !rect.isNull else { return }
|
|
522
|
+
let inset = UIEdgeInsets(top: 24, left: 24, bottom: 24, right: 24)
|
|
523
|
+
setVisibleMapRect(rect, edgePadding: inset, animated: false)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
|
|
527
|
+
guard let id = annotationsByID.first(where: { $0.value === annotation })?.key else { return nil }
|
|
528
|
+
let view = (dequeueReusableAnnotationView(withIdentifier: "craft-native-marker") as? MKMarkerAnnotationView)
|
|
529
|
+
?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "craft-native-marker")
|
|
530
|
+
view.annotation = annotation
|
|
531
|
+
view.markerTintColor = .systemRed
|
|
532
|
+
view.accessibilityIdentifier = id
|
|
533
|
+
return view
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
func mapView(_ mapView: MKMapView, didSelect annotation: MKAnnotation) {
|
|
537
|
+
guard let id = annotationsByID.first(where: { $0.value === annotation })?.key else { return }
|
|
538
|
+
onMarkerTap?(id)
|
|
539
|
+
deselectAnnotation(annotation, animated: false)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
|
|
543
|
+
let renderer = MKPolylineRenderer(overlay: overlay)
|
|
544
|
+
renderer.strokeColor = overlayColors[ObjectIdentifier(overlay)] ?? .systemBlue
|
|
545
|
+
renderer.lineWidth = 3
|
|
546
|
+
return renderer
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private static func coordinates(_ value: Any?) -> [CLLocationCoordinate2D] {
|
|
550
|
+
let numbers = (value as? [Any])?.compactMap { ($0 as? NSNumber)?.doubleValue }
|
|
551
|
+
?? (value as? [NSNumber])?.map(\.doubleValue)
|
|
552
|
+
?? []
|
|
553
|
+
return stride(from: 0, to: numbers.count - 1, by: 2).map {
|
|
554
|
+
CLLocationCoordinate2D(latitude: numbers[$0], longitude: numbers[$0 + 1])
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private static func region(_ value: Any?) -> MKCoordinateRegion? {
|
|
559
|
+
guard let value = value as? [String: Any],
|
|
560
|
+
let center = value["center"] as? [String: Any],
|
|
561
|
+
let span = value["span"] as? [String: Any],
|
|
562
|
+
let latitude = (center["latitude"] as? NSNumber)?.doubleValue,
|
|
563
|
+
let longitude = (center["longitude"] as? NSNumber)?.doubleValue,
|
|
564
|
+
let latitudeDelta = (span["latitudeDelta"] as? NSNumber)?.doubleValue ?? (span["latitude_delta"] as? NSNumber)?.doubleValue,
|
|
565
|
+
let longitudeDelta = (span["longitudeDelta"] as? NSNumber)?.doubleValue ?? (span["longitude_delta"] as? NSNumber)?.doubleValue else { return nil }
|
|
566
|
+
return MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), span: MKCoordinateSpan(latitudeDelta: latitudeDelta, longitudeDelta: longitudeDelta))
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private static func color(_ value: String?) -> UIColor {
|
|
570
|
+
switch value?.lowercased() {
|
|
571
|
+
case "red": return .systemRed
|
|
572
|
+
case "green": return .systemGreen
|
|
573
|
+
case "orange": return .systemOrange
|
|
574
|
+
case "purple": return .systemPurple
|
|
575
|
+
case "yellow": return .systemYellow
|
|
576
|
+
default: return .systemBlue
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private static func mapType(_ value: String?) -> MKMapType {
|
|
581
|
+
switch value?.lowercased() {
|
|
582
|
+
case "satellite": return .satellite
|
|
583
|
+
case "hybrid": return .hybrid
|
|
584
|
+
case "muted_standard": return .mutedStandard
|
|
585
|
+
default: return .standard
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
429
590
|
/// A WebView-free host for the first stx native vertical slice. The bundled
|
|
430
591
|
/// JavaScript sends whole, compiled view trees to UIKit and receives control
|
|
431
592
|
/// events and Craft API replies through JavaScriptCore.
|
|
@@ -781,11 +942,28 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
781
942
|
var wrapReverse = false { didSet { if wrapReverse != oldValue { invalidateMeasurements() } } }
|
|
782
943
|
var padding = UIEdgeInsets.zero {
|
|
783
944
|
didSet {
|
|
784
|
-
layoutMargins =
|
|
945
|
+
layoutMargins = effectivePadding
|
|
785
946
|
isLayoutMarginsRelativeArrangement = true
|
|
786
947
|
if padding != oldValue { invalidateMeasurements() }
|
|
787
948
|
}
|
|
788
949
|
}
|
|
950
|
+
/// Insets contributed by a native SafeAreaView root. Authored padding
|
|
951
|
+
/// remains separate so a reconciliation can remove either source without
|
|
952
|
+
/// leaving stale safe-area space behind.
|
|
953
|
+
var safeAreaPadding = UIEdgeInsets.zero {
|
|
954
|
+
didSet {
|
|
955
|
+
layoutMargins = effectivePadding
|
|
956
|
+
if safeAreaPadding != oldValue { invalidateMeasurements() }
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
var effectivePadding: UIEdgeInsets {
|
|
960
|
+
UIEdgeInsets(
|
|
961
|
+
top: padding.top + safeAreaPadding.top,
|
|
962
|
+
left: padding.left + safeAreaPadding.left,
|
|
963
|
+
bottom: padding.bottom + safeAreaPadding.bottom,
|
|
964
|
+
right: padding.right + safeAreaPadding.right
|
|
965
|
+
)
|
|
966
|
+
}
|
|
789
967
|
override var axis: NSLayoutConstraint.Axis {
|
|
790
968
|
didSet { if axis != oldValue { invalidateMeasurements() } }
|
|
791
969
|
}
|
|
@@ -1020,8 +1198,9 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1020
1198
|
let children = visibleChildren
|
|
1021
1199
|
let flow = children.filter { $0.craftLayoutStyle.position != "absolute" }
|
|
1022
1200
|
let horizontal = axis == .horizontal
|
|
1023
|
-
let
|
|
1024
|
-
let
|
|
1201
|
+
let contentPadding = effectivePadding
|
|
1202
|
+
let paddingWidth = contentPadding.left + contentPadding.right
|
|
1203
|
+
let paddingHeight = contentPadding.top + contentPadding.bottom
|
|
1025
1204
|
let innerWidth = width.map { max(0, $0 - paddingWidth) }
|
|
1026
1205
|
let innerHeight = height.map { max(0, $0 - paddingHeight) }
|
|
1027
1206
|
let roomWidth = max(0, (width ?? maxWidth) - paddingWidth)
|
|
@@ -1046,7 +1225,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1046
1225
|
let size = CGSize(width: width ?? contentSize.width + paddingWidth, height: height ?? contentSize.height + paddingHeight)
|
|
1047
1226
|
|
|
1048
1227
|
if place {
|
|
1049
|
-
let content = CGRect(x:
|
|
1228
|
+
let content = CGRect(x: contentPadding.left, y: contentPadding.top, width: max(0, size.width - paddingWidth), height: max(0, size.height - paddingHeight))
|
|
1050
1229
|
for child in children where child.craftLayoutStyle.position == "absolute" {
|
|
1051
1230
|
let style = child.craftLayoutStyle
|
|
1052
1231
|
var fixedWidth = style.left != nil && style.right != nil
|
|
@@ -1432,8 +1611,8 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1432
1611
|
? (sharedLine?[offset].position ?? (mainOffset + item.mainLeading))
|
|
1433
1612
|
: mainOffset + item.mainLeading
|
|
1434
1613
|
let frame = horizontal
|
|
1435
|
-
? CGRect(x:
|
|
1436
|
-
: CGRect(x:
|
|
1614
|
+
? CGRect(x: contentPadding.left + mainPosition, y: contentPadding.top + physicalLineOffset + physicalCrossPosition, width: item.main, height: item.cross)
|
|
1615
|
+
: CGRect(x: contentPadding.left + physicalLineOffset + physicalCrossPosition, y: contentPadding.top + mainPosition, width: item.cross, height: item.main)
|
|
1437
1616
|
frames.append((item.view, frame))
|
|
1438
1617
|
if !useSharedPositions { mainOffset += item.mainMargins + item.main + mainGap + between }
|
|
1439
1618
|
}
|
|
@@ -1475,7 +1654,10 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1475
1654
|
let align = style.alignSelf ?? alignItems
|
|
1476
1655
|
var fixedWidth = style.width?.resolve(cellWidth)
|
|
1477
1656
|
var fixedHeight = style.height?.resolve(nil)
|
|
1478
|
-
|
|
1657
|
+
// A cell's item is as wide as its column unless it sets a width
|
|
1658
|
+
// (or an aspect ratio derives one from its height), whatever its
|
|
1659
|
+
// height: a fixed-height swatch with no content was drawn 0 wide.
|
|
1660
|
+
if fixedWidth == nil && align == "stretch" && (fixedHeight == nil || style.aspectRatio == nil) { fixedWidth = available }
|
|
1479
1661
|
(fixedWidth, fixedHeight) = aspectDimensions(style, width: fixedWidth, height: fixedHeight)
|
|
1480
1662
|
let size = CraftNativeFlexLayout.measure(
|
|
1481
1663
|
child,
|
|
@@ -1506,7 +1688,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1506
1688
|
let alignment: UInt32 = align == "center" ? 1 : align == "flex-end" ? 2 : 0
|
|
1507
1689
|
var explicitWidth = style.width?.resolve(cellWidth)
|
|
1508
1690
|
var explicitHeight = style.height?.resolve(nil)
|
|
1509
|
-
if explicitWidth == nil && explicitHeight == nil
|
|
1691
|
+
if explicitWidth == nil && align == "stretch" && (explicitHeight == nil || style.aspectRatio == nil) {
|
|
1510
1692
|
explicitWidth = max(0, cellWidth - style.marginLeft - style.marginRight)
|
|
1511
1693
|
}
|
|
1512
1694
|
(explicitWidth, explicitHeight) = aspectDimensions(style, width: explicitWidth, height: explicitHeight)
|
|
@@ -1526,7 +1708,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1526
1708
|
if let sharedFrames {
|
|
1527
1709
|
for (index, child) in children.enumerated() {
|
|
1528
1710
|
let frame = sharedFrames[index]
|
|
1529
|
-
frames.append((child, CGRect(x:
|
|
1711
|
+
frames.append((child, CGRect(x: contentPadding.left + frame.x, y: contentPadding.top + frame.y, width: frame.width, height: frame.height)))
|
|
1530
1712
|
}
|
|
1531
1713
|
return CGSize(width: width, height: innerHeight ?? height)
|
|
1532
1714
|
}
|
|
@@ -1550,7 +1732,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1550
1732
|
case "stretch": rowExtra = gridFree / CGFloat(max(1, rowHeights.count))
|
|
1551
1733
|
default: break
|
|
1552
1734
|
}
|
|
1553
|
-
var rowTop =
|
|
1735
|
+
var rowTop = contentPadding.top + rowLeading
|
|
1554
1736
|
for (index, child) in children.enumerated() {
|
|
1555
1737
|
let style = child.craftLayoutStyle
|
|
1556
1738
|
let row = index / columns
|
|
@@ -1562,7 +1744,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1562
1744
|
let availableHeight = max(0, rowHeight - style.marginTop - style.marginBottom)
|
|
1563
1745
|
let childWidth = min(availableWidth, size.width)
|
|
1564
1746
|
let childHeight = align == "stretch" && style.height == nil && style.aspectRatio == nil ? availableHeight : min(availableHeight, size.height)
|
|
1565
|
-
let x =
|
|
1747
|
+
let x = contentPadding.left + CGFloat(column) * (cellWidth + horizontalGap) + style.marginLeft
|
|
1566
1748
|
+ (align == "center" ? (availableWidth - childWidth) / 2 : align == "flex-end" ? availableWidth - childWidth : 0)
|
|
1567
1749
|
let y = rowTop + style.marginTop
|
|
1568
1750
|
+ (align == "center" ? (availableHeight - childHeight) / 2 : align == "flex-end" ? availableHeight - childHeight : 0)
|
|
@@ -1574,6 +1756,41 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1574
1756
|
}
|
|
1575
1757
|
}
|
|
1576
1758
|
|
|
1759
|
+
/// A container that applies only the requested system safe-area edges. The
|
|
1760
|
+
/// host keeps the root edge-to-edge when this node is the screen root so the
|
|
1761
|
+
/// node, rather than the controller's outer stack, owns the insets.
|
|
1762
|
+
private final class CraftNativeSafeAreaView: CraftNativeFlowView {
|
|
1763
|
+
private var edges: Set<String> = ["top", "right", "bottom", "left"]
|
|
1764
|
+
|
|
1765
|
+
func setSafeAreaEdges(_ raw: Any?) {
|
|
1766
|
+
guard let values = raw as? [Any] else {
|
|
1767
|
+
edges = ["top", "right", "bottom", "left"]
|
|
1768
|
+
updateSafeAreaPadding()
|
|
1769
|
+
return
|
|
1770
|
+
}
|
|
1771
|
+
edges = Set(values.compactMap { value in
|
|
1772
|
+
guard let edge = value as? String, ["top", "right", "bottom", "left"].contains(edge) else { return nil }
|
|
1773
|
+
return edge
|
|
1774
|
+
})
|
|
1775
|
+
updateSafeAreaPadding()
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
override func safeAreaInsetsDidChange() {
|
|
1779
|
+
super.safeAreaInsetsDidChange()
|
|
1780
|
+
updateSafeAreaPadding()
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
private func updateSafeAreaPadding() {
|
|
1784
|
+
let insets = safeAreaInsets
|
|
1785
|
+
safeAreaPadding = UIEdgeInsets(
|
|
1786
|
+
top: edges.contains("top") ? insets.top : 0,
|
|
1787
|
+
left: edges.contains("left") ? insets.left : 0,
|
|
1788
|
+
bottom: edges.contains("bottom") ? insets.bottom : 0,
|
|
1789
|
+
right: edges.contains("right") ? insets.right : 0
|
|
1790
|
+
)
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1577
1794
|
/// A flex container that responds to the controller's keyboard overlap.
|
|
1578
1795
|
/// `padding` keeps the content visible without moving the field; `position`
|
|
1579
1796
|
/// lifts the whole container; `height` reduces its viewport.
|
|
@@ -1773,6 +1990,18 @@ private final class CraftNativeModalView: UIView {
|
|
|
1773
1990
|
}
|
|
1774
1991
|
}
|
|
1775
1992
|
|
|
1993
|
+
/// Whether a scrolling view's gesture runs together with `other`. A scroll
|
|
1994
|
+
/// view is the delegate of its own pan as well as of its keyboard tap, so this
|
|
1995
|
+
/// answers for both. Everything runs together (the keyboard tap watches
|
|
1996
|
+
/// alongside a control's own gestures; the back-swipe and the tab swipe decide
|
|
1997
|
+
/// for themselves) except two scroll views' pans: run together, a drag on a
|
|
1998
|
+
/// nested scroll view scrolled its parent at the same time.
|
|
1999
|
+
func craftScrollGesture(_ gesture: UIGestureRecognizer, runsWith other: UIGestureRecognizer) -> Bool {
|
|
2000
|
+
guard let scroll = gesture.view as? UIScrollView, gesture === scroll.panGestureRecognizer,
|
|
2001
|
+
let otherScroll = other.view as? UIScrollView, other === otherScroll.panGestureRecognizer else { return true }
|
|
2002
|
+
return false
|
|
2003
|
+
}
|
|
2004
|
+
|
|
1776
2005
|
private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDelegate {
|
|
1777
2006
|
let contentStack = CraftNativeFlowView()
|
|
1778
2007
|
private let keyboardTapGesture = UITapGestureRecognizer()
|
|
@@ -1994,6 +2223,22 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
1994
2223
|
}
|
|
1995
2224
|
}
|
|
1996
2225
|
|
|
2226
|
+
/// Applies the shared RefreshControl appearance props. UIKit's refresh
|
|
2227
|
+
/// control owns the spinner and optional title; keep the authored values
|
|
2228
|
+
/// on the control so a later reconciliation can clear them as well.
|
|
2229
|
+
func setRefreshAppearance(_ props: [String: Any]) {
|
|
2230
|
+
pullToRefresh.tintColor = (props["tintColor"] as? String).flatMap { UIColor(hex: $0) }
|
|
2231
|
+
guard let title = props["title"] as? String, !title.isEmpty else {
|
|
2232
|
+
pullToRefresh.attributedTitle = nil
|
|
2233
|
+
return
|
|
2234
|
+
}
|
|
2235
|
+
var attributes: [NSAttributedString.Key: Any] = [:]
|
|
2236
|
+
if let titleColor = (props["titleColor"] as? String).flatMap({ UIColor(hex: $0) }) {
|
|
2237
|
+
attributes[.foregroundColor] = titleColor
|
|
2238
|
+
}
|
|
2239
|
+
pullToRefresh.attributedTitle = NSAttributedString(string: title, attributes: attributes)
|
|
2240
|
+
}
|
|
2241
|
+
|
|
1997
2242
|
var isRefreshingNow: Bool { pullToRefresh.isRefreshing }
|
|
1998
2243
|
|
|
1999
2244
|
@objc private func refreshPulled() {
|
|
@@ -2025,7 +2270,7 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
2025
2270
|
}
|
|
2026
2271
|
|
|
2027
2272
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
2028
|
-
|
|
2273
|
+
craftScrollGesture(gestureRecognizer, runsWith: otherGestureRecognizer)
|
|
2029
2274
|
}
|
|
2030
2275
|
|
|
2031
2276
|
func setAxis(_ axis: NSLayoutConstraint.Axis) {
|
|
@@ -2510,6 +2755,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2510
2755
|
private var imageLoadHandlers: [ObjectIdentifier: String] = [:]
|
|
2511
2756
|
private var imageLoadEndHandlers: [ObjectIdentifier: String] = [:]
|
|
2512
2757
|
private var imageErrorHandlers: [ObjectIdentifier: String] = [:]
|
|
2758
|
+
/// Native links keep their target separate from optional press handlers.
|
|
2759
|
+
/// The compiler can emit an anchor as `TouchableOpacity`, so this map is
|
|
2760
|
+
/// intentionally shared by Link and the generic pressable path.
|
|
2761
|
+
private var linkTargets: [ObjectIdentifier: String] = [:]
|
|
2513
2762
|
private var renderedRoot: RenderedNode?
|
|
2514
2763
|
private weak var lastFocusedInput: UIView?
|
|
2515
2764
|
private var flatListRows: [ObjectIdentifier: [String: RenderedNode]] = [:]
|
|
@@ -2582,11 +2831,17 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2582
2831
|
view.addSubview(rootStack)
|
|
2583
2832
|
rootTopToSafeArea = rootStack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
|
|
2584
2833
|
rootTopToView = rootStack.topAnchor.constraint(equalTo: view.topAnchor)
|
|
2834
|
+
rootBottomToSafeArea = rootStack.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
|
|
2835
|
+
rootBottomToView = rootStack.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
|
2836
|
+
rootLeadingToSafeArea = rootStack.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor)
|
|
2837
|
+
rootLeadingToView = rootStack.leadingAnchor.constraint(equalTo: view.leadingAnchor)
|
|
2838
|
+
rootTrailingToSafeArea = rootStack.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
|
|
2839
|
+
rootTrailingToView = rootStack.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
|
2585
2840
|
NSLayoutConstraint.activate([
|
|
2586
2841
|
rootTopToSafeArea!,
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2842
|
+
rootBottomToSafeArea!,
|
|
2843
|
+
rootLeadingToSafeArea!,
|
|
2844
|
+
rootTrailingToSafeArea!
|
|
2590
2845
|
])
|
|
2591
2846
|
setupJavaScript()
|
|
2592
2847
|
loadBundle()
|
|
@@ -3137,7 +3392,17 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3137
3392
|
private var titleFromOptions = false
|
|
3138
3393
|
private var rootTopToSafeArea: NSLayoutConstraint?
|
|
3139
3394
|
private var rootTopToView: NSLayoutConstraint?
|
|
3395
|
+
private var rootBottomToSafeArea: NSLayoutConstraint?
|
|
3396
|
+
private var rootBottomToView: NSLayoutConstraint?
|
|
3397
|
+
private var rootLeadingToSafeArea: NSLayoutConstraint?
|
|
3398
|
+
private var rootLeadingToView: NSLayoutConstraint?
|
|
3399
|
+
private var rootTrailingToSafeArea: NSLayoutConstraint?
|
|
3400
|
+
private var rootTrailingToView: NSLayoutConstraint?
|
|
3401
|
+
private var rootIsSafeAreaView = false
|
|
3140
3402
|
private var navigationBarHidden: Bool?
|
|
3403
|
+
/// A native `<StatusBar>` node overrides the app default for this screen.
|
|
3404
|
+
private var statusBarStyleOverride: UIStatusBarStyle?
|
|
3405
|
+
private var statusBarHiddenOverride: Bool?
|
|
3141
3406
|
/// The app's tab bar away while this screen shows (`tabBarHidden`).
|
|
3142
3407
|
private var tabBarHidden = false
|
|
3143
3408
|
/// Whether a back-swipe may leave this screen at all, the edge's
|
|
@@ -3158,6 +3423,14 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3158
3423
|
/// full-screen timer is black edge to edge. nil is the app's.
|
|
3159
3424
|
private var screenBackgroundColor: UIColor?
|
|
3160
3425
|
|
|
3426
|
+
override var preferredStatusBarStyle: UIStatusBarStyle {
|
|
3427
|
+
statusBarStyleOverride ?? .default
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
override var prefersStatusBarHidden: Bool {
|
|
3431
|
+
statusBarHiddenOverride ?? false
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3161
3434
|
/// Whether this screen asked for a bar of its own through `setOptions`: a
|
|
3162
3435
|
/// title, a large title or buttons, and not `hidden`. A tab's root screen
|
|
3163
3436
|
/// in a hybrid app is shown bare unless it did.
|
|
@@ -3221,6 +3494,22 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3221
3494
|
}
|
|
3222
3495
|
}
|
|
3223
3496
|
|
|
3497
|
+
private func applyStatusBar(_ props: [String: Any]) {
|
|
3498
|
+
let style: UIStatusBarStyle?
|
|
3499
|
+
switch (props["barStyle"] as? String)?.lowercased() {
|
|
3500
|
+
case "light-content", "light": style = .lightContent
|
|
3501
|
+
case "dark-content", "dark": style = .darkContent
|
|
3502
|
+
case "default": style = .default
|
|
3503
|
+
default: style = nil
|
|
3504
|
+
}
|
|
3505
|
+
let hidden = props["hidden"] as? Bool
|
|
3506
|
+
if statusBarStyleOverride != style || statusBarHiddenOverride != hidden {
|
|
3507
|
+
statusBarStyleOverride = style
|
|
3508
|
+
statusBarHiddenOverride = hidden
|
|
3509
|
+
setNeedsStatusBarAppearanceUpdate()
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3224
3513
|
/// A bar button from `{ id, symbol | title }`. Its tap is the event
|
|
3225
3514
|
/// `navButton` with `{ id }`.
|
|
3226
3515
|
private func navigationButton(_ spec: [String: Any]) -> UIBarButtonItem? {
|
|
@@ -3417,10 +3706,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3417
3706
|
// effect, rather than over the bar's back button. Other screens stay
|
|
3418
3707
|
// inside the safe area.
|
|
3419
3708
|
let edgeToEdge = scroll != nil && (prefersLargeTitle == true || scroll === leading(renderedRoot))
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3709
|
+
let fullEdgeToEdge = rootIsSafeAreaView
|
|
3710
|
+
let topEdgeToEdge = fullEdgeToEdge || edgeToEdge
|
|
3711
|
+
rootTopToSafeArea?.isActive = !topEdgeToEdge
|
|
3712
|
+
rootTopToView?.isActive = topEdgeToEdge
|
|
3713
|
+
rootBottomToSafeArea?.isActive = !fullEdgeToEdge
|
|
3714
|
+
rootBottomToView?.isActive = fullEdgeToEdge
|
|
3715
|
+
rootLeadingToSafeArea?.isActive = !fullEdgeToEdge
|
|
3716
|
+
rootLeadingToView?.isActive = fullEdgeToEdge
|
|
3717
|
+
rootTrailingToSafeArea?.isActive = !fullEdgeToEdge
|
|
3718
|
+
rootTrailingToView?.isActive = fullEdgeToEdge
|
|
3424
3719
|
scroll?.contentInsetAdjustmentBehavior = edgeToEdge ? .automatic : .never
|
|
3425
3720
|
guard scroll !== trackedScrollView else { return }
|
|
3426
3721
|
trackedScrollView = scroll
|
|
@@ -3898,6 +4193,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3898
4193
|
|
|
3899
4194
|
private func renderCommitted(_ document: [String: Any]) {
|
|
3900
4195
|
let focused = firstResponder(in: rootStack) ?? lastFocusedInput
|
|
4196
|
+
statusBarStyleOverride = nil
|
|
4197
|
+
statusBarHiddenOverride = nil
|
|
3901
4198
|
let previous = renderedRoot
|
|
3902
4199
|
let next = reconcile(document, identity: "root", path: "root", previous: previous)
|
|
3903
4200
|
if previous?.view !== next.view {
|
|
@@ -3920,15 +4217,29 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3920
4217
|
rootStack.setLayoutStyle(rootStyle, for: next.view)
|
|
3921
4218
|
rootStack.invalidateMeasurements()
|
|
3922
4219
|
renderedRoot = next
|
|
4220
|
+
rootIsSafeAreaView = next.type == "SafeAreaView"
|
|
3923
4221
|
updateKeyboardAvoidingViews()
|
|
3924
4222
|
syncGlobalKeyboardSafeArea()
|
|
3925
4223
|
trackContentScrollView()
|
|
4224
|
+
setNeedsStatusBarAppearanceUpdate()
|
|
3926
4225
|
restoreFocus(focused)
|
|
3927
4226
|
}
|
|
3928
4227
|
|
|
3929
4228
|
private func clearRenderedTree() {
|
|
3930
4229
|
if let root = renderedRoot { detach(root, from: rootStack) }
|
|
3931
4230
|
renderedRoot = nil
|
|
4231
|
+
rootIsSafeAreaView = false
|
|
4232
|
+
rootTopToSafeArea?.isActive = true
|
|
4233
|
+
rootTopToView?.isActive = false
|
|
4234
|
+
rootBottomToSafeArea?.isActive = true
|
|
4235
|
+
rootBottomToView?.isActive = false
|
|
4236
|
+
rootLeadingToSafeArea?.isActive = true
|
|
4237
|
+
rootLeadingToView?.isActive = false
|
|
4238
|
+
rootTrailingToSafeArea?.isActive = true
|
|
4239
|
+
rootTrailingToView?.isActive = false
|
|
4240
|
+
statusBarStyleOverride = nil
|
|
4241
|
+
statusBarHiddenOverride = nil
|
|
4242
|
+
setNeedsStatusBarAppearanceUpdate()
|
|
3932
4243
|
syncGlobalKeyboardSafeArea()
|
|
3933
4244
|
}
|
|
3934
4245
|
|
|
@@ -3963,14 +4274,32 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3963
4274
|
let result = current.view
|
|
3964
4275
|
current.protocolId = node["id"] as? String
|
|
3965
4276
|
current.style = style
|
|
4277
|
+
let linkTarget = ["href", "to"].lazy.compactMap { props[$0] as? String }
|
|
4278
|
+
.first(where: { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty })?
|
|
4279
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
4280
|
+
if ["Link", "TouchableOpacity", "TouchableHighlight", "Pressable"].contains(type),
|
|
4281
|
+
props["disabled"] as? Bool != true, let linkTarget {
|
|
4282
|
+
linkTargets[ObjectIdentifier(result)] = linkTarget
|
|
4283
|
+
} else {
|
|
4284
|
+
linkTargets.removeValue(forKey: ObjectIdentifier(result))
|
|
4285
|
+
}
|
|
3966
4286
|
result.accessibilityIdentifier = accessibilityIdentifier(node, props: props) ?? path
|
|
3967
4287
|
if type != "View" && type != "SafeAreaView" && type != "KeyboardAvoidingView" && type != "ScrollView" && type != "SectionList" && type != "FlatList" && type != "Modal" {
|
|
3968
4288
|
result.setContentHuggingPriority(.required, for: .vertical)
|
|
3969
4289
|
}
|
|
3970
4290
|
applyViewStyle(style, to: result, node: current)
|
|
4291
|
+
if type == "SafeAreaView", let safeArea = result as? CraftNativeSafeAreaView {
|
|
4292
|
+
safeArea.setSafeAreaEdges(props["edges"])
|
|
4293
|
+
}
|
|
3971
4294
|
updateLayoutHandler(events["onLayout"], for: result)
|
|
4295
|
+
if type == "StatusBar" { applyStatusBar(props) }
|
|
3972
4296
|
|
|
3973
4297
|
switch type {
|
|
4298
|
+
case "StatusBar", "RefreshControl":
|
|
4299
|
+
// StatusBar is a screen trait, and RefreshControl is consumed by
|
|
4300
|
+
// its nearest ScrollView. Neither contributes a visible child.
|
|
4301
|
+
result.isHidden = true
|
|
4302
|
+
result.isUserInteractionEnabled = false
|
|
3974
4303
|
case "Text":
|
|
3975
4304
|
let label = result as! UILabel
|
|
3976
4305
|
label.numberOfLines = max(0, (props["numberOfLines"] as? NSNumber)?.intValue ?? 0)
|
|
@@ -4108,11 +4437,21 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4108
4437
|
case "DatePicker":
|
|
4109
4438
|
let picker = result as! UIDatePicker
|
|
4110
4439
|
let mode = Self.datePickerMode(props["mode"])
|
|
4111
|
-
|
|
4112
|
-
|
|
4440
|
+
// A countdown is wheels only: UIKit asserts (and the app aborts)
|
|
4441
|
+
// on a countdown in the compact or inline style, so the style is
|
|
4442
|
+
// settled before the mode becomes a countdown, and after it
|
|
4443
|
+
// stops being one.
|
|
4444
|
+
let display: UIDatePickerStyle = mode == .countDownTimer ? .wheels
|
|
4445
|
+
: props["display"] as? String == "inline" ? .inline
|
|
4113
4446
|
: props["display"] as? String == "wheels" || props["display"] as? String == "spinner" ? .wheels
|
|
4114
4447
|
: .compact
|
|
4115
|
-
if
|
|
4448
|
+
if mode == .countDownTimer {
|
|
4449
|
+
if picker.preferredDatePickerStyle != display { picker.preferredDatePickerStyle = display }
|
|
4450
|
+
if picker.datePickerMode != mode { picker.datePickerMode = mode }
|
|
4451
|
+
} else {
|
|
4452
|
+
if picker.datePickerMode != mode { picker.datePickerMode = mode }
|
|
4453
|
+
if picker.preferredDatePickerStyle != display { picker.preferredDatePickerStyle = display }
|
|
4454
|
+
}
|
|
4116
4455
|
// UIKit takes an interval that divides an hour, and ignores others.
|
|
4117
4456
|
let interval = (props["minuteInterval"] as? NSNumber)?.intValue ?? 1
|
|
4118
4457
|
picker.minuteInterval = (1...30).contains(interval) && 60 % interval == 0 ? interval : 1
|
|
@@ -4242,7 +4581,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4242
4581
|
updateAuxiliaryHandler(events["onLoad"], in: &imageLoadHandlers, for: image)
|
|
4243
4582
|
updateAuxiliaryHandler(events["onLoadEnd"], in: &imageLoadEndHandlers, for: image)
|
|
4244
4583
|
updateAuxiliaryHandler(events["onError"], in: &imageErrorHandlers, for: image)
|
|
4245
|
-
updateImage(image, source: props["source"])
|
|
4584
|
+
updateImage(image, source: props["source"], defaultSource: props["defaultSource"])
|
|
4246
4585
|
case "ImageBackground":
|
|
4247
4586
|
let background = result as! CraftNativeImageBackgroundView
|
|
4248
4587
|
let requestedResizeMode = style["resizeMode"] as? String ?? props["resizeMode"] as? String
|
|
@@ -4252,7 +4591,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4252
4591
|
updateAuxiliaryHandler(events["onLoad"], in: &imageLoadHandlers, for: background)
|
|
4253
4592
|
updateAuxiliaryHandler(events["onLoadEnd"], in: &imageLoadEndHandlers, for: background)
|
|
4254
4593
|
updateAuxiliaryHandler(events["onError"], in: &imageErrorHandlers, for: background)
|
|
4255
|
-
updateImage(background, source: props["source"])
|
|
4594
|
+
updateImage(background, source: props["source"], defaultSource: props["defaultSource"])
|
|
4256
4595
|
configureStack(background.contentStack, style: style)
|
|
4257
4596
|
reconcileChildren(children, in: background.contentStack, parent: current, path: path, style: style)
|
|
4258
4597
|
case "KeyboardAvoidingView":
|
|
@@ -4276,6 +4615,15 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4276
4615
|
controls: (props["controls"] as? Bool) == true,
|
|
4277
4616
|
resizeMode: style["resizeMode"] as? String ?? props["resizeMode"] as? String
|
|
4278
4617
|
)
|
|
4618
|
+
case "Map":
|
|
4619
|
+
let map = result as! CraftNativeMapView
|
|
4620
|
+
map.configure(props: props, children: children) { [weak self, weak map] markerID in
|
|
4621
|
+
guard let self, let map, let handler = map.markerHandlers[markerID], !handler.isEmpty else { return }
|
|
4622
|
+
self.send(type: "EVENT", payload: [
|
|
4623
|
+
"handlerName": handler,
|
|
4624
|
+
"nativeEvent": ["id": markerID],
|
|
4625
|
+
])
|
|
4626
|
+
}
|
|
4279
4627
|
case "ScrollView", "SectionList":
|
|
4280
4628
|
let scroll = result as! CraftNativeScrollView
|
|
4281
4629
|
let flexDirection = style["flexDirection"] as? String
|
|
@@ -4301,15 +4649,26 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4301
4649
|
updateAuxiliaryHandler(events["onScrollEndDrag"], in: &scrollEndHandlers, for: scroll)
|
|
4302
4650
|
updateAuxiliaryHandler(events["onMomentumScrollBegin"], in: &scrollMomentumBeginHandlers, for: scroll)
|
|
4303
4651
|
updateAuxiliaryHandler(events["onMomentumScrollEnd"], in: &scrollMomentumEndHandlers, for: scroll)
|
|
4304
|
-
|
|
4652
|
+
let refreshNode = children.compactMap { $0 as? [String: Any] }.first {
|
|
4653
|
+
canonicalType($0["type"] as? String ?? "") == "RefreshControl"
|
|
4654
|
+
}
|
|
4655
|
+
let refreshProps = refreshNode?["props"] as? [String: Any] ?? [:]
|
|
4656
|
+
let refreshEvents = refreshNode?["events"] as? [String: String] ?? [:]
|
|
4657
|
+
let refreshHandlerName = nonEmptyHandler(refreshEvents["onRefresh"])
|
|
4658
|
+
?? nonEmptyHandler(events["onRefresh"])
|
|
4659
|
+
scroll.setRefreshHandler(refreshHandlerName.map { [weak self] handler in
|
|
4305
4660
|
{ self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
4306
|
-
}, refreshing: props["refreshing"] as? Bool == true)
|
|
4661
|
+
}, refreshing: (refreshNode == nil ? props["refreshing"] : refreshProps["refreshing"]) as? Bool == true)
|
|
4662
|
+
scroll.setRefreshAppearance(refreshProps)
|
|
4307
4663
|
var contentStyle = style
|
|
4308
4664
|
if let override = props["contentContainerStyle"] as? [String: Any] {
|
|
4309
4665
|
contentStyle.merge(override) { _, next in next }
|
|
4310
4666
|
}
|
|
4311
4667
|
configureStack(scroll.contentStack, style: contentStyle)
|
|
4312
|
-
reconcileChildren(children
|
|
4668
|
+
reconcileChildren(children.filter {
|
|
4669
|
+
guard let child = $0 as? [String: Any] else { return true }
|
|
4670
|
+
return canonicalType(child["type"] as? String ?? "") != "RefreshControl"
|
|
4671
|
+
}, in: scroll.contentStack, parent: current, path: path, style: contentStyle)
|
|
4313
4672
|
case "FlatList":
|
|
4314
4673
|
let list = result as! CraftNativeFlatList
|
|
4315
4674
|
reconcileFlatList(
|
|
@@ -4326,7 +4685,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4326
4685
|
reconcileChildren(children, in: stack, parent: current, path: path, style: style)
|
|
4327
4686
|
}
|
|
4328
4687
|
updateLongPressHandler(props["disabled"] as? Bool == true ? nil : events["onLongPress"], for: result)
|
|
4329
|
-
if type != "Button" && type != "Link" && type != "Checkbox" && type != "TextInput" && type != "Picker" && type != "DatePicker" && type != "Modal" && type != "Switch" && type != "Slider" && type != "ActivityIndicator" {
|
|
4688
|
+
if type != "Button" && type != "Link" && type != "Checkbox" && type != "TextInput" && type != "Picker" && type != "DatePicker" && type != "Modal" && type != "Switch" && type != "Slider" && type != "ActivityIndicator" && type != "StatusBar" && type != "RefreshControl" && type != "Map" {
|
|
4330
4689
|
let disabled = props["disabled"] as? Bool == true
|
|
4331
4690
|
updatePressHandler(disabled ? nil : nonEmptyHandler(events["onPress"]) ?? nonEmptyHandler(events["onClick"]), for: result)
|
|
4332
4691
|
updatePressFeedback(
|
|
@@ -4391,6 +4750,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4391
4750
|
return slider
|
|
4392
4751
|
case "ActivityIndicator":
|
|
4393
4752
|
return UIActivityIndicatorView(style: .medium)
|
|
4753
|
+
case "StatusBar", "RefreshControl":
|
|
4754
|
+
return CraftNativeFlowView()
|
|
4394
4755
|
case "Image":
|
|
4395
4756
|
return UIImageView()
|
|
4396
4757
|
case "ImageBackground":
|
|
@@ -4399,6 +4760,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4399
4760
|
return CraftNativeKeyboardAvoidingView()
|
|
4400
4761
|
case "Video":
|
|
4401
4762
|
return CraftNativeVideoView(host: self)
|
|
4763
|
+
case "Map":
|
|
4764
|
+
return CraftNativeMapView()
|
|
4402
4765
|
case "Icon":
|
|
4403
4766
|
let icon = CraftNativeIconView()
|
|
4404
4767
|
icon.contentMode = .scaleAspectFit
|
|
@@ -4409,8 +4772,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4409
4772
|
return CraftNativeFlatList()
|
|
4410
4773
|
case "Svg":
|
|
4411
4774
|
return CraftNativeSvgView()
|
|
4412
|
-
case "View"
|
|
4775
|
+
case "View":
|
|
4413
4776
|
return CraftNativeFlowView()
|
|
4777
|
+
case "SafeAreaView":
|
|
4778
|
+
return CraftNativeSafeAreaView()
|
|
4414
4779
|
case "TouchableOpacity", "TouchableHighlight", "Pressable":
|
|
4415
4780
|
return CraftNativeFlowView()
|
|
4416
4781
|
default:
|
|
@@ -4433,9 +4798,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4433
4798
|
case "switch": return "Switch"
|
|
4434
4799
|
case "slider": return "Slider"
|
|
4435
4800
|
case "activityindicator": return "ActivityIndicator"
|
|
4801
|
+
case "statusbar": return "StatusBar"
|
|
4802
|
+
case "refreshcontrol": return "RefreshControl"
|
|
4436
4803
|
case "image": return "Image"
|
|
4437
4804
|
case "imagebackground": return "ImageBackground"
|
|
4438
4805
|
case "video": return "Video"
|
|
4806
|
+
case "map": return "Map"
|
|
4439
4807
|
case "icon": return "Icon"
|
|
4440
4808
|
case "scrollview": return "ScrollView"
|
|
4441
4809
|
case "sectionlist": return "SectionList"
|
|
@@ -4640,6 +5008,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4640
5008
|
let id = ObjectIdentifier(node.view)
|
|
4641
5009
|
if let modal = node.view as? CraftNativeModalView { modal.resetForTeardown() }
|
|
4642
5010
|
handlers.removeValue(forKey: id)
|
|
5011
|
+
linkTargets.removeValue(forKey: id)
|
|
4643
5012
|
pressInHandlers.removeValue(forKey: id)
|
|
4644
5013
|
pressOutHandlers.removeValue(forKey: id)
|
|
4645
5014
|
layoutHandlers.removeValue(forKey: id)
|
|
@@ -4754,7 +5123,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4754
5123
|
let id = ObjectIdentifier(view)
|
|
4755
5124
|
let handler = handler?.isEmpty == false ? handler : nil
|
|
4756
5125
|
updateHandler(handler, for: view)
|
|
4757
|
-
|
|
5126
|
+
let actionable = handler != nil || linkTargets[id] != nil
|
|
5127
|
+
if actionable, tapRecognizers[id] == nil {
|
|
4758
5128
|
let recognizer = UITapGestureRecognizer(target: self, action: #selector(viewPressed(_:)))
|
|
4759
5129
|
tapRecognizers[id] = recognizer
|
|
4760
5130
|
view.addGestureRecognizer(recognizer)
|
|
@@ -4763,7 +5133,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4763
5133
|
}
|
|
4764
5134
|
// A container keeps receiving touches without a handler of its own,
|
|
4765
5135
|
// or every control inside it (and the screen root) would be dead.
|
|
4766
|
-
if !(view is UIScrollView) { view.isUserInteractionEnabled =
|
|
5136
|
+
if !(view is UIScrollView) { view.isUserInteractionEnabled = actionable || longPressHandlers[id] != nil || view is CraftNativeFlowView }
|
|
4767
5137
|
}
|
|
4768
5138
|
|
|
4769
5139
|
private func updatePressFeedback(_ pressIn: String?, _ pressOut: String?, for view: UIView) {
|
|
@@ -4772,7 +5142,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4772
5142
|
if let pressOut, !pressOut.isEmpty { pressOutHandlers[id] = pressOut } else { pressOutHandlers.removeValue(forKey: id) }
|
|
4773
5143
|
guard pressInHandlers[id] != nil || pressOutHandlers[id] != nil else {
|
|
4774
5144
|
if let recognizer = pressFeedbackRecognizers.removeValue(forKey: id) { view.removeGestureRecognizer(recognizer) }
|
|
4775
|
-
if !(view is UIScrollView) { view.isUserInteractionEnabled = handlers[id] != nil || longPressHandlers[id] != nil || view is CraftNativeFlowView }
|
|
5145
|
+
if !(view is UIScrollView) { view.isUserInteractionEnabled = handlers[id] != nil || linkTargets[id] != nil || longPressHandlers[id] != nil || view is CraftNativeFlowView }
|
|
4776
5146
|
return
|
|
4777
5147
|
}
|
|
4778
5148
|
if pressFeedbackRecognizers[id] == nil {
|
|
@@ -4801,7 +5171,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4801
5171
|
if let recognizer = longPressRecognizers.removeValue(forKey: id) {
|
|
4802
5172
|
view.removeGestureRecognizer(recognizer)
|
|
4803
5173
|
}
|
|
4804
|
-
if !(view is UIScrollView) { view.isUserInteractionEnabled = handlers[id] != nil || view is CraftNativeFlowView }
|
|
5174
|
+
if !(view is UIScrollView) { view.isUserInteractionEnabled = handlers[id] != nil || linkTargets[id] != nil || view is CraftNativeFlowView }
|
|
4805
5175
|
}
|
|
4806
5176
|
}
|
|
4807
5177
|
|
|
@@ -5270,7 +5640,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5270
5640
|
return ["localhost", "127.0.0.1", "::1"].contains(bare) || bare.hasSuffix(".localhost")
|
|
5271
5641
|
}
|
|
5272
5642
|
|
|
5273
|
-
private func updateImage(_ view: UIImageView, source: Any?) {
|
|
5643
|
+
private func updateImage(_ view: UIImageView, source: Any?, defaultSource: Any? = nil) {
|
|
5274
5644
|
let uri = (source as? String) ?? (source as? [String: Any])?["uri"] as? String
|
|
5275
5645
|
let id = ObjectIdentifier(view)
|
|
5276
5646
|
guard let uri, !uri.isEmpty else {
|
|
@@ -5284,7 +5654,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5284
5654
|
imageTasks.removeValue(forKey: id)?.cancel()
|
|
5285
5655
|
imageSources[id] = uri
|
|
5286
5656
|
imageErrors.removeValue(forKey: id)
|
|
5287
|
-
view.image =
|
|
5657
|
+
view.image = immediateImage(defaultSource).map { imageForDisplay($0, view: view) }
|
|
5288
5658
|
view.accessibilityValue = nil
|
|
5289
5659
|
emitImageEvent(view, handler: imageLoadStartHandlers[id])
|
|
5290
5660
|
|
|
@@ -5332,6 +5702,20 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5332
5702
|
imageFailure(view, uri: uri, message: "Unsupported image source")
|
|
5333
5703
|
}
|
|
5334
5704
|
|
|
5705
|
+
/// `defaultSource` is intentionally local-only: data and bundled assets
|
|
5706
|
+
/// can be shown before a network image arrives without starting another
|
|
5707
|
+
/// request or bypassing the source's URL policy.
|
|
5708
|
+
private func immediateImage(_ source: Any?) -> UIImage? {
|
|
5709
|
+
let uri = (source as? String) ?? (source as? [String: Any])?["uri"] as? String
|
|
5710
|
+
guard let uri, !uri.isEmpty else { return nil }
|
|
5711
|
+
if uri.hasPrefix("data:image/"),
|
|
5712
|
+
let comma = uri.firstIndex(of: ","),
|
|
5713
|
+
let data = Data(base64Encoded: String(uri[uri.index(after: comma)...])) {
|
|
5714
|
+
return UIImage(data: data)
|
|
5715
|
+
}
|
|
5716
|
+
return !uri.contains(":") ? UIImage(named: uri) : nil
|
|
5717
|
+
}
|
|
5718
|
+
|
|
5335
5719
|
private func imageFailure(_ view: UIImageView, uri: String?, message: String) {
|
|
5336
5720
|
imageErrors[ObjectIdentifier(view)] = message
|
|
5337
5721
|
view.image = nil
|
|
@@ -5425,6 +5809,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5425
5809
|
|
|
5426
5810
|
@objc private func buttonPressed(_ sender: UIButton) {
|
|
5427
5811
|
let focused = firstResponder(in: rootStack) ?? lastFocusedInput
|
|
5812
|
+
if let target = linkTargets[ObjectIdentifier(sender)] {
|
|
5813
|
+
hybridEvents?.nativeScreen(self, open: target)
|
|
5814
|
+
return
|
|
5815
|
+
}
|
|
5428
5816
|
guard let handler = handlers[ObjectIdentifier(sender)] else { return }
|
|
5429
5817
|
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]])
|
|
5430
5818
|
restoreFocus(focused)
|
|
@@ -5534,7 +5922,13 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5534
5922
|
let value = snappedSliderValue(sender.value, for: sender)
|
|
5535
5923
|
if sender.value != value { sender.value = value }
|
|
5536
5924
|
guard let handler = handlers[ObjectIdentifier(sender)] else { return }
|
|
5537
|
-
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["value": value]])
|
|
5925
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["value": Self.sliderEventValue(value)]])
|
|
5926
|
+
}
|
|
5927
|
+
|
|
5928
|
+
/// A slider's value as the script reads it: the shortest decimal that is
|
|
5929
|
+
/// the same Float, so a step of 0.1 gives 0.8 and not 0.800000011920929.
|
|
5930
|
+
static func sliderEventValue(_ value: Float) -> Double {
|
|
5931
|
+
Double(value.description) ?? Double(value)
|
|
5538
5932
|
}
|
|
5539
5933
|
|
|
5540
5934
|
/// `@change` on a DatePicker: the value as `<input type="date|time|
|
|
@@ -5631,7 +6025,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5631
6025
|
|
|
5632
6026
|
@objc private func sliderFinished(_ sender: UISlider) {
|
|
5633
6027
|
guard let handler = sliderCompleteHandlers[ObjectIdentifier(sender)] else { return }
|
|
5634
|
-
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["value": snappedSliderValue(sender.value, for: sender)]])
|
|
6028
|
+
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["value": Self.sliderEventValue(snappedSliderValue(sender.value, for: sender))]])
|
|
5635
6029
|
}
|
|
5636
6030
|
|
|
5637
6031
|
func numberOfComponents(in pickerView: UIPickerView) -> Int { 1 }
|
|
@@ -5681,8 +6075,13 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5681
6075
|
}
|
|
5682
6076
|
|
|
5683
6077
|
@objc private func viewPressed(_ sender: UITapGestureRecognizer) {
|
|
5684
|
-
guard let view = sender.view
|
|
6078
|
+
guard let view = sender.view else { return }
|
|
5685
6079
|
let id = ObjectIdentifier(view)
|
|
6080
|
+
if let target = linkTargets[id] {
|
|
6081
|
+
hybridEvents?.nativeScreen(self, open: target)
|
|
6082
|
+
return
|
|
6083
|
+
}
|
|
6084
|
+
guard let handler = handlers[id] else { return }
|
|
5686
6085
|
if pressInHandlers[id] == nil, pressOutHandlers[id] == nil,
|
|
5687
6086
|
let pressable = view as? CraftNativeFlowView, let activeOpacity = pressable.pressActiveOpacity {
|
|
5688
6087
|
UIView.animate(withDuration: 0.1, animations: { pressable.alpha = pressable.pressBaseOpacity * activeOpacity }) { _ in
|