This repository has been archived by the owner on Oct 10, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 14
Turbo navigator without session #74
Closed
olivaresf
wants to merge
20
commits into
joemasilotti:main
from
olivaresf:turbo-navigator-without-session
Closed
Changes from 16 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
a71d2d3
TurboNavigator -> TurboNavigationHierarchyController
olivaresf e3790d0
TurboNavigator now becomes a single entry point
olivaresf 6dc761d
Remove session behavior from old TurboNavigation (now TurboNavigation…
olivaresf 93685d7
Continue polishing
olivaresf 5275fd1
Set session delegate
olivaresf 979400e
Add missing path config
olivaresf 438f8e7
Begin re-enabling tests
olivaresf 10f5e5d
Fix some more unit tests
olivaresf 7dd6260
Add a WebkitUIDelegate object
olivaresf ad0bdda
Add TurboWKUIDelegate conformance to TurboNavigator
olivaresf dd662ee
Fix typo in group name: HElpers → Helpers
joemasilotti ba44036
route(url:) → route(_:)
joemasilotti 9c82925
Ensure presenting modals on alerts works
joemasilotti 2e250ce
Rework TurboNavigatorDelegate
joemasilotti 657078a
Ensure WKUIDelegate is set upon initialization
joemasilotti 74124a0
Code formatting
joemasilotti ee81553
Convenience initializer to not expose Session
joemasilotti f753dff
Copy over cookies after each completed web request
joemasilotti eb1b4ea
Refactor default class to a protocol extension
joemasilotti ded3633
TEMP: Example with more Turbo Navigator features
joemasilotti File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
import SafariServices | ||
import Turbo | ||
import UIKit | ||
import WebKit | ||
|
||
/// Handles navigation to new URLs using the following rules: | ||
/// https://github.com/joemasilotti/TurboNavigator#handled-flows | ||
class TurboNavigationHierarchyController { | ||
let navigationController: UINavigationController | ||
let modalNavigationController: UINavigationController | ||
|
||
var rootViewController: UIViewController { navigationController } | ||
var activeNavigationController: UINavigationController { | ||
navigationController.presentedViewController != nil ? modalNavigationController : navigationController | ||
} | ||
|
||
var animationsEnabled: Bool = true | ||
|
||
enum NavigationStackType { | ||
case main | ||
case modal | ||
} | ||
|
||
func navController(for navigationType: NavigationStackType) -> UINavigationController { | ||
switch navigationType { | ||
case .main: navigationController | ||
case .modal: modalNavigationController | ||
} | ||
} | ||
|
||
/// Default initializer. | ||
/// | ||
/// - Parameters: | ||
/// - delegate: handles visits and refresh | ||
init(delegate: TurboNavigationHierarchyControllerDelegate) { | ||
self.delegate = delegate | ||
self.navigationController = UINavigationController() | ||
self.modalNavigationController = UINavigationController() | ||
} | ||
|
||
func route(controller: UIViewController, proposal: VisitProposal) { | ||
if let alert = controller as? UIAlertController { | ||
presentAlert(alert) | ||
} else { | ||
switch proposal.presentation { | ||
case .default: | ||
navigate(with: controller, via: proposal) | ||
case .pop: | ||
pop() | ||
case .replace: | ||
replace(with: controller, via: proposal) | ||
case .refresh: | ||
refresh() | ||
case .clearAll: | ||
clearAll() | ||
case .replaceRoot: | ||
replaceRoot(with: controller) | ||
case .none: | ||
break // Do nothing. | ||
} | ||
} | ||
} | ||
|
||
func openExternal(url: URL, navigationType: NavigationStackType) { | ||
if ["http", "https"].contains(url.scheme) { | ||
let safariViewController = SFSafariViewController(url: url) | ||
safariViewController.modalPresentationStyle = .pageSheet | ||
if #available(iOS 15.0, *) { | ||
safariViewController.preferredControlTintColor = .tintColor | ||
} | ||
let navController = navController(for: navigationType) | ||
navController.present(safariViewController, animated: animationsEnabled) | ||
} else if UIApplication.shared.canOpenURL(url) { | ||
UIApplication.shared.open(url) | ||
} | ||
} | ||
|
||
// MARK: Private | ||
|
||
@available(*, unavailable) | ||
required init?(coder aDecoder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
private unowned let delegate: TurboNavigationHierarchyControllerDelegate | ||
|
||
private func presentAlert(_ alert: UIAlertController) { | ||
if navigationController.presentedViewController != nil { | ||
modalNavigationController.present(alert, animated: animationsEnabled) | ||
} else { | ||
navigationController.present(alert, animated: animationsEnabled) | ||
} | ||
} | ||
|
||
private func navigate(with controller: UIViewController, via proposal: VisitProposal) { | ||
switch proposal.context { | ||
case .default: | ||
navigationController.dismiss(animated: animationsEnabled) | ||
pushOrReplace(on: navigationController, with: controller, via: proposal) | ||
if let visitable = controller as? Visitable { | ||
delegate.visit(visitable, on: .main, with: proposal.options) | ||
} | ||
case .modal: | ||
if navigationController.presentedViewController != nil { | ||
pushOrReplace(on: modalNavigationController, with: controller, via: proposal) | ||
} else { | ||
modalNavigationController.setViewControllers([controller], animated: animationsEnabled) | ||
navigationController.present(modalNavigationController, animated: animationsEnabled) | ||
} | ||
if let visitable = controller as? Visitable { | ||
delegate.visit(visitable, on: .modal, with: proposal.options) | ||
} | ||
} | ||
} | ||
|
||
private func pushOrReplace(on navigationController: UINavigationController, with controller: UIViewController, via proposal: VisitProposal) { | ||
if visitingSamePage(on: navigationController, with: controller, via: proposal.url) { | ||
navigationController.replaceLastViewController(with: controller) | ||
} else if visitingPreviousPage(on: navigationController, with: controller, via: proposal.url) { | ||
navigationController.popViewController(animated: animationsEnabled) | ||
} else if proposal.options.action == .advance { | ||
navigationController.pushViewController(controller, animated: animationsEnabled) | ||
} else { | ||
navigationController.replaceLastViewController(with: controller) | ||
} | ||
} | ||
|
||
private func visitingSamePage(on navigationController: UINavigationController, with controller: UIViewController, via url: URL) -> Bool { | ||
if let visitable = navigationController.topViewController as? Visitable { | ||
return visitable.visitableURL == url | ||
} else if let topViewController = navigationController.topViewController { | ||
return topViewController.isMember(of: type(of: controller)) | ||
} | ||
return false | ||
} | ||
|
||
private func visitingPreviousPage(on navigationController: UINavigationController, with controller: UIViewController, via url: URL) -> Bool { | ||
guard navigationController.viewControllers.count >= 2 else { return false } | ||
|
||
let previousController = navigationController.viewControllers[navigationController.viewControllers.count - 2] | ||
if let previousVisitable = previousController as? VisitableViewController { | ||
return previousVisitable.visitableURL == url | ||
} | ||
return type(of: previousController) == type(of: controller) | ||
} | ||
|
||
private func pop() { | ||
if navigationController.presentedViewController != nil { | ||
if modalNavigationController.viewControllers.count == 1 { | ||
navigationController.dismiss(animated: animationsEnabled) | ||
} else { | ||
modalNavigationController.popViewController(animated: animationsEnabled) | ||
} | ||
} else { | ||
navigationController.popViewController(animated: animationsEnabled) | ||
} | ||
} | ||
|
||
private func replace(with controller: UIViewController, via proposal: VisitProposal) { | ||
switch proposal.context { | ||
case .default: | ||
navigationController.dismiss(animated: animationsEnabled) | ||
navigationController.replaceLastViewController(with: controller) | ||
if let visitable = controller as? Visitable { | ||
delegate.visit(visitable, on: .main, with: proposal.options) | ||
} | ||
case .modal: | ||
if navigationController.presentedViewController != nil { | ||
modalNavigationController.replaceLastViewController(with: controller) | ||
} else { | ||
modalNavigationController.setViewControllers([controller], animated: false) | ||
navigationController.present(modalNavigationController, animated: animationsEnabled) | ||
} | ||
if let visitable = controller as? Visitable { | ||
delegate.visit(visitable, on: .modal, with: proposal.options) | ||
} | ||
} | ||
} | ||
|
||
private func refresh() { | ||
if navigationController.presentedViewController != nil { | ||
if modalNavigationController.viewControllers.count == 1 { | ||
navigationController.dismiss(animated: animationsEnabled) | ||
delegate.refresh(navigationStack: .main) | ||
} else { | ||
modalNavigationController.popViewController(animated: animationsEnabled) | ||
delegate.refresh(navigationStack: .modal) | ||
} | ||
} else { | ||
navigationController.popViewController(animated: animationsEnabled) | ||
delegate.refresh(navigationStack: .main) | ||
} | ||
} | ||
|
||
private func clearAll() { | ||
navigationController.dismiss(animated: animationsEnabled) | ||
navigationController.popToRootViewController(animated: animationsEnabled) | ||
delegate.refresh(navigationStack: .main) | ||
} | ||
|
||
private func replaceRoot(with controller: UIViewController) { | ||
navigationController.dismiss(animated: animationsEnabled) | ||
navigationController.setViewControllers([controller], animated: animationsEnabled) | ||
|
||
if let visitable = controller as? Visitable { | ||
delegate.visit(visitable, on: .main, with: .init(action: .replace)) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import SafariServices | ||
import Turbo | ||
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) | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this exist? I don't see a way to set it publicly as a consumer of the library. If anything, I'd prefer to see this live in the path configuration somehow so individual links can be customized instead of a global option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha. That was a hack. There's no way to pass along a type of
UINavigationController
into this class anymore, so testing was broken. What I did was create this variable and keep it internal so consumers cannot use it but unit tests can reach it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, nice catch! What about exposing the navigation controllers in the initializer but defaulted like we had before?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think that's the way forward. I originally didn't go with this because I didn't want to expose them, but since this class is now internal, I don't see any issues at all.