Skip to content

Commit 92a6d5d

Browse files
committed
Enhance medium-story.md and README.md with additional visualizations for app flow structures, including onboarding and form management. Update navigation examples to reflect new struct names and improve clarity on state management in SwiftUI applications.
1 parent d564793 commit 92a6d5d

File tree

2 files changed

+132
-65
lines changed

2 files changed

+132
-65
lines changed

README.md

Lines changed: 76 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,159 +3,169 @@
33
## Description
44
This repository provides a new simple way to describe routers.\
55
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+
67
## Example
78
Take for example an application with such a hierarchy of screens:
89
```swift
910
TabView
1011
┌────────────┼────────────┐
11-
Tab1 Tab2 NavigationView
12+
Home Explore NavigationView
1213
┌────────┴────────┐
13-
RootView Push1View
14+
ProfileView DetailView
1415
15-
PickerView
16+
ThemeSelector
1617
┌───────┴───────┐
17-
Text1 Text2
18+
Light Dark
1819
```
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.
2021

2122
Describe your flow as a struct with `Step` properties:
2223
```swift
2324
@Steps
24-
struct TabSteps {
25+
struct AppSteps {
2526

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
2930
var none
3031
}
3132

3233
@Steps
33-
struct NavigationSteps {
34+
struct ProfileSteps {
3435

35-
var screen1
36-
var screen2: PickerSteps = .none
36+
var main
37+
var detail: ThemeSteps = .none
3738
}
3839

3940
@Steps
40-
struct PickerSteps {
41+
struct ThemeSteps {
4142

42-
var text1
43-
var text2
43+
var light
44+
var dark
4445
var none
4546
}
4647
```
4748
```swift
48-
var steps: TabSteps = .tab1
49+
var steps: AppSteps = .home
4950
```
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:
5152
1. Set `selected` property:
5253
```swift
53-
steps.selected = .tab2
54+
steps.selected = .explore
5455
```
5556
2. Use auto-generated static functions:
5657
```swift
57-
steps = .tab2(SomeTab2Data())
58+
steps = .explore(ExploreData())
5859
```
5960
You can check which property is selected:
6061
1. With `selected` property:
6162
```swift
62-
$steps.selected == .tab2
63+
$steps.selected == .explore
6364
```
6465
Also you can set initial selected property:
6566
```swift
66-
var screen3: PickerSteps = .text1
67+
var profileFlow: ProfileSteps = .main
6768
```
6869
### 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`.
7071
```swift
71-
steps.tab3.$screen2.select(with: .text2)
72+
steps.profile.$detail.select(with: .dark)
7273
```
73-
Now `tab3`, `screen3`, `text2` properties are marked as selected.
74+
Now `profile`, `detail`, `dark` properties are marked as selected.
75+
7476
### Integration with UI
7577
SwiftUI is a state driven framework, so it's easy to implement navigation with `Step`s.
78+
7679
#### 1. `StateStep` property wrapper.
7780
`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<...>>`.\
7881
`stepEnvironment` binds current step down the view hierarchy for embedded `StateStep` properties.
7982
`step` modifier is just a combination of `tag` and `stepEnvironment` modifiers.
8083
```swift
81-
struct RootTabView: View {
84+
struct MainTabView: View {
8285

83-
@StateStep var step: TabSteps = .tab1
86+
@StateStep var step: AppSteps = .home
8487

8588
var body: some View {
8689
TabView(selection: $step.selected) {
87-
Tab1()
88-
.step(_step.$tab1)
90+
HomeView()
91+
.step(_step.$home)
8992

90-
Tab2()
91-
.step(_step.$tab2)
93+
ExploreView()
94+
.step(_step.$explore)
9295

93-
EmbededNavigation()
94-
.step(_step.$tab3)
96+
ProfileNavigation()
97+
.step(_step.$profile)
9598
}
9699
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
97100
}
98101
}
99102

100-
struct EmbededNavigation: View {
103+
struct ProfileNavigation: View {
101104

102-
@StateStep var step = NavigationSteps()
105+
@StateStep var step = ProfileSteps()
103106

104107
var body: some View {
105108
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)
110113
} label: {
111-
Text("push")
114+
Text("Change Theme")
112115
}
113116
}
114117
}
115118
}
116119
}
117120

118-
struct EmbededPicker: View {
121+
struct ThemeSelectorView: View {
119122

120-
@StateStep var step = PickerSteps()
123+
@StateStep var step = ThemeSteps()
121124

122125
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)
126129

127-
Text("\(step.prefixString) 1")
128-
.tag(PickerSteps.Steps.text2)
130+
Text("Dark Mode")
131+
.tag(ThemeSteps.Steps.dark)
129132
}
130133
.pickerStyle(WheelPickerStyle())
131134
}
132135
}
133136
```
134-
#### 4. Binding
137+
#### 2. Binding
135138
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.
136139

