Skip to content

Commit

Permalink
Merge pull request #50 from hotwired/tab-bar-switching
Browse files Browse the repository at this point in the history
Ignore view and session lifecycle events when switching between tabs
  • Loading branch information
svara authored Dec 4, 2024
2 parents f52aa92 + f9603b1 commit 08bdabe
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 11 deletions.
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
}
}

0 comments on commit 08bdabe

Please sign in to comment.