diff --git a/Source/Turbo Navigator/TurboNavigationHierarchyController.swift b/Source/Turbo Navigator/TurboNavigationHierarchyController.swift index 2d855b1..0a1995e 100644 --- a/Source/Turbo Navigator/TurboNavigationHierarchyController.swift +++ b/Source/Turbo Navigator/TurboNavigationHierarchyController.swift @@ -168,21 +168,28 @@ class TurboNavigationHierarchyController { if navigationController.presentedViewController != nil { if modalNavigationController.viewControllers.count == 1 { navigationController.dismiss(animated: proposal.animated) - delegate.refresh(navigationStack: .main) + refreshIfTopViewControllerIsVisitable(from: .main) } else { modalNavigationController.popViewController(animated: proposal.animated) - delegate.refresh(navigationStack: .modal) + refreshIfTopViewControllerIsVisitable(from: .modal) } } else { navigationController.popViewController(animated: proposal.animated) - delegate.refresh(navigationStack: .main) + refreshIfTopViewControllerIsVisitable(from: .main) + } + } + + private func refreshIfTopViewControllerIsVisitable(from stack: NavigationStackType) { + if let navControllerTopmostVisitable = navController(for: stack).topViewController as? Visitable { + delegate.refreshVisitable(navigationStack: stack, + newTopmostVisitable: navControllerTopmostVisitable) } } private func clearAll(via proposal: VisitProposal) { navigationController.dismiss(animated: proposal.animated) navigationController.popToRootViewController(animated: proposal.animated) - delegate.refresh(navigationStack: .main) + refreshIfTopViewControllerIsVisitable(from: .main) } private func replaceRoot(with controller: UIViewController, via proposal: VisitProposal) { diff --git a/Source/Turbo Navigator/TurboNavigationHierarchyControllerDelegate.swift b/Source/Turbo Navigator/TurboNavigationHierarchyControllerDelegate.swift index 32fccb9..a49093d 100644 --- a/Source/Turbo Navigator/TurboNavigationHierarchyControllerDelegate.swift +++ b/Source/Turbo Navigator/TurboNavigationHierarchyControllerDelegate.swift @@ -1,9 +1,23 @@ import SafariServices import WebKit -/// Implement to be notified when certain navigations are performed -/// or to render a native controller instead of a Turbo web visit. protocol TurboNavigationHierarchyControllerDelegate: AnyObject { - func visit(_ : Visitable, on: TurboNavigationHierarchyController.NavigationStackType, with: VisitOptions) - func refresh(navigationStack: TurboNavigationHierarchyController.NavigationStackType) + + /// Once the navigation hierarchy is modified, begin a visit on a navigation controller. + /// + /// - Parameters: + /// - _: the Visitable destination + /// - on: the navigation controller that was modified + /// - with: the visit options + func visit(_ : Visitable, + on: TurboNavigationHierarchyController.NavigationStackType, + with: VisitOptions) + + /// A refresh will pop (or dismiss) then ask the session to refresh the previous (or underlying) Visitable. + /// + /// - Parameters: + /// - navigationStack: the stack where the refresh is happening + /// - newTopmostVisitable: the visitable to be refreshed + func refreshVisitable(navigationStack: TurboNavigationHierarchyController.NavigationStackType, + newTopmostVisitable: Visitable) } diff --git a/Source/Turbo Navigator/TurboNavigator.swift b/Source/Turbo Navigator/TurboNavigator.swift index 7f6db64..bf27a0b 100644 --- a/Source/Turbo Navigator/TurboNavigator.swift +++ b/Source/Turbo Navigator/TurboNavigator.swift @@ -215,10 +215,13 @@ extension TurboNavigator: TurboNavigationHierarchyControllerDelegate { } } - func refresh(navigationStack: TurboNavigationHierarchyController.NavigationStackType) { + func refreshVisitable(navigationStack: TurboNavigationHierarchyController.NavigationStackType, + newTopmostVisitable: any Visitable) { switch navigationStack { - case .main: session.reload() - case .modal: modalSession.reload() + case .main: + session.visit(newTopmostVisitable, action: .restore) + case .modal: + modalSession.visit(newTopmostVisitable, action: .restore) } } } diff --git a/Tests/Turbo Navigator/TurboNavigatorTests.swift b/Tests/Turbo Navigator/TurboNavigatorTests.swift index aa26088..cba3e6f 100644 --- a/Tests/Turbo Navigator/TurboNavigatorTests.swift +++ b/Tests/Turbo Navigator/TurboNavigatorTests.swift @@ -82,6 +82,61 @@ final class TurboNavigationHierarchyControllerTests: XCTestCase { XCTAssert(navigationController.viewControllers.last is VisitableViewController) assertVisited(url: proposal.url, on: .main) } + + func test_default_default_refresh_refreshesPreviousController() { + navigator.route(oneURL) + XCTAssertEqual(navigationController.viewControllers.count, 1) + + navigator.route(twoURL) + XCTAssertEqual(navigator.rootViewController.viewControllers.count, 2) + + /// Refreshing should pop the view controller and refresh the underlying controller. + let proposal = VisitProposal(presentation: .refresh) + navigator.route(proposal) + + let visitable = navigator.session.activeVisitable as! VisitableViewController + XCTAssertEqual(visitable.visitableURL, oneURL) + XCTAssertEqual(navigator.rootViewController.viewControllers.count, 1) + } + + func test_default_modal_refresh_refreshesPreviousController() { + navigationController.pushViewController(UIViewController(), animated: false) + XCTAssertEqual(navigationController.viewControllers.count, 1) + + let oneURLProposal = VisitProposal(path: "/one", context: .modal) + navigator.route(oneURLProposal) + + let twoURLProposal = VisitProposal(path: "/two", context: .modal) + navigator.route(twoURLProposal) + XCTAssertEqual(modalNavigationController.viewControllers.count, 2) + + /// Refreshing should pop the view controller and refresh the underlying controller. + let proposal = VisitProposal(presentation: .refresh) + navigator.route(proposal) + + let visitable = navigator.modalSession.activeVisitable as! VisitableViewController + XCTAssertEqual(visitable.visitableURL, oneURL) + XCTAssertEqual(modalNavigationController.viewControllers.count, 1) + } + + func test_default_modal_refresh_dismissesAndRefreshesMainStackTopViewController() { + navigator.route(oneURL) + XCTAssertEqual(navigationController.viewControllers.count, 1) + + let twoURLProposal = VisitProposal(path: "/two", context: .modal) + navigator.route(twoURLProposal) + XCTAssertEqual(modalNavigationController.viewControllers.count, 1) + + /// Refreshing should dismiss the view controller and refresh the underlying controller. + let proposal = VisitProposal(context: .modal, presentation: .refresh) + navigator.route(proposal) + + let visitable = navigator.session.activeVisitable as! VisitableViewController + XCTAssertEqual(visitable.visitableURL, oneURL) + + XCTAssertNil(navigationController.presentedViewController) + XCTAssertEqual(navigator.rootViewController.viewControllers.count, 1) + } func test_default_modal_default_presentsModal() { navigationController.pushViewController(UIViewController(), animated: false) @@ -309,7 +364,7 @@ final class TurboNavigationHierarchyControllerTests: XCTestCase { private class EmptyNavigationDelegate: TurboNavigationHierarchyControllerDelegate { func visit(_: Visitable, on: TurboNavigationHierarchyController.NavigationStackType, with: VisitOptions) {} - func refresh(navigationStack: TurboNavigationHierarchyController.NavigationStackType) {} + func refreshVisitable(navigationStack: TurboNavigationHierarchyController.NavigationStackType, newTopmostVisitable: any Visitable) { } } // MARK: - VisitProposal extension