You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+20-10Lines changed: 20 additions & 10 deletions
Original file line number
Diff line number
Diff line change
@@ -27,13 +27,13 @@ The following are code excerpts of a feature that shows a blog entry from a data
27
27
28
28
### State Definition
29
29
30
-
The state is usually defined as an enum or a struct and represents the states that the view can have. It also declares the data and actions available for each model. Actions return one or more new states.
30
+
The state is usually defined as an enum or a struct and represents the states that the view can have. It also declares the data and actions available to the view for each model. Actions return one or more new states.
Copy file name to clipboardExpand all lines: Sources/VSM/Documentation.docc/ComprehensiveGuide/VSMOverview.md
+3-1Lines changed: 3 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -16,7 +16,9 @@ In VSM, the various responsibilities of a feature are divided into 3 concepts:
16
16
17
17
The structure of your code should follow the above pattern, with a view code file, a view state code file, and a file for each model's implementation.
18
18
19
-
Optionally, due to the reactive nature of VSM, Observable Repositories are an excellent companion to VSM models in performing data operations (such as loading, saving, etc.) whose results can be forwarded to the view. These repositories can be shared between views for a powerful, yet safe approach to synchronizing the state of various views and data in the app.
19
+
Thanks to the reactive nature of VSM, Observable Repositories are an excellent companion to VSM models in performing data operations (such as loading, saving, etc.) and managing the state of data. This data can then be forwarded to the view via Combine Publishers.
20
+
21
+
These repositories can also be shared between views to synchronize the state of various views and data in the app. While simple features may not need these repositories, they are an excellent tool for complex features. You'll learn more about these later in the guide.
We are required by the ``ViewStateRendering`` protocol to define a ``StateContainer``property and specify what the view state's type will be. In these examples, we will use the `LoadUserProfileViewState` and `EditUserProfileViewState` types from <doc:StateDefinition> to build two related VSM views.
27
+
To turn any view into a "VSM View", define a property that holds our current state and decorate it with the ``ViewState`` (`@ViewState`) property wrapper.
28
28
29
-
In SwiftUI, the `view` property is evaluated and the view is redrawn _every time the state changes_. In addition, any time a dynamic property changes, the `view`property will be reevaluated and redrawn. This includes properties wrapped with `@StateObject`, `@State`, `@ObservedObject`, and `@Binding`.
29
+
**The `@ViewState` property wrapper updates the view every time the state changes**. It works in the same way as other SwiftUI property wrappers (i.e., `@StateObject`, `@State`, `@ObservedObject`, and `@Binding`).
30
30
31
-
> Note: In SwiftUI, a view's initializer is called every time its parent view is updated and redrawn.
32
-
>
33
-
> The `@StateObject` property wrapper is the safest choice for declaring your `StateContainer` property. A `StateObject`'s current value is maintained by SwiftUI between redraws of the parent view. In contrast, `@ObservedObject`'s value is not maintained between redraws of the parent view, so it should only be used in scenarios where the view state can be safely recovered every time the parent view is redrawn.
34
-
35
-
## Displaying the State
31
+
As with other SwiftUI property wrappers, when the wrapped value (state) changes, the view's `body` property is reevaluated and the result is drawn on the screen.
36
32
37
-
The ``ViewStateRendering`` protocol provides a few properties and functions that help with displaying the current state, accessing the state data, and invoking actions.
33
+
In the following examples, we will use the `LoadUserProfileViewState` and `EditUserProfileViewState` types from <doc:StateDefinition> to build two related VSM views.
38
34
39
-
The first of these members is the ``ViewStateRendering/state`` property, which is always set to the current state.
35
+
## Displaying the State
40
36
41
37
As a refresher, the following flow chart expresses the requirements that we wish to draw in the view.
In SwiftUI, we simply write a switch statement within the `view` property to evaluate the current state and return the most appropriate view(s) for it.
63
+
In SwiftUI, we write a switch statement within the `body` property to evaluate the current state and draw the most appropriate content for it.
68
64
69
65
Note that if you avoid using a `default` case in your switch statement, the compiler will enforce any future changes to the shape of your feature. This is good because it will help you avoid bugs when maintaining the feature.
70
66
71
-
The resulting `view` property implementation takes this shape:
67
+
The resulting `body` property implementation takes this shape:
To render this editing form, we require an extra property be added to the SwiftUI view to keep track of what the user types for the "Username" field.
116
+
To render this editing form, we need a property that keeps track of what the user types for the "Username" field. A `@State` property called "username" will do nicely.
Since the root type of this view state is a struct instead of an enum, and this view has a more complicated hierarchy, you'll notice that we don't use a switch statement. Instead, we place components where they need to go and sprinkle in logic within areas of the view, as necessary.
165
+
Since the root type of this view state is a `struct` instead of an `enum`, and this view has a more complicated hierarchy, you'll notice that we don't use a switch statement. Instead, we place components where they need to go and sprinkle in logic within areas of the view, as necessary.
170
166
171
167
Additionally, you'll notice that there is a reference to a previously unknown view state member in the property wrapper `.disabled(state.isSaving)`. Due to the programming style used in SwiftUI APIs, we sometimes have to extend our view state to transform its shape to work better with SwiftUI views. We define these in view state extensions so that we can preserve the type safety of our feature shape, while reducing the friction when working with specific view APIs.
Now that we have our view states rendering correctly, we need to wire up the various actions in our views so that they are appropriately and safely invoked by the environment or the user.
206
202
207
-
VSM's ``ViewStateRendering`` protocolprovides a critically important function called ``ViewStateRendering/observe(_:)-7vht3``. This function updates the current state with all view states emitted by the action parameter, as they are emitted in real-time.
203
+
VSM's ``ViewState`` property wrapper provides a critically important function called ``StateObserving/observe(_:)-31ocs`` through its projected value (`$`). This function updates the current state with all view states emitted by an action, as they are emitted in real-time.
208
204
209
205
It is called like so:
210
206
211
207
```swift
212
-
observe(someState.someAction())
213
-
// or
214
-
observe(someState.someAction)
208
+
$state.observe(someState.someAction())
215
209
```
216
210
217
-
The only way to update the current view state is to use the `observe(_:)`function.
211
+
The only way to update the current view state is to use the `ViewState`'s `observe(_:)` function.
218
212
219
213
When `observe(_:)` is called, it cancels any existing Combine publisher subscriptions or Swift Concurrency tasks and ignores view state updates from any previously called actions. This prevents future view state corruption from previous actions and frees up device resources.
220
214
@@ -228,7 +222,7 @@ This is a helpful reminder in case you forget to wrap an action call with `obser
228
222
229
223
### Loading View Actions
230
224
231
-
There are two actions that we want to configure in the `LoadUserProfileView`. The `load()` action in the `initialized` view state and the `retry()` action for the `loadingError` view state. We want `load()` to be called only once in the view's lifetime, so we'll attach it to the `onAppear` event handler on one of the subviews. The `retry()` action will be nestled in the view that uses the unwrapped `errorModel`.
225
+
There are two actions that we want to call in the `LoadUserProfileView`. The `load()` action in the `initialized` view state and the `retry()` action for the `loadingError` view state. We want `load()` to be called only once in the view's lifetime, so we'll attach it to the `onAppear` event handler on one of the subviews. The `retry()` action will be nestled in the view that uses the unwrapped `errorModel`.
We have to use the `ViewStateRendering`'s ``ViewStateRendering/container`` property because it gives us access to the underlying `StateContainer`'s observable `@Published```StateContainer/state`` property which can be observed by `onReceive`.
346
+
We use the `ViewState`'s projected value (`$`) because it gives us access to the state ``StatePublishing/publisher`` property which can be observed by `onReceive`.
353
347
354
348
#### Custom Two-Way Bindings
355
349
356
350
If we wanted to ditch the `Save` button in favor of having the view input call `save(username:)` as the user is typing, SwiftUI's `Binding<T>` type behaves much like a property on an object by providing a two-way getter and a setter for a wrapped value. We can utilize this to trick the `TextField` view into thinking it has read/write access to the view state's `username` property.
357
351
358
-
A custom `Binding<T>` can be created as a view state extension property, as a `@Binding` property on the view, or on the fly right within the view's code, like so:
352
+
A custom `Binding<T>` can be created as a view state extension property, as a `@Binding` property on the view, or right within the view's code, like so:
Notice how our call to ``ViewStateRendering/observe(_:debounced:file:line:)-7ihyy`` includes a `debounced`property. This allows us to prevent thrashing the `save(username:)`call if the user is typing quickly. It will only call the action a maximum of once per second (or whatever time delay is given).
370
+
Notice how our call to ``StateObserving/observe(_:debounced:file:line:)-8vbf2`` includes a `debounced`parameter. This prevents excessive calls to the `save(username:)`function if the user is typing quickly. It will only call the action a maximum of once per second (or whatever time delay is given).
377
371
378
372
## View Construction
379
373
@@ -384,42 +378,60 @@ A VSM view's initializer can take either of two approaches (or both, if desired)
384
378
- Dependent: The parent is responsible for passing in the view's initial view state (and its associated model)
385
379
- Encapsulated: The view encapsulates its view state kickoff point (and associated model), only requiring that the parent provide dependencies needed by the view or the models.
386
380
387
-
The dependent initializer has one upside and one downside when compared to the encapsulated approach. The upside is that the initializer is convenient for use in SwiftUI Previews and automated UI tests. The downside is that it requires any parent view to have some knowledge of the inner workings of the view in question.
381
+
The "Dependent" initializer has two upsides and one downside when compared to the encapsulated approach. The upsides are that Swift provides a default initializer automatically and the initializer is convenient for use in SwiftUI Previews and automated UI tests. The downside is that it requires parent views to have some knowledge of the inner workings of the view in question.
388
382
389
383
### Loading View Initializers
390
384
391
385
The initializers for the `LoadUserProfileView` are as follows:
392
386
387
+
"Dependent" Approach
388
+
393
389
```swift
394
-
// Dependent
395
-
init(state: LoadUserProfileViewState) {
396
-
_container = .init(state: state)
397
-
}
390
+
// Parent View Code
391
+
let loaderModel = LoadUserProfileViewState.LoaderModel(userId: userId)
392
+
let state = .initialized(loaderModel)
393
+
LoadUserProfileView(state: state)
394
+
```
395
+
396
+
"Encapsulated" Approach
398
397
399
-
// Encapsulated
398
+
```swift
399
+
// LoadUserProfileView Code
400
400
init(userId: Int) {
401
401
let loaderModel = LoadUserProfileViewState.LoaderModel(userId: userId)
402
402
let state = .initialized(loaderModel)
403
-
_container= .init(state: state)
403
+
_state= .init(wrappedValue: state)
404
404
}
405
+
406
+
// Parent View Code
407
+
LoadUserProfileView(userId: someUserId)
405
408
```
406
409
407
410
### Editing View Initializers
408
411
409
412
The initializers for the `EditUserProfileView` are as follows:
410
413
414
+
"Dependent" Approach
415
+
411
416
```swift
412
-
// Dependent
413
-
init(state: EditUserProfileViewState) {
414
-
_container = .init(state: state)
415
-
}
417
+
// Parent View Code
418
+
let editingModel = EditUserProfileViewState.EditingModel(userData: userData)
419
+
let state =EditUserProfileViewState(data: userData, editingState: .editing(editingModel))
420
+
EditUserProfileView(state: state)
421
+
```
422
+
423
+
"Encapsulated" Approach
416
424
417
-
// Encapsulated
425
+
```swift
426
+
// EditUserProfileView Code
418
427
init(userData: UserData) {
419
428
let editingModel = EditUserProfileViewState.EditingModel(userData: userData)
420
429
let state =EditUserProfileViewState(data: userData, editingState: .editing(editingModel))
0 commit comments