137-
#### 5. UIKit
140+
#### 3. UIKit
138141
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:
139142
```swift
140-
let stepsSubject = CurrentValueSubject(TabSteps(.tab1))
143+
let stepsSubject = CurrentValueSubject(AppSteps(.home))
141144

142145
stepsSubject
143146
.map(\.selected)
144147
.removeDublicates()
145148
.sink { selected in
146149
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
149158
}
150159
}
151160

152-
stepsSubject.value.$tab2.select()
161+
stepsSubject.value.$explore.select()
153162
```
154163
or use `didSet`:
155164
```swift
156-
var steps = TabSteps(.tab1) {
165+
var steps = AppSteps(.home) {
157166
didSet {
158167
guard oldValue.selected != steps.selected else { return }
168+
// Handle selection change
159169
...
160170
}
161171
}
@@ -197,32 +207,34 @@ StepSystem.observer = MyStepsObserver()
197207
The observer will be called whenever any step changes in the application, allowing for centralized navigation tracking.
198208

199209
### Tools
210+
200211
#### `NavigationLink` convenience init
201212
```swift
202-
@StateStep var steps = Steps()
213+
@StateStep var steps = ProfileSteps()
203214
...
204-
NavigationLink(step: _steps.$link) {
205-
...
215+
NavigationLink(step: _steps.$detail) {
216+
ThemeSelectorView()
206217
} label: {
207-
...
218+
Text("Change Theme")
208219
}
209220
```
221+
210222
#### `navigationPath()` extension on `Binding<Step<...>>` and two `navigationDestination` methods
211223
```swift
212-
@StateStep var steps = Steps()
224+
@StateStep var steps = ProfileSteps()
213225

214226
var body: some View {
215227
NavigationStack(path: $steps.navigationPath) {
216-
RootView()
217-
.navigationDestination(step: _steps.$link) {
218-
PushView()
228+
ProfileView()
229+
.navigationDestination(step: _steps.$detail) {
230+
ThemeSelectorView()
219231
}
220232
// or
221233
.navigationDestination(for: _steps) {
222234
switch $0 {
223-
case .link:
224-
PushView()
225-
.step(_step.$link)
235+
case .detail:
236+
ThemeSelectorView()
237+
.step(_steps.$detail)
226238
default:
227239
EmptyView()
228240
}

medium-story.md

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ SwiftUI developers are familiar with the challenge: your UI design is complete,
66

77
Consider a fairly standard app structure:
88

9+
```swift
10+
TabView
11+
┌──────────────┼──────────────┐
12+
Home Search Profile
13+
14+
NavigationView NavigationView NavigationView
15+
16+
FeedView ResultsView ProfileView
17+
18+
DetailView SettingsView
19+
20+
EditView
21+
```
22+
923
- A main tab view (Home, Search, Profile)
1024
- A navigation stack in each tab
1125
- Detail screens with their own state
@@ -133,6 +147,15 @@ This enables:
133147

134148
Here's how the same navigation approach applies to an onboarding flow:
135149

150+
```swift
151+
OnboardingFlow
152+
┌─────────┼───────────┬─────────┐
153+
Welcome Permissions Profile Complete
154+
155+
┌──┴──┐
156+
Info Request
157+
```
158+
136159
**Traditional Approach:**
137160
```swift
138161
struct OnboardingCoordinator: View {
@@ -249,6 +272,20 @@ flow.selected = .home
249272
flow = .home(.feed(.reset)) // Using enum-like static functions
250273
```
251274

275+
This structure can be visualized as:
276+
277+
```swift
278+
AppFlow
279+
┌───────┼───────┐
280+
Home Search Profile
281+
282+
Feed Main
283+
284+
Detail Settings
285+
286+
Edit
287+
```
288+
252289
This preservation of state is crucial for maintaining form data, scroll positions, or other UI state when navigating between screens. When a complete reset is needed, the `@Steps` macro generates enum-like static functions for convenient initialization with new values.
253290

254291
## Beyond Screen Navigation
@@ -265,6 +302,11 @@ struct FormFlow {
265302
var review
266303
}
267304

305+
// Visualized as:
306+
// FormFlow
307+
// ┌─────────┼───────┬──────┐
308+
// Personal Address Payment Review
309+
268310
// Controlling UI components within a single screen
269311
@Steps
270312
struct MapViewState {
@@ -274,6 +316,11 @@ struct MapViewState {
274316
var locationDetails: LocationInfo?
275317
}
276318

319+
// Visualized as:
320+
// MapViewState
321+
// ┌────────────┼────────┬────────────┐
322+
// Standard Satellite Traffic LocationDetails
323+
277324
// Managing design system components
278325
@Steps
279326
struct ExpandableCardState {
@@ -286,6 +333,14 @@ struct ExpansionState {
286333
var basic
287334
var detailed
288335
}
336+
337+
// Visualized as:
338+
// ExpandableCardState
339+
// ┌────┴────┐
340+
// Collapsed Expanded
341+
//
342+
// ┌───┴────┐
343+
// Basic Detailed
289344
```
290345

291346
This separation of navigation state from UI presentation means VDFlow can be used for:
@@ -309,7 +364,7 @@ dependencies: [
309364
## Performance Considerations
310365

311366
VDFlow adds minimal overhead to an application:
312-
- Small binary footprint (~100KB)
367+
- Small binary footprint
313368
- No background processing
314369
- No additional memory pressure
315370
- No impact on app startup time

0 commit comments

Comments
 (0)