Skip to content

Commit b3d3210

Browse files
authored
Update README.md
1 parent 80a89b1 commit b3d3210

File tree

1 file changed

+252
-6
lines changed

1 file changed

+252
-6
lines changed

README.md

Lines changed: 252 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,268 @@ ViewStateController is a framework for Swift and SwiftUI developers that provide
66

77
There is an Example app available [here](https://github.com/mdb1/ViewStateControllerExampleApp), where most of the configuration options can be tweaked.
88

9-
// TODO: Upload a Video of the Example App
10-
119
# ViewStateController Object
1210

13-
# WithViewState Modifier
11+
The ViewStateController struct is the one that contains the array of historical [ViewStates](https://mdb1.github.io/2023-01-08-new-app-view-state/) and has computed properties that will be used by the ViewStateModifier to determine what to do.
12+
13+
* `isInitialLoading`: Returns true only if loading state was set once and there hasn't been errors or info yet.
14+
* `isLoading`: Returns true if state is loading.
15+
* `latestValidInfo`: Info associated to the last time `loaded` state was set. Nil if there has been an error after the latest info.
16+
* `latestInfo`: Info associated to the last time `loaded` state was set, disregarding if there has been an error afterwards.
17+
* `latestValidError`: Info associated to the last time `error` state was set. Nil if `info` has been loaded after the latest error.
18+
* `latestError`: Info associated to the last time loaded `error` was set, disregarding if there has been an error afterwards.
19+
* `latestNonLoading`: Returns the latest informational state (info, or error) if exists. Nil otherwise.
20+
21+
There are also two mutating methods:
22+
23+
* `setState(_ state: ViewState<Info>)`: Sets the new state into the states array.
24+
* `reset()`: Resets everything.
25+
26+
# ViewStateModifier
27+
28+
The ViewStateModifier is a ViewModifier that uses the given ViewStateController and configurable options to automatically update the state of a view.
29+
30+
The code of the modifier is pretty straight forward:
31+
```swift
32+
func body(content: Content) -> some View {
33+
if controller.isInitialLoading {
34+
// Initial loading modifier displayed on the initial loading state.
35+
content.modifier(initialLoadingModifier)
36+
} else if let info = controller.latestValidInfo {
37+
// If we have valid info loaded we display it:
38+
loadedView(info)
39+
.if(controller.isLoading) { view in
40+
// If we are on a subsequent loading, we add the modifier.
41+
view.modifier(loadingAfterInfoModifier)
42+
}
43+
} else if let error = controller.latestValidError {
44+
// If we have a value error we display it:
45+
errorView(error)
46+
.if(controller.isLoading) { view in
47+
// If we are on a subsequent loading, we add the modifier.
48+
view.modifier(loadingAfterErrorModifier)
49+
}
50+
} else {
51+
// Otherwise, we display the initial content.
52+
content
53+
}
54+
}
55+
```
56+
57+
The method `withViewStateModifier`, is just a convenience way to add the ViewStateModifier to any view:
58+
59+
```swift
60+
/// Adds a view state modifier that can display different views depending on the state of a `ViewStateController`.
61+
/// - Parameters:
62+
/// - controller: The `ViewStateController` that controls the state of the view.
63+
/// - indicatorView: The view to show when the view is loading.
64+
/// - initialLoadingType: The type of loading indicator to show when the view is initially loading.
65+
/// - loadedView: The view to show when the view is not loading and has valid information.
66+
/// - loadingAfterInfoType: The type of loading indicator to show when the view is loading after it has already
67+
/// displayed valid information.
68+
/// - errorView: The view to show when the view has an error.
69+
/// - loadingAfterErrorType: The type of loading indicator to show when the view is loading after it has displayed
70+
/// an error.
71+
func withViewStateModifier<Info, IndicatorView: View, LoadedView: View>(
72+
controller: ViewStateController<Info>,
73+
indicatorView: IndicatorView = ProgressView(),
74+
initialLoadingType: LoadingModifierType = .material(),
75+
loadedView: @escaping (Info) -> LoadedView,
76+
loadingAfterInfoType: LoadingModifierType = .horizontal(),
77+
errorView: @escaping (Error) -> ErrorView,
78+
loadingAfterErrorType: LoadingModifierType = .overCurrentContent(alignment: .trailing)
79+
) -> some View
80+
```
81+
82+
## Usage
83+
84+
The ideal usage would be to:
85+
86+
1. Decide your strategy for `initialLoading`, `loadingAfterInfo`, `errorView`, and `loadingAfterError` states.
87+
2. Create the placeholder view (The one that will be there before the initial loading). (This could be an `EmptyView()` or the `loadedView` with a `.redacted` modifier).
88+
3. Create the `loadedView`.
89+
4. Decide if the error state will have a retry action or not.
1490

1591
## Examples with code samples
1692

17-
TODO: Upload videos here
93+
Let's take a look at how we can use it in our views:
94+
95+
We will be using this view and controller in our examples:
96+
```swift
97+
@State var controller: ViewStateController<User> = .init()
98+
99+
...
100+
101+
struct User {
102+
let name: String
103+
let age: Int
104+
let emoji: String
105+
}
106+
107+
...
108+
109+
func loadedView(user: User) -> some View {
110+
HStack(spacing: 8) {
111+
ZStack {
112+
Circle()
113+
.frame(width: 50, height: 50)
114+
.foregroundColor(Color.gray.opacity(0.2))
115+
Text(user.emoji)
116+
}
117+
VStack(alignment: .leading, spacing: 8) {
118+
Text("Name: \(user.name)")
119+
Text("Age: \(user.age.description)")
120+
}
121+
Spacer()
122+
}
123+
}
124+
```
125+
126+
### Redacted
127+
128+
```swift
129+
loadedView(user: .init(name: "Placeholder", age: 99, emoji: "")) // 1. Create a placeholder view
130+
.redacted(reason: .placeholder) // 2. Use redacted to hide the texts
131+
.withViewStateModifier( // 3. Apply view modifier
132+
controller: controller
133+
) { user in
134+
loadedView(user: user) // 4. Provide the view for the loaded information
135+
} errorView: { _ in // 5. Provide an error view
136+
.init { setLoading() }
137+
}
138+
```
139+
140+
https://user-images.githubusercontent.com/5333984/222929861-695a50d0-d503-4f03-a69e-da70c7027e5c.mov
141+
142+
Since we are not changing the values for the loading types it's using the default values:
143+
144+
* `.material()` for the initial loading
145+
* `.horizontal()` for the loading after info type
146+
* `.overCurrentContent(alignment: .trailing)` for the loading after error type
147+
148+
### Changing the indicator view
149+
150+
If you have a custom progress view, you can use it in the `indicatorView` parameter. Example from [this post](https://mdb1.github.io/2023-01-04-new-app-components/):
151+
152+
```swift
153+
loadedView(user: .init(name: "Placeholder", age: 99, emoji: ""))
154+
.redacted(reason: .placeholder)
155+
.withViewStateModifier(
156+
controller: controller,
157+
indicatorView: SpinnerProgressView(color: .green)
158+
) { user in
159+
loadedView(user: user)
160+
} errorView: { _ in
161+
.init { setLoading() }
162+
}
163+
```
164+
165+
https://user-images.githubusercontent.com/5333984/222929864-76133f77-25b3-49ac-81b6-38cdb443324e.mov
166+
167+
### Changing Loading types
168+
169+
By changing the `initialLoadingType`, `loadingAfterInfoType`, or `loadingAfterErrorType` you can provide different ways of displaying the loading states.
170+
171+
You can find a list of the possible options [here](https://github.com/mdb1/ViewStateController/blob/main/Sources/ViewStateController/ViewModifiers/LoadingViewModifier.swift)
172+
173+
Example:
174+
```swift
175+
loadedView(user: .init(name: "Placeholder", age: 99, emoji: ""))
176+
.redacted(reason: .placeholder)
177+
.withViewStateModifier(
178+
controller: controller,
179+
indicatorView: SpinnerProgressView(color: .purple), // Mark 1
180+
initialLoadingType: .vertical(option: .bottom, alignment: .center), // Mark 2
181+
loadedView: { user in
182+
loadedView(user: user)
183+
},
184+
loadingAfterInfoType: .horizontal(option: .leading, contentOpacity: 0.3, alignment: .center, spacing: 32), // Mark 3
185+
errorView: { _ in .init { setLoading() } },
186+
loadingAfterErrorType: .overCurrentContent(contentOpacity: 0.5, alignment: .bottomTrailing) // Mark 4
187+
)
188+
```
189+
190+
In this example:
191+
192+
* Mark 1: We are changing the indicator view to use a custom one.
193+
* Mark 2: We are changing the initial loading type, to use a VStack with the indicator at the bottom and center alignment.
194+
* Mark 3: We are changing the loading after info type, to use an HStack with the indicator in the leading position, 0.3 as the opacity for the content, center alignment, and 32 of spacing.
195+
* Mark 4: We are changing the loading after error type to use the `overCurrentContent` type with 0.5 for the content opacity, and bottomTrailing alignment.
196+
197+
https://user-images.githubusercontent.com/5333984/222929867-8e1afc93-dc0b-47c3-8014-3a9cda76b02d.mov
198+
199+
### Using Custom Views
200+
201+
Let's say that the screen/view you are working on requires some special views for each state. You could use the `.custom` type for any of the states:
202+
203+
```swift
204+
EmptyView() // Mark 1
205+
.withViewStateModifier(
206+
controller: controller,
207+
indicatorView: SpinnerProgressView(color: .orange), // Mark 2
208+
initialLoadingType: .custom( // Mark 3
209+
VStack {
210+
Text("This is the initial loading")
211+
SpinnerProgressView(color: .blue, size: 50, lineWidth: 5)
212+
}.asAnyView()
213+
),
214+
loadedView: { user in
215+
loadedView(user: user)
216+
},
217+
loadingAfterInfoType: .custom( // Mark 4
218+
HStack {
219+
Image(systemName: "network")
220+
Text("I got info, but I am loading again")
221+
SpinnerProgressView(color: .black)
222+
}.asAnyView()
223+
),
224+
errorView: { error in // Mark 5
225+
.init(type: .custom(
226+
VStack {
227+
Text("I got an error")
228+
Text(error.localizedDescription)
229+
}
230+
.foregroundColor(.red)
231+
.asAnyView()
232+
))
233+
},
234+
loadingAfterErrorType: .custom( // Mark 6
235+
HStack {
236+
Image(systemName: "network")
237+
Text("I got info, but I am loading again")
238+
SpinnerProgressView(color: .red)
239+
}
240+
.foregroundColor(.red)
241+
.asAnyView()
242+
))
243+
```
244+
245+
In this example:
246+
247+
* Mark 1: We are using an EmptyView as the placeholder view.
248+
* Mark 2: We are changing the indicator view to use a custom one.
249+
* Mark 3: We are changing the initial loading type, to use a custom view.
250+
* Mark 4: We are changing the loading after info type, to use a custom view.
251+
* Mark 5: We are changing the error view, to use a custom view.
252+
* Mark 6: We are changing the loading after error type, to use a custom view.
253+
254+
https://user-images.githubusercontent.com/5333984/222929869-8eec3ce7-ce59-491c-a46d-824243b348e9.mov
255+
256+
## Demo: Loading Type Options
257+
258+
https://user-images.githubusercontent.com/5333984/222930157-f1c0ef5d-4383-4073-87c2-9d98e2f09c29.mp4
259+
260+
In this video, we are tweaking around some properties and pass them to the `withViewStateModifier` to demonstrate the different loading and error states that comes for free. Everything is configurable, and there is also the ability to provide custom views for loading states, the indicator, and the error states.
261+
262+
The app used for this video can be downloaded from [this repository](https://github.com/mdb1/ViewStateControllerExampleApp).
18263

19264
# Toast
20265

21-
## Examples with code samples
266+
// TODO: Explain the Toast functionality
22267

23-
TODO: Upload videos here
268+
## Examples with code samples
24269

270+
// TODO: Upload videos of toasts/snackBars here
25271

26272
# Internal Project Tools
27273

0 commit comments

Comments
 (0)