|
3 | 3 | ## Description |
4 | 4 | This repository provides a new simple way to describe routers.\ |
5 | 5 | I view the application flow as a tree of all possible screen states. From this point of view, navigation is the selection of a node of this tree. |
| 6 | + |
6 | 7 | ## Example |
7 | 8 | Take for example an application with such a hierarchy of screens: |
8 | 9 | ```swift |
9 | 10 | TabView |
10 | 11 | ┌────────────┼────────────┐ |
11 | | - Tab1 Tab2 NavigationView |
| 12 | + Home Explore NavigationView |
12 | 13 | ┌────────┴────────┐ |
13 | | - RootView Push1View |
| 14 | + ProfileView DetailView |
14 | 15 | │ |
15 | | - PickerView |
| 16 | + ThemeSelector |
16 | 17 | ┌───────┴───────┐ |
17 | | - Text1 Text2 |
| 18 | + Light Dark |
18 | 19 | ``` |
19 | | -`PickerView` is here to demonstrate that navigation can mean not only changing screens, but also changing any state of any view. |
| 20 | +`ThemeSelector` is here to demonstrate that navigation can mean not only changing screens, but also changing any state of any view. |
20 | 21 |
|
21 | 22 | Describe your flow as a struct with `Step` properties: |
22 | 23 | ```swift |
23 | 24 | @Steps |
24 | | -struct TabSteps { |
| 25 | +struct AppSteps { |
25 | 26 |
|
26 | | - var tab1 |
27 | | - var tab2: SomeTab2Data = .init() |
28 | | - var tab3: NavigationSteps = .screen1 |
| 27 | + var home |
| 28 | + var explore = ExploreData() |
| 29 | + var profile: ProfileSteps = .main |
29 | 30 | var none |
30 | 31 | } |
31 | 32 |
|
32 | 33 | @Steps |
33 | | -struct NavigationSteps { |
| 34 | +struct ProfileSteps { |
34 | 35 |
|
35 | | - var screen1 |
36 | | - var screen2: PickerSteps = .none |
| 36 | + var main |
| 37 | + var detail: ThemeSteps = .none |
37 | 38 | } |
38 | 39 |
|
39 | 40 | @Steps |
40 | | -struct PickerSteps { |
| 41 | +struct ThemeSteps { |
41 | 42 |
|
42 | | - var text1 |
43 | | - var text2 |
| 43 | + var light |
| 44 | + var dark |
44 | 45 | var none |
45 | 46 | } |
46 | 47 | ``` |
47 | 48 | ```swift |
48 | | -var steps: TabSteps = .tab1 |
| 49 | +var steps: AppSteps = .home |
49 | 50 | ``` |
50 | | -If you want to open `Tab2` you need mark `tab2` as selected. You have several ways to do it: |
| 51 | +If you want to open `Explore` you need mark `explore` as selected. You have several ways to do it: |
51 | 52 | 1. Set `selected` property: |
52 | 53 | ```swift |
53 | | -steps.selected = .tab2 |
| 54 | +steps.selected = .explore |
54 | 55 | ``` |
55 | 56 | 2. Use auto-generated static functions: |
56 | 57 | ```swift |
57 | | -steps = .tab2(SomeTab2Data()) |
| 58 | +steps = .explore(ExploreData()) |
58 | 59 | ``` |
59 | 60 | You can check which property is selected: |
60 | 61 | 1. With `selected` property: |
61 | 62 | ```swift |
62 | | -$steps.selected == .tab2 |
| 63 | +$steps.selected == .explore |
63 | 64 | ``` |
64 | 65 | Also you can set initial selected property: |
65 | 66 | ```swift |
66 | | -var screen3: PickerSteps = .text1 |
| 67 | +var profileFlow: ProfileSteps = .main |
67 | 68 | ``` |
68 | 69 | ### Deeplink |
69 | | - Then you got a deep link for example and you need to change `Tab2` to third tab with `NavigationView`, push to `Push2View` and select `Text2` in `PickerView`. |
| 70 | + Then you got a deep link for example and you need to navigate to the `Profile` tab, push to `DetailView` and select `Dark` theme in `ThemeSelector`. |
70 | 71 | ```swift |
71 | | - steps.tab3.$screen2.select(with: .text2) |
| 72 | + steps.profile.$detail.select(with: .dark) |
72 | 73 | ``` |
73 | | - Now `tab3`, `screen3`, `text2` properties are marked as selected. |
| 74 | + Now `profile`, `detail`, `dark` properties are marked as selected. |
| 75 | + |
74 | 76 | ### Integration with UI |
75 | 77 | SwiftUI is a state driven framework, so it's easy to implement navigation with `Step`s. |
| 78 | + |
76 | 79 | #### 1. `StateStep` property wrapper. |
77 | 80 | `StateStep` updates view, stores your flow struct or binds it from parent view as an environment value. To bind flow down the view hierarchy you need use `.step(...)` or `.stepEnvironment(...)` view modifiers or initialize `StateStep` with `Binding<Step<...>>`.\ |
78 | 81 | `stepEnvironment` binds current step down the view hierarchy for embedded `StateStep` properties. |
79 | 82 | `step` modifier is just a combination of `tag` and `stepEnvironment` modifiers. |
80 | 83 | ```swift |
81 | | -struct RootTabView: View { |
| 84 | +struct MainTabView: View { |
82 | 85 |
|
83 | | - @StateStep var step: TabSteps = .tab1 |
| 86 | + @StateStep var step: AppSteps = .home |
84 | 87 |
|
85 | 88 | var body: some View { |
86 | 89 | TabView(selection: $step.selected) { |
87 | | - Tab1() |
88 | | - .step(_step.$tab1) |
| 90 | + HomeView() |
| 91 | + .step(_step.$home) |
89 | 92 |
|
90 | | - Tab2() |
91 | | - .step(_step.$tab2) |
| 93 | + ExploreView() |
| 94 | + .step(_step.$explore) |
92 | 95 |
|
93 | | - EmbededNavigation() |
94 | | - .step(_step.$tab3) |
| 96 | + ProfileNavigation() |
| 97 | + .step(_step.$profile) |
95 | 98 | } |
96 | 99 | .tabViewStyle(PageTabViewStyle(indexDisplayMode: .always)) |
97 | 100 | } |
98 | 101 | } |
99 | 102 |
|
100 | | -struct EmbededNavigation: View { |
| 103 | +struct ProfileNavigation: View { |
101 | 104 |
|
102 | | - @StateStep var step = NavigationSteps() |
| 105 | + @StateStep var step = ProfileSteps() |
103 | 106 |
|
104 | 107 | var body: some View { |
105 | 108 | NavigationView { |
106 | | - RootView { |
107 | | - NavigationLink(isActive: $step.isSelected(.screen3)) { |
108 | | - EmbededPicker() |
109 | | - .stepEnvironment($step.$screen2) |
| 109 | + ProfileView { |
| 110 | + NavigationLink(isActive: $step.isSelected(.detail)) { |
| 111 | + ThemeSelectorView() |
| 112 | + .stepEnvironment($step.$detail) |
110 | 113 | } label: { |
111 | | - Text("push") |
| 114 | + Text("Change Theme") |
112 | 115 | } |
113 | 116 | } |
114 | 117 | } |
115 | 118 | } |
116 | 119 | } |
117 | 120 |
|
118 | | -struct EmbededPicker: View { |
| 121 | +struct ThemeSelectorView: View { |
119 | 122 |
|
120 | | - @StateStep var step = PickerSteps() |
| 123 | + @StateStep var step = ThemeSteps() |
121 | 124 |
|
122 | 125 | var body: some View { |
123 | | - Picker("3", selection: $step.selected) { |
124 | | - Text("\(step.prefixString) 0") |
125 | | - .tag(PickerSteps.Steps.text1) |
| 126 | + Picker("Theme", selection: $step.selected) { |
| 127 | + Text("Light Mode") |
| 128 | + .tag(ThemeSteps.Steps.light) |
126 | 129 |
|
127 | | - Text("\(step.prefixString) 1") |
128 | | - .tag(PickerSteps.Steps.text2) |
| 130 | + Text("Dark Mode") |
| 131 | + .tag(ThemeSteps.Steps.dark) |
129 | 132 | } |
130 | 133 | .pickerStyle(WheelPickerStyle()) |
131 | 134 | } |
132 | 135 | } |
133 | 136 | ``` |
134 | | -#### 4. Binding |
| 137 | +#### 2. Binding |
135 | 138 | You can use `Step` directly without `StateStep` wrapper, in `ObservableObject` view model or as a part of state in [TCA](https://github.com/pointfreeco/swift-composable-architecture) `Store`, etc. |
136 | 139 |
|
137 | | -#### 5. UIKit |
| 140 | +#### 3. UIKit |
138 | 141 | There is no any special instrument for UIKit, because UIKit doesn't support state driven navigation, but it's possible to use Combine to subscribe on `Step` changes: |
139 | 142 | ```swift |
140 | | -let stepsSubject = CurrentValueSubject(TabSteps(.tab1)) |
| 143 | +let stepsSubject = CurrentValueSubject(AppSteps(.home)) |
141 | 144 |
|
142 | 145 | stepsSubject |
143 | 146 | .map(\.selected) |
144 | 147 | .removeDublicates() |
145 | 148 | .sink { selected in |
146 | 149 | switch selected { |
147 | | - case .tab1: |
148 | | - ... |
| 150 | + case .home: |
| 151 | + // Handle home tab selection |
| 152 | + case .explore: |
| 153 | + // Handle explore tab selection |
| 154 | + case .profile: |
| 155 | + // Handle profile tab selection |
| 156 | + default: |
| 157 | + break |
149 | 158 | } |
150 | 159 | } |
151 | 160 |
|
152 | | -stepsSubject.value.$tab2.select() |
| 161 | +stepsSubject.value.$explore.select() |
153 | 162 | ``` |
154 | 163 | or use `didSet`: |
155 | 164 | ```swift |
156 | | -var steps = TabSteps(.tab1) { |
| 165 | +var steps = AppSteps(.home) { |
157 | 166 | didSet { |
158 | 167 | guard oldValue.selected != steps.selected else { return } |
| 168 | + // Handle selection change |
159 | 169 | ... |
160 | 170 | } |
161 | 171 | } |
@@ -197,32 +207,34 @@ StepSystem.observer = MyStepsObserver() |
197 | 207 | The observer will be called whenever any step changes in the application, allowing for centralized navigation tracking. |
198 | 208 |
|
199 | 209 | ### Tools |
| 210 | + |
200 | 211 | #### `NavigationLink` convenience init |
201 | 212 | ```swift |
202 | | -@StateStep var steps = Steps() |
| 213 | +@StateStep var steps = ProfileSteps() |
203 | 214 | ... |
204 | | -NavigationLink(step: _steps.$link) { |
205 | | - ... |
| 215 | +NavigationLink(step: _steps.$detail) { |
| 216 | + ThemeSelectorView() |
206 | 217 | } label: { |
207 | | - ... |
| 218 | + Text("Change Theme") |
208 | 219 | } |
209 | 220 | ``` |
| 221 | + |
210 | 222 | #### `navigationPath()` extension on `Binding<Step<...>>` and two `navigationDestination` methods |
211 | 223 | ```swift |
212 | | -@StateStep var steps = Steps() |
| 224 | +@StateStep var steps = ProfileSteps() |
213 | 225 |
|
214 | 226 | var body: some View { |
215 | 227 | NavigationStack(path: $steps.navigationPath) { |
216 | | - RootView() |
217 | | - .navigationDestination(step: _steps.$link) { |
218 | | - PushView() |
| 228 | + ProfileView() |
| 229 | + .navigationDestination(step: _steps.$detail) { |
| 230 | + ThemeSelectorView() |
219 | 231 | } |
220 | 232 | // or |
221 | 233 | .navigationDestination(for: _steps) { |
222 | 234 | switch $0 { |
223 | | - case .link: |
224 | | - PushView() |
225 | | - .step(_step.$link) |
| 235 | + case .detail: |
| 236 | + ThemeSelectorView() |
| 237 | + .step(_steps.$detail) |
226 | 238 | default: |
227 | 239 | EmptyView() |
228 | 240 | } |
|
0 commit comments