/
ContentView.swift
217 lines (188 loc) · 6.01 KB
/
ContentView.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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
//
// ContentView.swift
// EnvironmentDependencies
//
// Created by Lucas van Dongen on 15/03/2024.
//
import SharedDependencies
import SwiftUI
@MainActor
struct AuthenticatedView: View {
@Environment(\.logInSwitcher) private var logInSwitcher: LogInSwitching
@State private var isLoggingOut = false
var body: some View {
VStack {
// Here, we need to assume the right environment values were set the moment we got the token
// If we would have multiple Views consuming the same dependencies, it would save a lot of work
// When not set, we would have a crash here
UserManagementView()
StoriesView()
Button {
isLoggingOut = true
} label: {
Text("Log Out")
}.task(id: isLoggingOut, priority: .high) {
guard isLoggingOut else {
return
}
defer {
isLoggingOut = false
}
await logInSwitcher.loggedOut()
}
}
}
}
@MainActor
struct UserManagementView: View {
private enum ViewState: Equatable {
case loaded
case updating
case failed(reason: String)
case updated
}
@State private var state: ViewState = .loaded
@Environment(\.userManager) private var userManager: UserManaging
var body: some View {
VStack {
Text("You are logged in with token \(userManager.token)")
switch state {
case .loaded:
Button {
state = .updating
} label: {
Text("Update User")
}
case .updating:
Text("Updating User...")
case let .failed(reason):
Text("Failed updating User:\n\(reason)")
case .updated:
Text("You have updated your user successfully")
}
}.task(id: state, priority: .high) {
guard state == .updating else {
return
}
do {
_ = try await userManager.update(user: "")
state = .updated
} catch {
// Ignore for now
}
}
}
}
@MainActor
struct StoriesView: View {
private enum LoadingState {
case fetching
case failed(reason: String)
case fetched(stories: [Story])
}
@State private var state: LoadingState = .fetching
@Environment(StoryFetcher.self) private var storyFetcher: StoryFetcher
var body: some View {
switch state {
case .fetching:
Text("Fetching stories...")
.task {
state = .fetching
do {
let stories = try await storyFetcher.fetchStories()
state = .fetched(stories: stories) // You can safely mutate state properties from any thread.
} catch let error {
state = .failed(reason: error.localizedDescription)
}
}
case let .failed(reason):
Text("Failed fetching stories:\n\(reason)")
case let .fetched(stories):
List {
ForEach(stories, id: \.name) { story in
Text("Author: \(story.author)\nName: \(story.name)")
}
}
}
}
}
@MainActor
struct LogInView: View {
@Environment(\.authentication) private var authentication: Authenticating
@Environment(\.logInSwitcher) private var logInSwitcher: LogInSwitching
@State private var isAuthenticating = false
var body: some View {
switch isAuthenticating {
case true:
Text("Authenticating...")
case false:
VStack {
Text("You are logged out now")
Button {
Task.detached {
await authenticate()
}
} label: {
Text("Log In")
}
}
}
}
private func authenticate() async {
isAuthenticating = true
defer {
isAuthenticating = false
}
do {
let token = try await authentication.authenticate()
await logInSwitcher.tokenChannel.send(token)
} catch {
// Not implemented
}
}
}
@MainActor
class AppViewModel: ObservableObject {
@Published private(set) var state: AppState = .loggedOut
init(logInSwitcher: LogInSwitching) {
Task.detached { [weak self] in
await self?.observe(logInSwitcher: logInSwitcher)
}
}
private func observe(logInSwitcher: LogInSwitching) async {
for await token in logInSwitcher.tokenChannel {
state = switch token.isEmpty {
case true: .loggedOut
case false: .authenticated(token: token)
}
}
}
}
@MainActor
struct AppView: View {
@StateObject var viewModel: AppViewModel
var body: some View {
switch viewModel.state {
case let .authenticated(token):
// AppView is now responsible for passing the right dependencies into `environment` or `environmentObject`
// Failing to set the `UserManager` will result in the `PlaceholderUserManager` being used
// Failing to set the `StoryFetcher` will result in a crash when it's accessed
// It does work really well to keep certain dependencies only in one part of the tree
AuthenticatedView()
.environment(\.userManager, UserManager(token: token))
.environment(StoryFetcher(token: token))
case .loggedOut:
LogInView()
}
}
}
@MainActor
struct ContentView: View {
@Environment(\.logInSwitcher) private var logInSwitcher: LogInSwitching
var body: some View {
AppView(viewModel: AppViewModel(logInSwitcher: logInSwitcher))
}
}
#Preview {
ContentView()
}