also looking at this
fix(native): retain keyed controls and correct map and tab chrome
#368
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.
| @@ -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 { |
| @@ -6740,7 +6766,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate, | ||
| 6740 | 6766 | |
| 6741 | 6767 | private func explicitKey(_ node: [String: Any], props: [String: Any]) -> String? { |
| 6742 | 6768 | 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 } | |
| 6744 | 6774 | } |
| 6745 | 6775 | |
| 6746 | 6776 | /// The key a node's author gave it. A wrapper of one child (a |
| @@ -6752,7 +6782,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate, | ||
| 6752 | 6782 | private func authoredKey(_ node: [String: Any], props: [String: Any]) -> String? { |
| 6753 | 6783 | // The current @stacksjs/stx native compiler emits key in props; accept the IR |
| 6754 | 6784 | // 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"]] | |
| 6756 | 6786 | .compactMap { $0 as? String } |
| 6757 | 6787 | .first { !$0.isEmpty } |
| 6758 | 6788 | if let direct { return direct } |