-
Notifications
You must be signed in to change notification settings - Fork 86
/
TurboNavigationController.swift
113 lines (96 loc) · 4.73 KB
/
TurboNavigationController.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
//
// TurboNavigationController.swift
// Demo
//
// Created by Fernando Olivares on 08/11/21.
//
import Foundation
import UIKit
import Turbo
class TurboNavigationController : UINavigationController {
var session: Session!
var modalSession: Session!
func push(url: URL) {
let properties = session.pathConfiguration?.properties(for: url) ?? [:]
route(url: url,
options: VisitOptions(action: .advance),
properties: properties)
}
func route(url: URL, options: VisitOptions, properties: PathProperties) {
// This is a simplified version of how you might build out the routing
// and navigation functions of your app. In a real app, these would be separate objects
// Dismiss any modals when receiving a new navigation
if presentedViewController != nil {
dismiss(animated: true)
}
// Special case of navigating home, issue a reload
if url.path == "/", !viewControllers.isEmpty {
popViewController(animated: false)
session.reload()
return
}
// - Create view controller appropriate for url/properties
// - Navigate to that with the correct presentation
// - Initiate the visit with Turbo
let viewController = makeViewController(for: url, properties: properties)
navigate(to: viewController, action: options.action, properties: properties)
visit(viewController: viewController, with: options, modal: isModal(properties))
}
}
extension TurboNavigationController {
private func isModal(_ properties: PathProperties) -> Bool {
// For simplicity, we're using string literals for various keys and values of the path configuration
// but most likely you'll want to define your own enums these properties
let presentation = properties["presentation"] as? String
return presentation == "modal"
}
private func makeViewController(for url: URL, properties: PathProperties = [:]) -> UIViewController {
// There are many options for determining how to map urls to view controllers
// The demo uses the path configuration for determining which view controller and presentation
// to use, but that's completely optional. You can use whatever logic you prefer to determine
// how you navigate and route different URLs.
if let viewController = properties["view-controller"] as? String {
switch viewController {
case "numbers":
let numbersVC = NumbersViewController()
numbersVC.url = url
return numbersVC
case "numbersDetail":
let alertController = UIAlertController(title: "Number", message: "\(url.lastPathComponent)", preferredStyle: .alert)
alertController.addAction(.init(title: "OK", style: .default, handler: nil))
return alertController
default:
assertionFailure("Invalid view controller, defaulting to WebView")
}
}
return ViewController(url: url)
}
private func navigate(to viewController: UIViewController, action: VisitAction, properties: PathProperties = [:], animated: Bool = true) {
// We support three types of navigation in the app: advance, replace, and modal
if isModal(properties) {
if viewController is UIAlertController {
present(viewController, animated: animated, completion: nil)
} else {
let modalNavController = UINavigationController(rootViewController: viewController)
present(modalNavController, animated: animated)
}
} else if action == .replace {
let viewControllers = Array(viewControllers.dropLast()) + [viewController]
setViewControllers(viewControllers, animated: false)
} else {
pushViewController(viewController, animated: animated)
}
}
private func visit(viewController: UIViewController, with options: VisitOptions, modal: Bool = false) {
guard let visitable = viewController as? Visitable else { return }
// Each Session corresponds to a single web view. A good rule of thumb
// is to use a session per navigation stack. Here we're using a different session
// when presenting a modal. We keep that around for any modal presentations so
// we don't have to create more than we need since each new session incurs a cold boot visit cost
if modal {
modalSession.visit(visitable, options: options)
} else {
session.visit(visitable, options: options)
}
}
}