@craft-native/ios 0.0.201 → 0.0.202

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.201",
3
+ "version": "0.0.202",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/ios#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -792,10 +792,12 @@ final class CraftChrome: ObservableObject {
792
792
  withAnimation(nil) { selected = id }
793
793
  }
794
794
 
795
- func cancelTabSwipe() {
795
+ func cancelTabSwipe(restoringSelection: Bool = true) {
796
796
  guard let previous = tabSwipePrevious else { return }
797
797
  tabSwipePrevious = nil
798
- withAnimation(nil) { selected = previous }
798
+ if restoringSelection {
799
+ withAnimation(nil) { selected = previous }
800
+ }
799
801
  }
800
802
 
801
803
  func commitTabSwipe(to id: String) {
@@ -1023,6 +1023,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
1023
1023
  private var tabSwipeRecognizers: [UIGestureRecognizer] = []
1024
1024
  private var activeTabSwipeTarget: String?
1025
1025
  private var tabSwipeSettling = false
1026
+ private var tabSwipeGeneration: UInt = 0
1026
1027
  /// The edge the incoming tab started from for the current gesture. Keep
1027
1028
  /// this separate from the live frame: deriving it after the drag begins
1028
1029
  /// makes the target jump when the translation crosses the midpoint.
@@ -1195,10 +1196,13 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
1195
1196
  let end = completed ? 0 : direction * width
1196
1197
  let settlingDuration = completed ? 0.22 : 0.18
1197
1198
  let duration = CraftChrome.motion(.easeOut(duration: settlingDuration)) == nil ? 0 : settlingDuration
1199
+ let generation = tabSwipeGeneration
1198
1200
  UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseOut, .beginFromCurrentState], animations: {
1199
1201
  target.view.frame.origin.x = end
1200
1202
  current.view.frame.origin.x = completed ? -direction * width : 0
1201
1203
  }, completion: { _ in
1204
+ guard self.tabSwipeGeneration == generation,
1205
+ self.activeTabSwipeTarget == targetId else { return }
1202
1206
  if completed {
1203
1207
  current.view.isHidden = true
1204
1208
  current.view.frame = self.view.bounds
@@ -1216,6 +1220,29 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
1216
1220
  })
1217
1221
  }
1218
1222
 
1223
+ /// A newer navigation action wins immediately, including a tap back on
1224
+ /// the outgoing tab. Invalidate the old animation before removing it:
1225
+ /// UIKit may deliver its completion while the replacement is starting.
1226
+ private func interruptTabSwipe(restoringSelection: Bool) {
1227
+ guard let targetId = activeTabSwipeTarget else { return }
1228
+ tabSwipeGeneration &+= 1
1229
+ activeTabSwipeTarget = nil
1230
+ tabSwipeSettling = false
1231
+ for nav in [visibleStack, stacks[targetId]].compactMap({ $0 }) {
1232
+ nav.view.layer.removeAllAnimations()
1233
+ nav.view.frame = view.bounds
1234
+ nav.view.isHidden = nav !== visibleStack
1235
+ }
1236
+ // Cancel the current touch stream, so its later moved/ended events
1237
+ // cannot claim another tab after the explicit navigation.
1238
+ for recognizer in tabSwipeRecognizers {
1239
+ recognizer.isEnabled = false
1240
+ recognizer.isEnabled = true
1241
+ }
1242
+ CraftChrome.shared.cancelTabSwipe(restoringSelection: restoringSelection)
1243
+ if visibleStack?.topViewController is CraftNativeScreenController { parkWeb() }
1244
+ }
1245
+
1219
1246
  private func horizontalScrollAt(_ point: CGPoint, in root: UIView) -> Bool {
1220
1247
  let local = root.convert(point, from: view)
1221
1248
  guard root.bounds.contains(local), let hit = root.hitTest(local, with: nil) else { return false }
@@ -1365,6 +1392,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
1365
1392
  }
1366
1393
 
1367
1394
  private func show(tab id: String, rootPath: String? = nil) {
1395
+ interruptTabSwipe(restoringSelection: false)
1368
1396
  cancelPendingTab()
1369
1397
  let nav = stacks[id] ?? makeStack(tab: id, rootPath: rootPath ?? (id.isEmpty ? hybrid.startPath : id))
1370
1398
  // A stack is hidden and shown rather than moved, so UIKit tells its
@@ -1448,6 +1476,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
1448
1476
  /// it), a tab's root, or a new one pushed.
1449
1477
  func showNative(path raw: String, replace: Bool, fromLink: Bool = false) {
1450
1478
  guard let match = hybrid.routes.match(raw) else { return }
1479
+ interruptTabSwipe(restoringSelection: true)
1451
1480
  let tabs = hybrid.tabIds
1452
1481
  if let tab = tabs.first(where: { CraftHybridRoutes.normalize($0) == match.path }) {
1453
1482
  if tab != currentTab {
@@ -1483,6 +1512,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
1483
1512
  /// slide, then the entry slides in over the screen that opened it.
1484
1513
  private func pushWeb(_ path: String) {
1485
1514
  guard hybrid.hasWeb, !openingWeb, let nav = visibleStack else { return }
1515
+ interruptTabSwipe(restoringSelection: true)
1486
1516
  openingWeb = true
1487
1517
  hybrid.navigatePage(path) { [weak self, weak nav] depth in
1488
1518
  guard let self else { return }
@@ -1557,7 +1587,11 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
1557
1587
  // MARK: Tabs
1558
1588
 
1559
1589
  func tabTapped(_ id: String, reselect: Bool) -> Bool {
1560
- if reselect || id == currentTab {
1590
+ let interruptedSwipe = activeTabSwipeTarget != nil
1591
+ interruptTabSwipe(restoringSelection: false)
1592
+ // The incoming tab is only a preview until the swipe commits, so a
1593
+ // tap on its already-lit indicator is a selection, not a reselect.
1594
+ if (reselect && !interruptedSwipe) || id == currentTab {
1561
1595
  guard let nav = stacks[id] else { return true }
1562
1596
  if nav.viewControllers.count > 1 {
1563
1597
  nav.popToRootViewController(animated: true)