ReviewOS

also looking at this

craft-native/craft

fix(native): retain keyed controls and correct map and tab chrome

#368
Merged chris wants to merge codex/native-repeated-selector-identity into main
32 files +449 -100

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/ios/templates/CraftNativeScreen.swiftmodified+41-11
Changes to packages/ios/templates/CraftNativeScreen.swift
@@ -1435,21 +1435,47 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
14351435
14361436 private func configureMarkerView(_ view: MKAnnotationView, id: String) {
14371437 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)
14441441 view.transform = CGAffineTransform(rotationAngle: CGFloat((bearing - camera.heading) * .pi / 180))
14451442 } 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)
14491446 view.transform = .identity
14501447 }
14511448 }
14521449
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
14531479 override func layoutSubviews() {
14541480 super.layoutSubviews()
14551481 if tileTemplate != nil {
@@ -6740,7 +6766,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
67406766
67416767 private func explicitKey(_ node: [String: Any], props: [String: Any]) -> String? {
67426768 if let authored = authoredKey(node, props: props) { return authored }
6743 return (node["id"] as? String).flatMap { $0.isEmpty ? nil : $0 }
6769 // Repeated test selectors are valid for list rows. Retain each row by
6770 // its unique protocol ID before considering legacy selector identity.
6771 return [node["id"], props["testID"]]
6772 .compactMap { $0 as? String }
6773 .first { !$0.isEmpty }
67446774 }
67456775
67466776 /// The key a node's author gave it. A wrapper of one child (a
@@ -6752,7 +6782,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
67526782 private func authoredKey(_ node: [String: Any], props: [String: Any]) -> String? {
67536783 // The current @stacksjs/stx native compiler emits key in props; accept the IR
67546784 // field too so keyed children keep working when the compiler adopts it.
6755 let direct = [node["key"], props["key"], props["testID"]]
6785 let direct = [node["key"], props["key"]]
67566786 .compactMap { $0 as? String }
67576787 .first { !$0.isEmpty }
67586788 if let direct { return direct }