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/src/index.test.tsmodified+1-1
Changes to packages/ios/src/index.test.ts
@@ -1709,7 +1709,7 @@ describe('Craft iOS builder', () => {
17091709 // A screen can ask the tab bar away while it shows; the next one says again.
17101710 expect(screen).toContain('if let hidden = options["tabBarHidden"] as? Bool {')
17111711 expect(screen).toContain('CraftChrome.shared.setHiddenByScreen(tabBarHidden)')
1712 expect(screen).toContain('CraftChrome.shared.updateTabAccessory(nil)')
1712 expect(screen).toContain('CraftChrome.shared.clearTabAccessory(ownedBy: self)')
17131713 expect(screen).toContain('if let contentSwipeBack = options["contentSwipeBack"] as? Bool {')
17141714 expect(screen).toContain('private(set) var contentSwipeBack = true')
17151715 expect(screen).toContain('if let tabSwipeEnabled = options["tabSwipeEnabled"] as? Bool {')
packages/ios/templates/CraftApp.swiftmodified+9-1
Changes to packages/ios/templates/CraftApp.swift
@@ -670,6 +670,7 @@ final class CraftChrome: ObservableObject {
670670
671671 @Published var tabs: [Tab] = []
672672 @Published var tabAccessory: Tab?
673 private weak var tabAccessoryOwner: AnyObject?
673674 @Published var selected: String?
674675 let tabSwipeProgress = CraftTabSwipeProgress()
675676 private(set) var tabSwipePosition: CGFloat? {
@@ -771,7 +772,7 @@ final class CraftChrome: ObservableObject {
771772 tabs[index] = next
772773 }
773774 /// 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) {
775776 let next: Tab?
776777 if let item, let path = item["path"] as? String,
777778 path.hasPrefix("/"), !path.hasPrefix("//"),
@@ -782,10 +783,17 @@ final class CraftChrome: ObservableObject {
782783 } else {
783784 next = nil
784785 }
786 tabAccessoryOwner = owner
785787 guard tabAccessory != next else { return }
786788 tabAccessory = next
787789 }
788790
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
789797 func setHiddenByScreen(_ hidden: Bool) {
790798 guard hiddenByScreen != hidden else { return }
791799 hiddenByScreen = hidden
packages/ios/templates/CraftNativeScreen.swiftmodified+5-6
Changes to packages/ios/templates/CraftNativeScreen.swift
@@ -4633,10 +4633,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
46334633 super.viewWillDisappear(animated)
46344634 screenVisibilityChanged(false)
46354635 stopCelebrations(in: view)
4636 // The accessory belongs to the visible native screen. Clear the
4637 // shared shell state before a hybrid/web route appears, otherwise a
4638 // destination from the outgoing screen remains tappable on top of it.
4639 CraftChrome.shared.updateTabAccessory(nil)
4636 // Release only this screen's accessory. A retained or prepared screen
4637 // can disappear after the destination has already published its own.
4638 CraftChrome.shared.clearTabAccessory(ownedBy: self)
46404639 // Popped (or its stack dismissed) while the bar was away: it comes
46414640 // back for whatever shows next, a web page included.
46424641 if tabBarHidden, isMovingFromParent || isBeingDismissed || navigationController?.isBeingDismissed == true {
@@ -4677,7 +4676,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
46774676 private func restoreShellChrome() {
46784677 guard !isAppScript else { return }
46794678 CraftChrome.shared.setHiddenByScreen(tabBarHidden)
4680 CraftChrome.shared.updateTabAccessory(tabAccessory)
4679 CraftChrome.shared.updateTabAccessory(tabAccessory, owner: self)
46814680 }
46824681
46834682 /// The screen came on screen or went off it: a screen pushed over it was
@@ -5348,7 +5347,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
53485347 if options.keys.contains("tabAccessory") {
53495348 tabAccessory = options["tabAccessory"] as? [String: Any]
53505349 if navigationController?.topViewController === self, view.window != nil {
5351 CraftChrome.shared.updateTabAccessory(tabAccessory)
5350 CraftChrome.shared.updateTabAccessory(tabAccessory, owner: self)
53525351 }
53535352 }
53545353 if options.keys.contains("title") {
scripts/native-renderer-contract.test.tsmodified+1-1
Changes to scripts/native-renderer-contract.test.ts
@@ -2924,7 +2924,7 @@ describe('native renderer component contract', () => {
29242924 expect(iosNavigationTest).toContain('craft-tab-accessory')
29252925 expect(androidNavigationTest).toContain('pushedScreenTabAccessoryOpensNativeRouteAndClearsOnBack')
29262926 expect(androidNavigationTest).toContain('Open capabilities')
2927 expect(ios).toContain('CraftChrome.shared.updateTabAccessory(nil)')
2927 expect(ios).toContain('CraftChrome.shared.clearTabAccessory(ownedBy: self)')
29282928 expect(androidShell).toContain('accessorySpec?.optString("path")?.let { onAccessory?.invoke(it) }')
29292929 expect(guide).toContain('`tabAccessory` adds one destination button beside the tab pill')
29302930 expect(iosReadme).toContain('`tabAccessory` destination beside the tab pill')