@craft-native/ios 0.0.243 → 0.0.244

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-native/ios",
3
- "version": "0.0.243",
3
+ "version": "0.0.244",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/ios#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -670,6 +670,7 @@ final class CraftChrome: ObservableObject {
670
670
 
671
671
  @Published var tabs: [Tab] = []
672
672
  @Published var tabAccessory: Tab?
673
+ private weak var tabAccessoryOwner: AnyObject?
673
674
  @Published var selected: String?
674
675
  let tabSwipeProgress = CraftTabSwipeProgress()
675
676
  private(set) var tabSwipePosition: CGFloat? {
@@ -771,7 +772,7 @@ final class CraftChrome: ObservableObject {
771
772
  tabs[index] = next
772
773
  }
773
774
  /// A destination beside the tab pill, outside the tab stacks and swipe order.
774
- func updateTabAccessory(_ item: [String: Any]?) {
775
+ func updateTabAccessory(_ item: [String: Any]?, owner: AnyObject? = nil) {
775
776
  let next: Tab?
776
777
  if let item, let path = item["path"] as? String,
777
778
  path.hasPrefix("/"), !path.hasPrefix("//"),
@@ -782,10 +783,17 @@ final class CraftChrome: ObservableObject {
782
783
  } else {
783
784
  next = nil
784
785
  }
786
+ tabAccessoryOwner = owner
785
787
  guard tabAccessory != next else { return }
786
788
  tabAccessory = next
787
789
  }
788
790
 
791
+ /// A retained or prepared screen cannot clear another screen's chrome.
792
+ func clearTabAccessory(ownedBy owner: AnyObject) {
793
+ guard tabAccessoryOwner === owner else { return }
794
+ updateTabAccessory(nil)
795
+ }
796
+
789
797
  func setHiddenByScreen(_ hidden: Bool) {
790
798
  guard hiddenByScreen != hidden else { return }
791
799
  hiddenByScreen = hidden
@@ -29,6 +29,7 @@ struct CraftNativeMutationResult {
29
29
  final class CraftNativeMutationDocument {
30
30
  private struct Node {
31
31
  let id: String
32
+ let key: String?
32
33
  var type: String
33
34
  var props: [String: Any]
34
35
  var style: [String: Any]
@@ -177,6 +178,7 @@ final class CraftNativeMutationDocument {
177
178
  }
178
179
  nodes[id] = Node(
179
180
  id: id,
181
+ key: value["key"] as? String,
180
182
  type: type,
181
183
  props: dictionary(value["props"]),
182
184
  style: dictionary(value["style"]),
@@ -330,6 +332,7 @@ final class CraftNativeMutationDocument {
330
332
  "style": node.style,
331
333
  "events": node.events,
332
334
  ]
335
+ if let key = node.key { value["key"] = key }
333
336
  let text: [Any] = node.text
334
337
  let children: [Any] = node.children.compactMap { materialize($0, nodes: nodes) }
335
338
  value["children"] = text + children
@@ -350,6 +353,7 @@ final class CraftNativeMutationDocument {
350
353
  let children = value["children"] as? [Any] ?? []
351
354
  nodes[id] = Node(
352
355
  id: id,
356
+ key: value["key"] as? String,
353
357
  type: value["type"] as? String ?? "View",
354
358
  props: props,
355
359
  style: dictionary(value["style"]),
@@ -1435,21 +1435,47 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
1435
1435
 
1436
1436
  private func configureMarkerView(_ view: MKAnnotationView, id: String) {
1437
1437
  if let bearing = markerBearings[id] {
1438
- let configuration = UIImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
1439
- .applying(UIImage.SymbolConfiguration(paletteColors: [.white, .systemGreen]))
1440
- // MapKit otherwise tints this as a monochrome template and loses
1441
- // the contrasting route arrow when displayed over map content.
1442
- view.image = UIImage(systemName: "arrow.up.circle.fill", withConfiguration: configuration)?
1443
- .withRenderingMode(.alwaysOriginal)
1438
+ let color = (markerPinColors[id] ?? .systemGreen).resolvedColor(with: traitCollection)
1439
+ view.tintColor = color
1440
+ view.image = Self.markerImage(color: color, direction: true)
1444
1441
  view.transform = CGAffineTransform(rotationAngle: CGFloat((bearing - camera.heading) * .pi / 180))
1445
1442
  } else {
1446
- view.tintColor = markerPinColors[id] ?? .systemRed
1447
- view.image = UIImage(systemName: "mappin.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 28))?
1448
- .withTintColor(view.tintColor, renderingMode: .alwaysOriginal)
1443
+ let color = (markerPinColors[id] ?? .systemRed).resolvedColor(with: traitCollection)
1444
+ view.tintColor = color
1445
+ view.image = Self.markerImage(color: color, direction: false)
1449
1446
  view.transform = .identity
1450
1447
  }
1451
1448
  }
1452
1449
 
1450
+ private static let markerImages: NSCache<NSString, UIImage> = {
1451
+ let cache = NSCache<NSString, UIImage>()
1452
+ cache.countLimit = 64
1453
+ return cache
1454
+ }()
1455
+
1456
+ private static func markerImage(color: UIColor, direction: Bool) -> UIImage {
1457
+ let key = "\(direction ? "direction" : "pin"):\(color.description)" as NSString
1458
+ if let cached = markerImages.object(forKey: key) { return cached }
1459
+ let size = direction ? CGSize(width: 20, height: 24) : CGSize(width: 28, height: 28)
1460
+ // Explicit native drawing preserves the light backing independently
1461
+ // of MapKit's tint and SF Symbols' multicolor rendering preferences.
1462
+ let image = UIGraphicsImageRenderer(size: size).image { _ in
1463
+ let border = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size).insetBy(dx: 1, dy: 1),
1464
+ cornerRadius: (size.width - 2) / 2)
1465
+ UIColor.white.setFill()
1466
+ border.fill()
1467
+ color.setStroke()
1468
+ border.lineWidth = 1.5
1469
+ border.stroke()
1470
+ let symbol = UIImage(systemName: direction ? "arrow.up" : "mappin",
1471
+ withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .semibold))?
1472
+ .withTintColor(color, renderingMode: .alwaysOriginal)
1473
+ symbol?.draw(in: direction ? CGRect(x: 5, y: 6, width: 10, height: 12) : CGRect(x: 7, y: 5, width: 14, height: 18))
1474
+ }.withRenderingMode(.alwaysOriginal)
1475
+ markerImages.setObject(image, forKey: key)
1476
+ return image
1477
+ }
1478
+
1453
1479
  override func layoutSubviews() {
1454
1480
  super.layoutSubviews()
1455
1481
  if tileTemplate != nil {
@@ -4621,10 +4647,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
4621
4647
  super.viewWillDisappear(animated)
4622
4648
  screenVisibilityChanged(false)
4623
4649
  stopCelebrations(in: view)
4624
- // The accessory belongs to the visible native screen. Clear the
4625
- // shared shell state before a hybrid/web route appears, otherwise a
4626
- // destination from the outgoing screen remains tappable on top of it.
4627
- CraftChrome.shared.updateTabAccessory(nil)
4650
+ // Release only this screen's accessory. A retained or prepared screen
4651
+ // can disappear after the destination has already published its own.
4652
+ CraftChrome.shared.clearTabAccessory(ownedBy: self)
4628
4653
  // Popped (or its stack dismissed) while the bar was away: it comes
4629
4654
  // back for whatever shows next, a web page included.
4630
4655
  if tabBarHidden, isMovingFromParent || isBeingDismissed || navigationController?.isBeingDismissed == true {
@@ -4669,7 +4694,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
4669
4694
  private func restoreShellChrome() {
4670
4695
  guard !isAppScript else { return }
4671
4696
  CraftChrome.shared.setHiddenByScreen(tabBarHidden)
4672
- CraftChrome.shared.updateTabAccessory(tabAccessory)
4697
+ CraftChrome.shared.updateTabAccessory(tabAccessory, owner: self)
4673
4698
  }
4674
4699
 
4675
4700
  /// The screen came on screen or went off it: a screen pushed over it was
@@ -5340,7 +5365,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5340
5365
  if options.keys.contains("tabAccessory") {
5341
5366
  tabAccessory = options["tabAccessory"] as? [String: Any]
5342
5367
  if navigationController?.topViewController === self, view.window != nil {
5343
- CraftChrome.shared.updateTabAccessory(tabAccessory)
5368
+ CraftChrome.shared.updateTabAccessory(tabAccessory, owner: self)
5344
5369
  }
5345
5370
  }
5346
5371
  if options.keys.contains("title") {
@@ -6865,7 +6890,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6865
6890
 
6866
6891
  private func explicitKey(_ node: [String: Any], props: [String: Any]) -> String? {
6867
6892
  if let authored = authoredKey(node, props: props) { return authored }
6868
- return (node["id"] as? String).flatMap { $0.isEmpty ? nil : $0 }
6893
+ // Repeated test selectors are valid for list rows. Retain each row by
6894
+ // its unique protocol ID before considering legacy selector identity.
6895
+ return [node["id"], props["testID"]]
6896
+ .compactMap { $0 as? String }
6897
+ .first { !$0.isEmpty }
6869
6898
  }
6870
6899
 
6871
6900
  /// The key a node's author gave it. A wrapper of one child (a
@@ -6877,7 +6906,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6877
6906
  private func authoredKey(_ node: [String: Any], props: [String: Any]) -> String? {
6878
6907
  // The current @stacksjs/stx native compiler emits key in props; accept the IR
6879
6908
  // field too so keyed children keep working when the compiler adopts it.
6880
- let direct = [node["key"], props["key"], props["testID"]]
6909
+ let direct = [node["key"], props["key"]]
6881
6910
  .compactMap { $0 as? String }
6882
6911
  .first { !$0.isEmpty }
6883
6912
  if let direct { return direct }
@@ -448,7 +448,11 @@ final class DriverTests: XCTestCase {
448
448
  var found = element(containing: arg)
449
449
  var swipes = 0
450
450
  while (found == nil || !(found!.isHittable)) && swipes < 8 {
451
- app.swipeUp()
451
+ if let target = found, !target.frame.isEmpty, target.frame.midY < app.frame.midY {
452
+ app.swipeDown()
453
+ } else {
454
+ app.swipeUp()
455
+ }
452
456
  swipes += 1
453
457
  found = element(containing: arg)
454
458
  }