Skip to content

Commit 8de9220

Browse files
committed
Add medium-story.md to document a practical approach to SwiftUI navigation using VDFlow, highlighting common challenges, tree structure modeling, deep linking, state persistence, and implementation details.</message>
1 parent 4f1da41 commit 8de9220

File tree

1 file changed

+279
-0
lines changed

1 file changed

+279
-0
lines changed

medium-story.md

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
# Taming SwiftUI Navigation: A Practical Approach with VDFlow
2+
3+
SwiftUI developers are familiar with the challenge: your UI design is complete, components are built, but then comes the navigation logic. What starts as a simple implementation quickly grows into a complex system of state variables, conditional views, and navigation paths.
4+
5+
## Common Navigation Challenges in SwiftUI
6+
7+
Consider a fairly standard app structure:
8+
9+
- A main tab view (Home, Search, Profile)
10+
- A navigation stack in each tab
11+
- Detail screens with their own state
12+
- Modals appearing contextually
13+
- Deep linking capabilities
14+
15+
The conventional approach often leads to code like this:
16+
17+
```swift
18+
struct ContentView: View {
19+
@State private var selectedTab = 0
20+
@State private var showingHomeDetail = false
21+
@State private var homeNavigationPath = NavigationPath()
22+
@State private var searchNavigationPath = NavigationPath()
23+
@State private var profileNavigationPath = NavigationPath()
24+
@State private var showingSettings = false
25+
@State private var showingProfileEdit = false
26+
// And so on...
27+
28+
// Functions to manage state
29+
func navigateToHomeDetail() { ... }
30+
func resetSearchStack() { ... }
31+
func handleDeepLink(to destination: DeepLink) {
32+
// Extensive conditional logic
33+
}
34+
}
35+
```
36+
37+
As the app grows, this approach becomes increasingly difficult to maintain. Each screen needs to manage its own state, pass bindings around, and coordinate with parent views.
38+
39+
## Navigation as a Tree Structure
40+
41+
VDFlow takes a different approach by modeling navigation as a tree of possible states. Unlike many similar libraries that use enums for navigation state, VDFlow uses structs:
42+
43+
```swift
44+
@Steps
45+
struct AppFlow {
46+
var home: HomeFlow = .feed
47+
var search
48+
var profile: ProfileFlow = .none
49+
}
50+
51+
@Steps
52+
struct HomeFlow {
53+
var feed
54+
var detail: PostDetail = .none
55+
}
56+
57+
@Steps
58+
struct ProfileFlow {
59+
var main
60+
var edit
61+
var settings
62+
var none
63+
}
64+
```
65+
66+
With this structure, the entire app's navigation state is consolidated into a coherent model:
67+
68+
```swift
69+
struct ContentView: View {
70+
@StateStep var flow = AppFlow.home
71+
72+
var body: some View {
73+
TabView(selection: $flow.selected) {
74+
HomeView()
75+
.step(_flow.$home)
76+
77+
SearchView()
78+
.step(_flow.$search)
79+
80+
ProfileView()
81+
.step(_flow.$profile)
82+
}
83+
}
84+
}
85+
```
86+
87+
## Simplified Deep Linking
88+
89+
With navigation modeled as a tree, deep linking becomes straightforward:
90+
91+
```swift
92+
func handleDeepLink(to destination: DeepLink) {
93+
switch destination {
94+
case .profile:
95+
flow.selected = .profile
96+
case .postDetail(let postID):
97+
flow.home.detail.select(with: PostDetail(id: postID))
98+
case .settings:
99+
flow.profile.select(with: .settings)
100+
}
101+
}
102+
```
103+
104+
## State Persistence with Codable
105+
106+
Since all steps conform to `Codable` by default, navigation state can be persisted and restored:
107+
108+
```swift
109+
// Save current navigation state
110+
func saveNavigationState() {
111+
let encoder = JSONEncoder()
112+
if let data = try? encoder.encode(flow) {
113+
UserDefaults.standard.set(data, forKey: "savedNavigation")
114+
}
115+
}
116+
117+
// Restore navigation state
118+
func restoreNavigationState() {
119+
if let data = UserDefaults.standard.data(forKey: "savedNavigation"),
120+
let savedFlow = try? JSONDecoder().decode(AppFlow.self, from: data) {
121+
flow = savedFlow
122+
}
123+
}
124+
```
125+
126+
This enables:
127+
- Navigation state persistence between app launches
128+
- Handling app termination
129+
- "Continue where you left off" functionality
130+
- Bookmarkable states within the app
131+
132+
## Practical Example: Onboarding Flow
133+
134+
Here's how the same navigation approach applies to an onboarding flow:
135+
136+
**Traditional Approach:**
137+
```swift
138+
struct OnboardingCoordinator: View {
139+
@State private var currentStep = 0
140+
@State private var showingPermissionsRequest = false
141+
@State private var permissionsGranted = false
142+
@State private var userProfile: UserProfile?
143+
@State private var showingProfileCreation = false
144+
145+
var body: some View {
146+
if currentStep == 0 {
147+
WelcomeView(proceed: { currentStep = 1 })
148+
} else if currentStep == 1 {
149+
PermissionsInfoView(
150+
requestPermissions: { showingPermissionsRequest = true }
151+
)
152+
.sheet(isPresented: $showingPermissionsRequest) {
153+
RequestPermissionsView(granted: {
154+
permissionsGranted = true
155+
currentStep = 2
156+
})
157+
}
158+
} else if currentStep == 2 {
159+
if userProfile == nil {
160+
ProfileCreationView(profile: { profile in
161+
userProfile = profile
162+
currentStep = 3
163+
})
164+
} else {
165+
FinalOnboardingView(complete: { /* Complete onboarding */ })
166+
}
167+
}
168+
}
169+
}
170+
```
171+
172+
**With VDFlow:**
173+
```swift
174+
@Steps
175+
struct OnboardingFlow {
176+
var welcome
177+
var permissions: PermissionsStep = .info
178+
var profile: UserProfile?
179+
var complete
180+
}
181+
182+
@Steps
183+
struct PermissionsStep {
184+
var info
185+
var request
186+
}
187+
188+
struct OnboardingCoordinator: View {
189+
@StateStep var flow = OnboardingFlow.welcome
190+
191+
var body: some View {
192+
switch flow.selected {
193+
case .welcome:
194+
WelcomeView(proceed: { flow.selected = .permissions })
195+
196+
case .permissions:
197+
NavigationView {
198+
PermissionsInfoView()
199+
.navigationDestination(isPresented: $flow.permissions.isSelected(.request)) {
200+
RequestPermissionsView(granted: {
201+
flow.selected = .profile
202+
})
203+
}
204+
}
205+
206+
case .profile:
207+
ProfileCreationView(created: { profile in
208+
flow.profile.select(with: profile)
209+
flow.selected = .complete
210+
})
211+
212+
case .complete:
213+
FinalOnboardingView()
214+
}
215+
}
216+
}
217+
```
218+
219+
This structure makes it easier to navigate to any point in the flow for testing or to handle external events.
220+
221+
## Key Benefits
222+
223+
VDFlow offers several advantages:
224+
225+
1. **Unified navigation state** - Navigation logic is centralized
226+
2. **Data-driven approach** - Navigation becomes a value type
227+
3. **Composable flows** - Nested navigations work together
228+
4. **Natural deep linking** - Tree structure facilitates deep linking
229+
5. **SwiftUI integration** - Works with native SwiftUI patterns
230+
6. **Lightweight implementation** - Small binary size (~100KB) with minimal overhead
231+
232+
Unlike some navigation solutions that add significant binary weight or require restructuring an entire app, VDFlow is focused on solving the navigation problem specifically, with minimal overhead.
233+
234+
A key architectural choice is using structs rather than enums (common in other navigation libraries). This means VDFlow preserves values of unselected steps, changing only the selection key without affecting attached values. For example:
235+
236+
```swift
237+
// Initial state
238+
var flow = AppFlow.home(.feed)
239+
240+
// Navigate to profile
241+
flow.selected = .profile
242+
243+
// Later, navigate back to home - the feed state is preserved
244+
flow.selected = .home
245+
// flow.home.selected is still .feed
246+
247+
// When you need to reset state along with selection:
248+
flow = .home(.feed(.reset)) // Using enum-like static functions
249+
```
250+
251+
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.
252+
253+
## Implementation Details
254+
255+
Adding VDFlow to a project is straightforward with Swift Package Manager:
256+
257+
```swift
258+
dependencies: [
259+
.package(url: "https://github.com/dankinsoid/VDFlow.git", from: "4.31.0")
260+
]
261+
```
262+
263+
## Performance Considerations
264+
265+
VDFlow adds minimal overhead to an application:
266+
- Small binary footprint (~100KB)
267+
- No background processing
268+
- No additional memory pressure
269+
- No impact on app startup time
270+
271+
## Conclusion
272+
273+
SwiftUI navigation doesn't have to be complex. By modeling navigation as a tree of states instead of scattered boolean flags, VDFlow provides a structured approach to what is often a challenging aspect of SwiftUI development.
274+
275+
The library focuses on simplifying navigation management while remaining lightweight and performant, making it suitable for both small projects and production applications with complex navigation requirements.
276+
277+
---
278+
279+
*What navigation challenges have you faced in SwiftUI? Have you tried tree-based navigation approaches? Share your experiences in the comments.*

0 commit comments

Comments
 (0)