Skip to content

Commit c20db17

Browse files
Adds FlowNavigator environment object
Adds a FlowNavigator environment object for easier navigation from deeply nested views.
1 parent 88a9913 commit c20db17

File tree

8 files changed

+364
-88
lines changed

8 files changed

+364
-88
lines changed

FlowStacksApp/Shared/NumberCoordinator.swift

Lines changed: 34 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,23 @@ extension Screen: ExpressibleByIntegerLiteral, Hashable {
1515

1616
struct NumberCoordinator: View {
1717
@State var routes: Routes<Screen> = [.root(0, embedInNavigationView: true)]
18-
18+
1919
var randomRoutes: [Route<Screen>] {
2020
let options: [[Route<Screen>]] = [
2121
[.root(0, embedInNavigationView: true)],
2222
[.root(0, embedInNavigationView: true), .push(1), .push(2), .push(3), .sheet(4, embedInNavigationView: true), .push(5)],
2323
[.root(0, embedInNavigationView: true), .push(1), .push(2), .push(3)],
2424
[.root(0, embedInNavigationView: true), .push(1), .sheet(2, embedInNavigationView: true), .push(3), .sheet(4, embedInNavigationView: true), .push(5)],
25-
[.root(0, embedInNavigationView: true), .sheet(1, embedInNavigationView: true), .cover(2, embedInNavigationView: true), .push(3), .sheet(4, embedInNavigationView: true), .push(5)]
25+
[.root(0, embedInNavigationView: true), .sheet(1, embedInNavigationView: true), .cover(2, embedInNavigationView: true), .push(3), .sheet(4, embedInNavigationView: true), .push(5)],
2626
]
2727
return options.randomElement()!
2828
}
29-
29+
3030
var body: some View {
31-
Router($routes) { $screen, index in
31+
Router($routes) { $screen, _ in
3232
if let number = Binding(unwrapping: $screen, case: /Screen.number) {
3333
NumberView(
3434
number: number,
35-
presentDoubleCover: { number in
36-
routes.presentCover(.number(number * 2), embedInNavigationView: true)
37-
},
38-
presentDoubleSheet: { number in
39-
routes.presentSheet(.number(number * 2), embedInNavigationView: true)
40-
},
41-
pushNext: { number in
42-
routes.push(.number(number + 1))
43-
},
44-
goBack: index != 0 ? { routes.goBack() } : nil,
45-
goBackToRoot: {
46-
$routes.withDelaysIfUnsupported {
47-
$0.goBackToRoot()
48-
}
49-
},
5035
goRandom: {
5136
$routes.withDelaysIfUnsupported {
5237
$0 = randomRoutes
@@ -62,7 +47,7 @@ struct NumberCoordinator: View {
6247
follow(deeplink)
6348
}
6449
}
65-
50+
6651
private func follow(_ deeplink: Deeplink) {
6752
guard case .numberCoordinator(let link) = deeplink else {
6853
return
@@ -80,27 +65,29 @@ struct NumberCoordinator: View {
8065

8166
struct NumberView: View {
8267
@Binding var number: Int
83-
84-
let presentDoubleCover: (Int) -> Void
85-
let presentDoubleSheet: (Int) -> Void
86-
let pushNext: (Int) -> Void
87-
let goBack: (() -> Void)?
88-
let goBackToRoot: () -> Void
68+
@EnvironmentObject var navigator: FlowNavigator<Screen>
69+
8970
let goRandom: (() -> Void)?
90-
71+
9172
var body: some View {
9273
VStack(spacing: 8) {
9374
Stepper("\(number)", value: $number)
94-
Button("Present Double (cover)") { presentDoubleCover(number) }
95-
Button("Present Double (sheet)") { presentDoubleSheet(number) }
96-
Button("Push next") { pushNext(number) }
75+
Button("Present Double (cover)") {
76+
navigator.presentCover(.number(number * 2), embedInNavigationView: true)
77+
}
78+
Button("Present Double (sheet)") {
79+
navigator.presentSheet(.number(number * 2), embedInNavigationView: true)
80+
}
81+
Button("Push next") {
82+
navigator.push(.number(number + 1))
83+
}
9784
if let goRandom = goRandom {
9885
Button("Go random", action: goRandom)
9986
}
100-
if let goBack = goBack {
101-
Button("Go back", action: goBack)
87+
if navigator.routes.count > 1 {
88+
Button("Go back") { navigator.goBack() }
89+
Button("Go back to root") { navigator.goBackToRoot() }
10290
}
103-
Button("Go back to root", action: goBackToRoot)
10491
}
10592
.padding()
10693
.navigationTitle("\(number)")
@@ -109,17 +96,21 @@ struct NumberView: View {
10996

11097
// Included so that the same example code can be used for macOS too.
11198
#if os(macOS)
112-
extension Route {
113-
114-
static func cover(_ screen: Screen, embedInNavigationView: Bool = false) -> Route {
115-
sheet(screen, embedInNavigationView: embedInNavigationView)
99+
extension Route {
100+
static func cover(_ screen: Screen, embedInNavigationView: Bool = false) -> Route {
101+
sheet(screen, embedInNavigationView: embedInNavigationView)
102+
}
103+
}
104+
105+
extension Array where Element: RouteProtocol {
106+
mutating func presentCover(_ screen: Element.Screen, embedInNavigationView: Bool = false) {
107+
presentSheet(screen, embedInNavigationView: embedInNavigationView)
108+
}
116109
}
117-
}
118110

119-
extension Array where Element: RouteProtocol {
120-
121-
mutating func presentCover(_ screen: Element.Screen, embedInNavigationView: Bool = false) {
122-
presentSheet(screen, embedInNavigationView: embedInNavigationView)
111+
extension FlowNavigator {
112+
func presentCover(_ screen: Screen, embedInNavigationView: Bool = false) {
113+
presentSheet(screen, embedInNavigationView: embedInNavigationView)
114+
}
123115
}
124-
}
125116
#endif

FlowStacksApp/Shared/ShowingCoordinator.swift

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,36 @@ import SwiftUINavigation
44

55
struct ShowingCoordinator: View {
66
@State var routes: Routes<Int> = []
7-
7+
88
var body: some View {
99
Button("Show 42", action: { routes.push(42) })
10-
.showing($routes, embedInNavigationView: true) { $number, index in
11-
NumberView(
12-
number: $number,
13-
presentDoubleCover: { number in
14-
routes.presentCover(number * 2 , embedInNavigationView: true)
15-
},
16-
presentDoubleSheet: { number in
17-
routes.presentSheet(number * 2 , embedInNavigationView: true)
18-
},
19-
pushNext: { number in
20-
routes.push(number + 1)
21-
},
22-
goBack: { routes.goBack() },
23-
goBackToRoot: {
24-
$routes.withDelaysIfUnsupported {
25-
$0 = []
26-
}
27-
},
28-
goRandom: nil
29-
)
10+
.showing($routes, embedInNavigationView: true) { $number, _ in
11+
ShownNumberView(number: $number)
12+
}
13+
}
14+
}
15+
16+
struct ShownNumberView: View {
17+
@Binding var number: Int
18+
@EnvironmentObject var navigator: FlowNavigator<Int>
19+
20+
var body: some View {
21+
VStack(spacing: 8) {
22+
Stepper("\(number)", value: $number)
23+
Button("Present Double (cover)") {
24+
navigator.presentCover(number * 2, embedInNavigationView: true)
25+
}
26+
Button("Present Double (sheet)") {
27+
navigator.presentSheet(number * 2, embedInNavigationView: true)
28+
}
29+
Button("Push next") {
30+
navigator.push(number + 1)
31+
}
32+
if !navigator.routes.isEmpty {
33+
Button("Go back") { navigator.goBack() }
3034
}
35+
}
36+
.padding()
37+
.navigationTitle("\(number)")
3138
}
3239
}

FlowStacksApp/Shared/VMCoordinator.swift

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,25 @@ class VMCoordinatorViewModel: ObservableObject {
77
case numberList(NumberListView.ViewModel)
88
case numberDetail(NumberDetailView.ViewModel)
99
}
10-
10+
1111
@Published var routes: Routes<Screen> = []
12-
12+
1313
init() {
1414
routes.presentSheet(.home(.init(pickANumberSelected: showNumberList)))
1515
}
16-
16+
1717
func showNumberList() {
1818
routes.presentSheet(.numberList(.init(numberSelected: showNumber, cancel: dismiss)))
1919
}
20-
20+
2121
func showNumber(_ number: Int) {
2222
routes.presentSheet(.numberDetail(.init(number: number, cancel: goBackToRoot)))
2323
}
24-
24+
2525
func dismiss() {
2626
routes.goBack()
2727
}
28-
28+
2929
func goBackToRoot() {
3030
RouteSteps.withDelaysIfUnsupported(self, \.routes) {
3131
$0.goBackToRoot()
@@ -35,7 +35,7 @@ class VMCoordinatorViewModel: ObservableObject {
3535

3636
struct VMCoordinator: View {
3737
@ObservedObject var viewModel = VMCoordinatorViewModel()
38-
38+
3939
var body: some View {
4040
Router($viewModel.routes) { screen, _ in
4141
switch screen {
@@ -55,14 +55,14 @@ struct VMCoordinator: View {
5555
struct HomeView: View {
5656
class ViewModel: ObservableObject {
5757
let pickANumberSelected: () -> Void
58-
58+
5959
init(pickANumberSelected: @escaping () -> Void) {
6060
self.pickANumberSelected = pickANumberSelected
6161
}
6262
}
63-
63+
6464
@ObservedObject var viewModel: ViewModel
65-
65+
6666
var body: some View {
6767
VStack {
6868
Button("Pick a number", action: viewModel.pickANumberSelected)
@@ -76,15 +76,15 @@ struct NumberListView: View {
7676
let numbers = 1 ... 100
7777
let numberSelected: (Int) -> Void
7878
let cancel: () -> Void
79-
79+
8080
init(numberSelected: @escaping (Int) -> Void, cancel: @escaping () -> Void) {
8181
self.numberSelected = numberSelected
8282
self.cancel = cancel
8383
}
8484
}
85-
85+
8686
@ObservedObject var viewModel: ViewModel
87-
87+
8888
var body: some View {
8989
VStack(spacing: 12) {
9090
List(viewModel.numbers, id: \.self) { number in
@@ -100,21 +100,21 @@ struct NumberDetailView: View {
100100
class ViewModel: ObservableObject {
101101
let number: Int
102102
let cancel: () -> Void
103-
103+
104104
init(number: Int, cancel: @escaping () -> Void) {
105105
self.number = number
106106
self.cancel = cancel
107107
}
108108
}
109-
109+
110110
@ObservedObject var viewModel: ViewModel
111-
111+
112112
@Environment(\.presentationMode) var presentationMode
113-
113+
114114
var body: some View {
115115
VStack {
116116
Text("\(viewModel.number)")
117-
Button("Go back", action: viewModel.cancel)
117+
Button("Go back to root", action: viewModel.cancel)
118118
Button("PresentationMode Dismiss") {
119119
presentationMode.wrappedValue.dismiss()
120120
}

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,25 @@ The routes array can be managed using normal Array methods, but a number of conv
8989

9090
If the user taps the back button, the routes array will be automatically updated to reflect the new navigation state. Navigating back with an edge swipe gesture or via a long-press gesture on the back button will also update the routes array automatically, as will swiping to dismiss a sheet.
9191

92+
### FlowNavigator
93+
94+
The example above passes closures to screen views for presenting new screens and going back. However, passing closures can soon become unwieldy if you need to pass them down through multiple layers of views. Instead, a `FlowNavigator` object is available through the environment, giving access to the current routes array and the ability to update it via all its convenience methods. It can be accessed via the environment from any view within the router, e.g.:
95+
96+
```swift
97+
@EnvironmentObject var navigator: FlowNavigator<ScreenType>
98+
99+
var body: some View {
100+
VStack {
101+
Button("View detail") {
102+
navigator.push(.detail)
103+
}
104+
Button("Go back to root") {
105+
navigator.goBackToRoot()
106+
}
107+
}
108+
}
109+
```
110+
92111
### Bindings
93112

94113
The Router can be configured to work with a binding to the screen state, rather than just a read-only value - just add `$` before the screen argument in the view-builder closure. The screen itself can then be responsible for updating its state within the routes array. Normally an enum is used to represent the screen, so it might be necessary to further extract the associated value for a particular screen as a binding. You can do that using the [SwiftUINavigation](https://github.com/pointfreeco/swiftui-navigation) library, which includes a number of helpful Binding transformations for optional and enum state, e.g.:

0 commit comments

Comments
 (0)