diff --git a/Source/HotwireConfig.swift b/Source/HotwireConfig.swift index f0360f0..b7ea101 100644 --- a/Source/HotwireConfig.swift +++ b/Source/HotwireConfig.swift @@ -36,9 +36,9 @@ public struct HotwireConfig { } /// The navigation controller used in `Navigator` for the main and modal stacks. - /// Must be a `UINavigationController` or subclass. + /// Must be a `HotwireNavigationController` or subclass. public var defaultNavigationController: () -> UINavigationController = { - UINavigationController() + HotwireNavigationController() } /// Optionally customize the web views used by each Turbo Session. diff --git a/Source/HotwireNavigationController.swift b/Source/HotwireNavigationController.swift new file mode 100644 index 0000000..685aa1d --- /dev/null +++ b/Source/HotwireNavigationController.swift @@ -0,0 +1,70 @@ +import UIKit + +/// The `HotwireNavigationController` is a custom subclass of `UINavigationController` designed to enhance the management of `VisitableViewController` instances within a navigation stack. +/// It tracks the reasons why a view controller appears or disappears, which is crucial for handling navigation in Hotwire-powered applications. +/// - Important: If you are using a custom or third-party navigation controller, subclass `HotwireNavigationController` to integrate its behavior. +/// +/// ## Usage Notes +/// +/// - **Integrating with Custom Navigation Controllers:** +/// If you're using a custom or third-party navigation controller, subclass `HotwireNavigationController` to incorporate the necessary behavior. +/// +/// ```swift +/// open class YourCustomNavigationController: HotwireNavigationController { +/// // Make sure to always call super when overriding functions from `HotwireNavigationController`. +/// } +/// ``` +/// +/// - **Extensibility:** +/// The class is marked as `open`, allowing you to subclass and extend its functionality to suit your specific needs. +/// +/// ## Limitations +/// +/// - **Other Container Controllers:** +/// The current implementation focuses on `UINavigationController` and includes handling for `UITabBarController`. It does not provide out-of-the-box support for other container controllers like `UISplitViewController`. +/// +/// - **Custom Navigation Setups:** +/// For completely custom navigation setups or container controllers, you will need to implement similar logic to manage the `appearReason` and `disappearReason` of `VisitableViewController` instances. +open class HotwireNavigationController: UINavigationController { + open override func pushViewController(_ viewController: UIViewController, animated: Bool) { + if let visitableViewController = viewController as? VisitableViewController { + visitableViewController.appearReason = .pushedOntoNavigationStack + } + + if let topVisitableViewController = topViewController as? VisitableViewController { + topVisitableViewController.disappearReason = .coveredByPush + } + + super.pushViewController(viewController, animated: animated) + } + + open override func popViewController(animated: Bool) -> UIViewController? { + let poppedViewController = super.popViewController(animated: animated) + if let poppedVisitableViewController = poppedViewController as? VisitableViewController { + poppedVisitableViewController.disappearReason = .poppedFromNavigationStack + } + + if let topVisitableViewController = topViewController as? VisitableViewController { + topVisitableViewController.appearReason = .revealedByPop + } + + return poppedViewController + } + + open override func viewWillAppear(_ animated: Bool) { + if let topVisitableViewController = topViewController as? VisitableViewController, + topVisitableViewController.disappearReason == .tabDeselected { + topVisitableViewController.appearReason = .tabSelected + } + super.viewWillAppear(animated) + } + + open override func viewWillDisappear(_ animated: Bool) { + if tabBarController != nil, + let topVisitableViewController = topViewController as? VisitableViewController { + topVisitableViewController.disappearReason = .tabDeselected + } + + super.viewWillDisappear(animated) + } +} diff --git a/Source/Turbo/Session/Session.swift b/Source/Turbo/Session/Session.swift index 98f5172..677332e 100644 --- a/Source/Turbo/Session/Session.swift +++ b/Source/Turbo/Session/Session.swift @@ -234,7 +234,7 @@ extension Session: VisitableDelegate { previousVisit = nil } - guard let topmostVisit = topmostVisit, let currentVisit = currentVisit else { return } + guard let topmostVisit, let currentVisit else { return } if isSnapshotCacheStale { clearSnapshotCache() @@ -244,21 +244,33 @@ extension Session: VisitableDelegate { if isShowingStaleContent { reload() isShowingStaleContent = false - } else if visitable === topmostVisit.visitable && visitable.visitableViewController.isMovingToParent { - // Back swipe gesture canceled + return + } + + // Back swipe gesture canceled. + if visitable === topmostVisit.visitable && visitable.visitableViewController.isMovingToParent { if topmostVisit.state == .completed { currentVisit.cancel() } else { visit(visitable, action: .advance) } - } else if visitable === currentVisit.visitable && currentVisit.state == .started { - // Navigating forward - complete navigation early + return + } + + // Navigating forward - complete navigation early. + if visitable === currentVisit.visitable && currentVisit.state == .started { completeNavigationForCurrentVisit() - } else if visitable !== topmostVisit.visitable { - // Navigating backward from a web view screen to a web view screen. + return + } + + // Navigating backward from a web view screen to a web view screen. + if visitable !== topmostVisit.visitable { visit(visitable, action: .restore) - } else if visitable === previousVisit?.visitable { - // Navigating backward from a native to a web view screen. + return + } + + // Navigating backward from a native to a web view screen. + if visitable === previousVisit?.visitable { visit(visitable, action: .restore) } } diff --git a/Source/Turbo/Visitable/VisitableViewController.swift b/Source/Turbo/Visitable/VisitableViewController.swift index 6efb7b1..7c5c0f6 100644 --- a/Source/Turbo/Visitable/VisitableViewController.swift +++ b/Source/Turbo/Visitable/VisitableViewController.swift @@ -4,6 +4,8 @@ import WebKit open class VisitableViewController: UIViewController, Visitable { open weak var visitableDelegate: VisitableDelegate? open var visitableURL: URL! + var appearReason: AppearReason = .pushedOntoNavigationStack + var disappearReason: DisappearReason = .poppedFromNavigationStack public convenience init(url: URL) { self.init() @@ -20,21 +22,25 @@ open class VisitableViewController: UIViewController, Visitable { override open func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + if appearReason == .tabSelected { return } visitableDelegate?.visitableViewWillAppear(self) } override open func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + if appearReason == .tabSelected { return } visitableDelegate?.visitableViewDidAppear(self) } override open func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + if disappearReason == .tabDeselected { return } visitableDelegate?.visitableViewWillDisappear(self) } override open func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) + if disappearReason == .tabDeselected { return } visitableDelegate?.visitableViewDidDisappear(self) } @@ -78,3 +84,17 @@ open class VisitableViewController: UIViewController, Visitable { ]) } } + +extension VisitableViewController { + public enum AppearReason { + case pushedOntoNavigationStack + case revealedByPop + case tabSelected + } + + public enum DisappearReason { + case coveredByPush + case poppedFromNavigationStack + case tabDeselected + } +}