Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore view and session lifecycle events when switching between tabs #50

Merged
merged 12 commits into from
Dec 4, 2024
Merged
4 changes: 2 additions & 2 deletions Source/HotwireConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
70 changes: 70 additions & 0 deletions Source/HotwireNavigationController.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
30 changes: 21 additions & 9 deletions Source/Turbo/Session/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
}
}
Expand Down
20 changes: 20 additions & 0 deletions Source/Turbo/Visitable/VisitableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
}

Expand Down Expand Up @@ -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
}
}