diff --git a/README.md b/README.md index 9c01939..c14c534 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,13 @@ The following are code excerpts of a feature that shows a blog entry from a data ### State Definition -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. +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. ```swift enum BlogEntryViewState { case initialized(loaderModel: LoaderModeling) case loading(errorModel: ErrorModeling?) - case loaded(blogModel: LoadedModeling) + case loaded(blogModel: BlogModeling) } protocol LoaderModeling { @@ -45,15 +45,16 @@ protocol ErrorModeling { func retry() -> AnyPublisher } -protocol LoadedModeling { +protocol BlogModeling { var title: String { get } var text: String { get } + func refresh() -> AnyPublisher } ``` ### Model Definition -The models provide the data for a given view state and implement the business logic. +The discrete models provide the data for a given view state and implement the business logic within the actions. ```swift struct LoaderModel: LoaderModeling { @@ -69,32 +70,41 @@ struct ErrorModel: ErrorModeling { } } -struct LoadedModel: LoadedModeling { +struct BlogModel: BlogModeling { var title: String var body: String + func refresh() -> AnyPublisher { + ... + } } ``` ### View Definition -The view observes and renders the state using the `StateContainer` type. State changes will automatically update the view. +The view observes and renders the state using the `ViewState` property wrapper. State changes will automatically update the view. ```swift -struct BlogEntryView: View, ViewStateRendering { - @StateObject var container: StateContainer +struct BlogEntryView: View { + @ViewState var state: BlogEntryViewState init() { - _container = .init(state: .initialized(LoaderModel())) + _state = .init(wrappedValue: .initialized(LoaderModel())) } var body: some View { switch state { case .initialized(loaderModel: let loaderModel): ... + .onAppear { + $state.observe(loaderModel.load()) + } case .loading(errorModel: let errorModel): ... - case .loaded(loadedModel: let loadedModel) + case .loaded(blogModel: let blogModel) ... + Button("Reload") { + $state.observe(blogModel.refresh()) + } } } } diff --git a/Sources/VSM/Documentation.docc/ComprehensiveGuide/DataDefinition.md b/Sources/VSM/Documentation.docc/ComprehensiveGuide/DataDefinition.md index 43427f0..74f93e3 100644 --- a/Sources/VSM/Documentation.docc/ComprehensiveGuide/DataDefinition.md +++ b/Sources/VSM/Documentation.docc/ComprehensiveGuide/DataDefinition.md @@ -193,13 +193,13 @@ typealias Dependencies = UserDataProvidingDependency The resulting initializer chain will end up looking something like this: ```swift -struct UserBioView: View, ViewStateRendering { +struct UserBioView: View { typealias Dependencies = UserBioViewState.LoaderModel.Dependencies & UserBioViewState.ErrorModel.Dependencies init(dependencies: Dependencies) { let loaderModel = UserBioViewState.LoaderModel(dependencies: Dependencies) let state = UserBioViewState.initialized(loaderModel) - _container = .init(state: state) + _state = .init(wrappedValue: state) } } diff --git a/Sources/VSM/Documentation.docc/ComprehensiveGuide/StateDefinition.md b/Sources/VSM/Documentation.docc/ComprehensiveGuide/StateDefinition.md index 9dda9a8..3c0d607 100644 --- a/Sources/VSM/Documentation.docc/ComprehensiveGuide/StateDefinition.md +++ b/Sources/VSM/Documentation.docc/ComprehensiveGuide/StateDefinition.md @@ -120,7 +120,7 @@ States usually match up 1:1 with variations in the view. So, we can safely assum > ```swift > someView.onAppear { > if case .initialized(let loadingModel) = state { -> observe(loadingModel.load()) +> $state.observe(loadingModel.load()) > } > } > ``` diff --git a/Sources/VSM/Documentation.docc/ComprehensiveGuide/VSMOverview.md b/Sources/VSM/Documentation.docc/ComprehensiveGuide/VSMOverview.md index 4debd0d..1a89f94 100644 --- a/Sources/VSM/Documentation.docc/ComprehensiveGuide/VSMOverview.md +++ b/Sources/VSM/Documentation.docc/ComprehensiveGuide/VSMOverview.md @@ -16,7 +16,9 @@ In VSM, the various responsibilities of a feature are divided into 3 concepts: 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. -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. +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. + +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. ![VSM Feature Structure Diagram](vsm-structure.jpg) diff --git a/Sources/VSM/Documentation.docc/ComprehensiveGuide/ViewDefinition-SwiftUI.md b/Sources/VSM/Documentation.docc/ComprehensiveGuide/ViewDefinition-SwiftUI.md index dcc54da..e670068 100644 --- a/Sources/VSM/Documentation.docc/ComprehensiveGuide/ViewDefinition-SwiftUI.md +++ b/Sources/VSM/Documentation.docc/ComprehensiveGuide/ViewDefinition-SwiftUI.md @@ -15,8 +15,8 @@ The basic structure of a SwiftUI VSM view is as follows: ```swift import VSM -struct LoadUserProfileView: View, ViewStateRendering { - @StateObject var container: StateContainer +struct LoadUserProfileView: View { + @ViewState var state: LoadUserProfileViewState var body: some View { // View definitions go here @@ -24,19 +24,15 @@ struct LoadUserProfileView: View, ViewStateRendering { } ``` -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 to build two related VSM views. +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. -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`. +**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`). -> Note: In SwiftUI, a view's initializer is called every time its parent view is updated and redrawn. -> -> 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. - -## Displaying the State +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. -The ``ViewStateRendering`` protocol provides a few properties and functions that help with displaying the current state, accessing the state data, and invoking actions. +In the following examples, we will use the `LoadUserProfileViewState` and `EditUserProfileViewState` types from to build two related VSM views. -The first of these members is the ``ViewStateRendering/state`` property, which is always set to the current state. +## Displaying the State As a refresher, the following flow chart expresses the requirements that we wish to draw in the view. @@ -64,11 +60,11 @@ protocol LoadingErrorModeling { } ``` -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. +In SwiftUI, we write a switch statement within the `body` property to evaluate the current state and draw the most appropriate content for it. 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. -The resulting `view` property implementation takes this shape: +The resulting `body` property implementation takes this shape: ```swift var body: some View { @@ -117,17 +113,17 @@ protocol SavingErrorModeling { } ``` -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. +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. ```swift -struct EditUserProfileView: View, ViewStateRendering { - @StateObject var container: StateContainer +struct EditUserProfileView: View { + @ViewState var state: EditUserProfileViewState @State var username: String = "" init(userData: UserData) { let editingModel = EditUserProfileViewState.EditingModel(userData: userData) let state = EditUserProfileViewState(data: userData, editingState: .editing(editingModel)) - _container = .init(state: state) + _state = .init(wrappedValue: state) } var body: some View { @@ -166,7 +162,7 @@ struct EditUserProfileView: View, ViewStateRendering { } ``` -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. +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. 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. @@ -204,17 +200,15 @@ extension EditUserProfileViewState { 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. -VSM's ``ViewStateRendering`` protocol provides 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. +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. It is called like so: ```swift -observe(someState.someAction()) -// or -observe(someState.someAction) +$state.observe(someState.someAction()) ``` -The only way to update the current view state is to use the `observe(_:)` function. +The only way to update the current view state is to use the `ViewState`'s `observe(_:)` function. 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. @@ -228,7 +222,7 @@ This is a helpful reminder in case you forget to wrap an action call with `obser ### Loading View Actions -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`. +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`. ```swift var body: some View { @@ -241,13 +235,13 @@ var body: some View { case .loadingError(let errorModel): Text(errorModel.message) Button("Retry") { - observe(errorModel.retry()) + $state.observe(errorModel.retry()) } } } .onAppear { if case .initialized(let loaderModel) = state { - observe(loaderModel.load()) + $state.observe(loaderModel.load()) } } } @@ -271,7 +265,7 @@ var body: some View { .textFieldStyle(.roundedBorder) Button("Save") { if case .editing(let editingModel) = state.editingState { - observe(editingModel.save(username: username)) + $state.observe(editingModel.save(username: username)) } } } @@ -285,10 +279,10 @@ var body: some View { Text(errorModel.message) HStack { Button("Retry") { - observe(errorModel.retry()) + $state.observe(errorModel.retry()) } Button("Cancel") { - observe(errorModel.cancel()) + $state.observe(errorModel.cancel()) } } } @@ -343,19 +337,19 @@ var body: some View { ZStack { ... } - .onReceive(container.$state) { newState in + .onReceive($state.publisher) { newState in username = newState.data.username } } ``` -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`. +We use the `ViewState`'s projected value (`$`) because it gives us access to the state ``StatePublishing/publisher`` property which can be observed by `onReceive`. #### Custom Two-Way Bindings 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` 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. -A custom `Binding` 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: +A custom `Binding` 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: ```swift var body: some View { @@ -363,7 +357,7 @@ var body: some View { get: { state.data.username }, set: { newValue in if case .editing(let editingModel) = state.editingState { - observe(editingModel.save(username: newValue), + $state.observe(editingModel.save(username: newValue), debounced: .seconds(1)) } } @@ -373,7 +367,7 @@ var body: some View { } ``` -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). +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). ## View Construction @@ -384,42 +378,60 @@ A VSM view's initializer can take either of two approaches (or both, if desired) - Dependent: The parent is responsible for passing in the view's initial view state (and its associated model) - 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. -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. +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. ### Loading View Initializers The initializers for the `LoadUserProfileView` are as follows: +"Dependent" Approach + ```swift -// Dependent -init(state: LoadUserProfileViewState) { - _container = .init(state: state) -} +// Parent View Code +let loaderModel = LoadUserProfileViewState.LoaderModel(userId: userId) +let state = .initialized(loaderModel) +LoadUserProfileView(state: state) +``` + +"Encapsulated" Approach -// Encapsulated +```swift +// LoadUserProfileView Code init(userId: Int) { let loaderModel = LoadUserProfileViewState.LoaderModel(userId: userId) let state = .initialized(loaderModel) - _container = .init(state: state) + _state = .init(wrappedValue: state) } + +// Parent View Code +LoadUserProfileView(userId: someUserId) ``` ### Editing View Initializers The initializers for the `EditUserProfileView` are as follows: +"Dependent" Approach + ```swift -// Dependent -init(state: EditUserProfileViewState) { - _container = .init(state: state) -} +// Parent View Code +let editingModel = EditUserProfileViewState.EditingModel(userData: userData) +let state = EditUserProfileViewState(data: userData, editingState: .editing(editingModel)) +EditUserProfileView(state: state) +``` + +"Encapsulated" Approach -// Encapsulated +```swift +// EditUserProfileView Code init(userData: UserData) { let editingModel = EditUserProfileViewState.EditingModel(userData: userData) let state = EditUserProfileViewState(data: userData, editingState: .editing(editingModel)) - _container = .init(state: state) + _state = .init(wrappedValue: state) } + +// Parent View Code +EditUserProfileView(userData: someUserData) ``` ## Iterative View Development diff --git a/Sources/VSM/Documentation.docc/ComprehensiveGuide/ViewDefinition-UIKit.md b/Sources/VSM/Documentation.docc/ComprehensiveGuide/ViewDefinition-UIKit.md index 7be57a8..532f55c 100644 --- a/Sources/VSM/Documentation.docc/ComprehensiveGuide/ViewDefinition-UIKit.md +++ b/Sources/VSM/Documentation.docc/ComprehensiveGuide/ViewDefinition-UIKit.md @@ -15,48 +15,30 @@ The basic structure of a UIKit VSM view is as follows: ```swift import VSM -class UserProfileViewController: UIViewController, ViewStateRendering { - var container: StateContainer - var stateSubscription: AnyCancellable? +class UserProfileViewController: UIViewController { + @RenderedViewState var state: LoadUserProfileViewState required init?(state: LoadUserProfileViewState, coder: NSCoder) { - container = .init(state: state) + _state = .init(wrappedValue: state, render: Self.render) super.init(coder: coder) } - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - stateSubscription = container.$state - .sink { [weak self] newState in - self?.render(newState) - } - super.viewDidLoad() - } + ... - func render(state: LoadUserProfileViewState) { + func render() { // View configuration goes here } } ``` -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 to build two related VSM views. +To turn any UIView or UIViewController into a "VSM View", define a property that holds our current state and decorate it with the `@RenderedViewState` property wrapper. -> Note: In the examples found in this article, we will be using Storyboards. As a result, you can see that we used a custom `NSCoder` initializer above. If you are using a code-first approach to UIKit, you can use whichever initialization mechanism is most appropriate. +**The UIKit-only `@RenderedViewState` property wrapper updates the view every time the state changes**. `@RenderedViewState` requires a `render` _function type_ parameter to call when the state changes. You must define this function in your UIView or UIViewController. -In UIKit, we have to manually `sink` the state changes to a `render(state:)` function. This render function will be called any time the state changes and can be used to create, destroy, or configure views or components within the view controller. Make sure your reference to `self` is weak. Make sure that you subscribe to the state publisher after the view has been created (in `viewDidLoad()` or later) because the render function will be fired immediately and will crash if the `UIViewController`'s `view` property is not yet initialized. +> Note: In the examples found in this article, we will be using Storyboards. As a result, we used a custom `NSCoder` initializer. If you are using a code-first approach to UIKit, you can use whichever initialization mechanism is most appropriate. ## Displaying the State -The ``ViewStateRendering`` protocol provides a few properties and functions that help with displaying the current state, accessing the state data, and invoking actions. - -The first of these members is the ``ViewStateRendering/state`` property, which reflects the current state of the view. - -> Important: In UIKit, the ``StateContainer/state`` publisher will call `render(state:)` on the state's `willChange` event. Therefore, any evaluations of `self.state` will give you the previous state's value. If you want the current state, use the state parameter that is passed into the render function. - As a refresher, the following flow chart expresses the requirements that we wish to draw in the view. ![VSM User Flow Diagram Example](vsm-user-flow-example.jpg) @@ -83,11 +65,11 @@ protocol LoadingErrorModeling { } ``` -In UIKit, we simply write a switch statement within the `render(state:)` function to evaluate the current state and configure the views for each state. +In UIKit, we write a switch statement within the `render()` function to evaluate the current state and configure the views for each state. 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. -The resulting `render(state:)` function implementation takes this shape: +The resulting `render()` function implementation takes this shape: ```swift @IBOutlet weak var loadingView: UIActivityIndicatorView! @@ -96,7 +78,7 @@ The resulting `render(state:)` function implementation takes this shape: @IBOutlet weak var errorLabel: UILabel! @IBOutlet weak var retryButton: UIButton! -func render(_ state: LoadUserProfileViewState) { +func render() { switch state { case .initialized, .loading: errorView.isHidden = true @@ -193,7 +175,7 @@ The following code renders each of the states for `EditUserProfileViewState` by @IBOutlet weak var retryButton: UIButton! @IBOutlet weak var cancelButton: UIButton! -func render(_ state: EditUserProfileViewState) { +func render() { switch state.editingState { case .editing: errorView.isHidden = true @@ -217,17 +199,15 @@ As in our first example, you can see that the various views are connected to the 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. -VSM's ``ViewStateRendering`` protocol provides 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. +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. It is called like so: ```swift -observe(someState.someAction()) -// or -observe(someState.someAction) +$state.observe(someState.someAction()) ``` -The only way to update the current view state is to use the `observe(_:)` function. +The only way to update the current view state is to use the `RenderedViewState`'s `observe(_:)` function. 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. @@ -241,7 +221,7 @@ This is a helpful reminder in case you forget to wrap an action call with `obser ### Loading View Actions -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 the `load()` call to only happen once for the view's lifetime, so we'll attach it to the `viewDidAppear` delegate method. Since the retry button is created by the Storyboard, the `retry()` action will be configured on the button in a special `setUpViews()` function. +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 the `load()` call to only happen once for the view's lifetime, so we'll attach it to the `viewDidAppear` delegate method. Since the retry button is created by the Storyboard, the `retry()` action will be configured on the button in a special `setUpViews()` function. ```swift override func viewDidLoad() { @@ -252,7 +232,7 @@ override func viewDidLoad() { override func viewDidAppear(_ animated: Bool) { if case .initialized(let loaderModel) = state { - observe(loaderModel.load()) + $state.observe(loaderModel.load()) } super.viewDidAppear(animated) } @@ -264,7 +244,7 @@ func setUpViews() { handler: { [weak self] action in guard let strongSelf = self else { return } if case .loadingError(let errorModel) = strongSelf.state { - strongSelf.observe(errorModel.retry()) + strongSelf.$state.observe(errorModel.retry()) } } ), @@ -285,7 +265,7 @@ func setUpViews() { handler: { [weak self] action in guard let strongSelf = self else { return } if case .editing(let editingModel) = strongSelf.state.editingState { - strongSelf.observe( + strongSelf.$state.observe( editingModel.save(username: strongSelf.usernameTextField.text ?? "") ) } @@ -300,7 +280,7 @@ func setUpViews() { handler: { [weak self] action in guard let strongSelf = self else { return } if case .savingError(let errorModel) = strongSelf.state.editingState { - strongSelf.observe(errorModel.retry()) + strongSelf.$state.observe(errorModel.retry()) } } ), @@ -313,7 +293,7 @@ func setUpViews() { handler: { [weak self] action in guard let strongSelf = self else { return } if case .savingError(let errorModel) = strongSelf.state.editingState { - strongSelf.observe(errorModel.cancel()) + strongSelf.$state.observe(errorModel.cancel()) } } ), @@ -324,7 +304,7 @@ func setUpViews() { You can see that based on the type-system constraints, _these actions can never be called from the wrong state_, and the feature code indicates this very clearly. -> Note: There is a special observe overload ``ViewStateRendering/observe(_:debounced:file:line:)-7ihyy`` which includes a `debounced` property. This allows us to avoid calling an action too many times when tied to user input that may be triggered rapidly, like typing in a text field. It will only call the action a maximum of once per second (or whatever time delay is given). +> Note: There is a special observe overload ``StateObserving/observe(_:debounced:file:line:)-8vbf2`` which includes a `debounced` property. This allows us to avoid calling an action too many times when tied to user input that may be triggered rapidly, like typing in a text field. It will only call the action a maximum of once per second (or whatever time delay is given). ## View Construction @@ -335,46 +315,74 @@ A VSM view's initializer can take either of two approaches (or both, if desired) - Dependent: The parent is responsible for passing in the view's initial view state (and its associated model) - 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. -The dependent initializer has one downside when compared to the encapsulated approach, in that it requires any parent view to have some knowledge of the inner workings of the view in question. +The "Dependent" initializer has one downside when compared to the encapsulated approach, in that it requires any parent view to have some knowledge of the inner workings of the view in question. ### Loading View Initializers The initializers for the `LoadUserProfileViewController` are as follows: +"Dependent" Approach + ```swift -// Dependent +// LoadUserProfileViewController Code required init?(state: LoadUserProfileViewState, coder: NSCoder) { - container = .init(state: state) + _state = .init(wrappedValue: state, render: Self.render) super.init(coder: coder) } -// Encapsulated +// Parent View Code +let loaderModel = LoadUserProfileViewState.LoaderModel(userId: someUserId) +let state = .initialized(loaderModel) +LoadUserProfileViewController(state: state, coder: coder) +``` + +"Encapsulated" Approach + +```swift +// LoadUserProfileViewController Code required init?(userId: Int, coder: NSCoder) { let loaderModel = LoadUserProfileViewState.LoaderModel(userId: userId) let state = .initialized(loaderModel) - container = .init(state: state) + _state = .init(wrappedValue: state, render: Self.render) super.init(coder: coder) } + +// Parent View Code +LoadUserProfileViewController(userId: someUserId, coder: coder) ``` ### Editing View Initializers The initializers for the `EditUserProfileViewController` are as follows: +"Dependent" Approach + ```swift -// Dependent +// EditUserProfileViewController Code init?(state: EditUserProfileViewState, coder: NSCoder) { - container = .init(state: state) + _state = .init(wrappedValue: state, render: Self.render) super.init(coder: coder) } -// Encapsulated +// Parent View Code +let savingModel = EditUserProfileViewState.EditingModel(userData: someUserData) +let state = EditUserProfileViewState(data: userData, editingState: .editing(savingModel)) +EditUserProfileViewController(state: state, code: coder) +``` + +"Encapsulated" Approach + +```swift +// EditUserProfileViewController Code init?(userData: UserData, coder: NSCoder) { let savingModel = EditUserProfileViewState.EditingModel(userData: userData) let state = EditUserProfileViewState(data: userData, editingState: .editing(savingModel)) - container = .init(state: state) + _state = .init(wrappedValue: state, render: Self.render) super.init(coder: coder) } + +// Parent View Code +EditUserProfileViewController(userData: someUserData, code: coder) ``` ## Synchronize View Logic diff --git a/Sources/VSM/Documentation.docc/Quickstart/QuickstartGuide.tutorial b/Sources/VSM/Documentation.docc/Quickstart/QuickstartGuide.tutorial index df40586..be71f38 100644 --- a/Sources/VSM/Documentation.docc/Quickstart/QuickstartGuide.tutorial +++ b/Sources/VSM/Documentation.docc/Quickstart/QuickstartGuide.tutorial @@ -86,7 +86,7 @@ @Section(title: "Building the View") { @ContentAndMedia { - Now that we've defined our Feature Shape, we can start building the view. We will utilize two VSM framework types to make it easier to follow the pattern: and . + Now that we've defined our Feature Shape, we can start building the view. We will utilize the property wrapper to make it easier to follow the pattern. > Note: This example covers only SwiftUI, but UIKit is fully supported by VSM. To learn more about building views in VSM, see or . @@ -101,9 +101,7 @@ } @Step { - Next, conform your view to the `ViewStateRendering` protocol. You are required to add a `StateContainer` property which manages the view’s state for you. - - Wrap that property with the `@StateObject` property wrapper so that the view state is persisted between view updates. + Next, add your view property and decorate it with `@ViewState`. The ``ViewState`` property wrapper will persist and manage the view's state for you. @Code(name: "BlogEntryView.swift", file: quickstart-view-conformance) } @@ -324,7 +322,7 @@ } @Step { - Finally, instantiate the `container` property with the state you just created. Your feature is now ready to use. + Finally, instantiate the `state` property with the state you just created. Your feature is now ready to use. > Note: To learn more about different ways for your feature to be initialized, see: or . diff --git a/Sources/VSM/Documentation.docc/Quickstart/quickstart-feature-init-final.swift b/Sources/VSM/Documentation.docc/Quickstart/quickstart-feature-init-final.swift index 0cb3962..2fd0861 100644 --- a/Sources/VSM/Documentation.docc/Quickstart/quickstart-feature-init-final.swift +++ b/Sources/VSM/Documentation.docc/Quickstart/quickstart-feature-init-final.swift @@ -1,10 +1,10 @@ -struct BlogEntryView: View, ViewStateRendering { - @StateObject var container: StateContainer +struct BlogEntryView: View { + @ViewState var state: BlogEntryViewState init(repository: BlogEntryProviding, entryId: Int) { let loaderModel = LoaderModel(repository: repository, entryId: entryId) let state = BlogEntryViewState.initialized(loaderModel: loaderModel) - _container = .init(state: state) + _state = .init(wrappedValue: state) } var body: some View { diff --git a/Sources/VSM/Documentation.docc/Quickstart/quickstart-feature-init-state.swift b/Sources/VSM/Documentation.docc/Quickstart/quickstart-feature-init-state.swift index 4acb41e..28c3fe7 100644 --- a/Sources/VSM/Documentation.docc/Quickstart/quickstart-feature-init-state.swift +++ b/Sources/VSM/Documentation.docc/Quickstart/quickstart-feature-init-state.swift @@ -1,5 +1,5 @@ -struct BlogEntryView: View, ViewStateRendering { - @StateObject var container: StateContainer +struct BlogEntryView: View { + @ViewState var state: BlogEntryViewState init(repository: BlogEntryProviding, entryId: Int) { let loaderModel = LoaderModel(repository: repository, entryId: entryId) diff --git a/Sources/VSM/Documentation.docc/Quickstart/quickstart-feature-init.swift b/Sources/VSM/Documentation.docc/Quickstart/quickstart-feature-init.swift index 2005522..965c090 100644 --- a/Sources/VSM/Documentation.docc/Quickstart/quickstart-feature-init.swift +++ b/Sources/VSM/Documentation.docc/Quickstart/quickstart-feature-init.swift @@ -1,5 +1,5 @@ -struct BlogEntryView: View, ViewStateRendering { - @StateObject var container: StateContainer +struct BlogEntryView: View { + @ViewState var state: BlogEntryViewState init(repository: BlogEntryProviding, entryId: Int) { diff --git a/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-conformance.swift b/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-conformance.swift index 2d081c5..f4d6e11 100644 --- a/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-conformance.swift +++ b/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-conformance.swift @@ -1,5 +1,5 @@ -struct BlogEntryView: View, ViewStateRendering { - @StateObject var container: StateContainer +struct BlogEntryView: View { + @ViewState var state: BlogEntryViewState var body: some View { diff --git a/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-error-model.swift b/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-error-model.swift index 0db62a2..a6b9dfd 100644 --- a/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-error-model.swift +++ b/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-error-model.swift @@ -1,12 +1,12 @@ -struct BlogEntryView: View, ViewStateRendering { - @StateObject var container: StateContainer +struct BlogEntryView: View { + @ViewState var state: BlogEntryViewState var body: some View { switch state { case .initialized(loaderModel: let loaderModel): ProgressView() .onAppear() { - observe(loaderModel.load()) + $state.observe(loaderModel.load()) } case .loading(errorModel: let errorModel): ZStack { @@ -15,7 +15,7 @@ struct BlogEntryView: View, ViewStateRendering { VStack { Text(errorModel.message) Button("Retry") { - observe(errorModel.retry()) + $state.observe(errorModel.retry()) } } .background(Color.white) diff --git a/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-error-view.swift b/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-error-view.swift index 0db62a2..a6b9dfd 100644 --- a/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-error-view.swift +++ b/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-error-view.swift @@ -1,12 +1,12 @@ -struct BlogEntryView: View, ViewStateRendering { - @StateObject var container: StateContainer +struct BlogEntryView: View { + @ViewState var state: BlogEntryViewState var body: some View { switch state { case .initialized(loaderModel: let loaderModel): ProgressView() .onAppear() { - observe(loaderModel.load()) + $state.observe(loaderModel.load()) } case .loading(errorModel: let errorModel): ZStack { @@ -15,7 +15,7 @@ struct BlogEntryView: View, ViewStateRendering { VStack { Text(errorModel.message) Button("Retry") { - observe(errorModel.retry()) + $state.observe(errorModel.retry()) } } .background(Color.white) diff --git a/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-final.swift b/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-final.swift index fbabef9..067ad91 100644 --- a/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-final.swift +++ b/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-final.swift @@ -1,12 +1,12 @@ -struct BlogEntryView: View, ViewStateRendering { - @StateObject var container: StateContainer +struct BlogEntryView: View { + @ViewState var state: BlogEntryViewState var body: some View { switch state { case .initialized(loaderModel: let loaderModel): ProgressView() .onAppear() { - observe(loaderModel.load()) + $state.observe(loaderModel.load()) } case .loading(errorModel: let errorModel): ZStack { @@ -15,7 +15,7 @@ struct BlogEntryView: View, ViewStateRendering { VStack { Text(errorModel.message) Button("Retry") { - observe(errorModel.retry()) + $state.observe(errorModel.retry()) } } .background(Color.white) diff --git a/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-initialized-state.swift b/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-initialized-state.swift index e31be1b..9b321d9 100644 --- a/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-initialized-state.swift +++ b/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-initialized-state.swift @@ -1,5 +1,5 @@ -struct BlogEntryView: View, ViewStateRendering { - @StateObject var container: StateContainer +struct BlogEntryView: View { + @ViewState var state: BlogEntryViewState var body: some View { switch state { diff --git a/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-loader-action.swift b/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-loader-action.swift index 788b58b..a2f0066 100644 --- a/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-loader-action.swift +++ b/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-loader-action.swift @@ -1,12 +1,12 @@ -struct BlogEntryView: View, ViewStateRendering { - @StateObject var container: StateContainer +struct BlogEntryView: View { + @ViewState var state: BlogEntryViewState var body: some View { switch state { case .initialized(loaderModel: let loaderModel): ProgressView() .onAppear() { - observe(loaderModel.load()) + $state.observe(loaderModel.load()) } } } diff --git a/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-loading-state.swift b/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-loading-state.swift index 6f7c39b..0392870 100644 --- a/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-loading-state.swift +++ b/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-loading-state.swift @@ -1,12 +1,12 @@ -struct BlogEntryView: View, ViewStateRendering { - @StateObject var container: StateContainer +struct BlogEntryView: View { + @ViewState var state: BlogEntryViewState var body: some View { switch state { case .initialized(loaderModel: let loaderModel): ProgressView() .onAppear() { - observe(loaderModel.load()) + $state.observe(loaderModel.load()) } case .loading(errorModel: let errorModel): ZStack { diff --git a/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-switch.swift b/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-switch.swift index 34b45fb..1b49b14 100644 --- a/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-switch.swift +++ b/Sources/VSM/Documentation.docc/Quickstart/quickstart-view-switch.swift @@ -1,5 +1,5 @@ -struct BlogEntryView: View, ViewStateRendering { - @StateObject var container: StateContainer +struct BlogEntryView: View { + @ViewState var state: BlogEntryViewState var body: some View { switch state { diff --git a/Sources/VSM/Documentation.docc/Reference/ModelActions.md b/Sources/VSM/Documentation.docc/Reference/ModelActions.md index 87c0ade..a7d323d 100644 --- a/Sources/VSM/Documentation.docc/Reference/ModelActions.md +++ b/Sources/VSM/Documentation.docc/Reference/ModelActions.md @@ -4,7 +4,7 @@ A reference for choosing the best Action types for your VSM Models ## Overview -Actions are responsible for progressing the "State Journey" of the feature and need to be flexible to account for any functionality. The VSM framework supports several Action types within the `StateContainer`'s `observe()` overloads. This article covers each and gives examples of how to use them appropriately. +Actions are responsible for progressing the "State Journey" of the feature and need to be flexible to account for any functionality. The VSM framework supports several Action types within the `StateObserving`'s `observe()` overloads. This article covers each and gives examples of how to use them appropriately. ## State Publisher @@ -12,7 +12,7 @@ Actions are responsible for progressing the "State Journey" of the feature and n func loadUser() -> AnyPublisher ``` -This is the most common action shape used in VSM because it allows the action to return any number of states to the view until another action is called. The ``StateContainer`` accomplishes this through the ``StateContainer/observe(_:)-1uta3`` function by subscribing to the publisher, which is returned from your action. It will only observe (subscribe to) one action at a time. It does this to prevent view state corruption by previously called actions. +This is the most common action shape used in VSM because it allows the action to return any number of states to the view until another action is called. The ``ViewState`` accomplishes this through the ``StateObserving/observe(_:)-31ocs`` function by subscribing to the publisher, which is returned from your action. It will only observe (subscribe to) one action at a time. It does this to prevent view state corruption by previously called actions. The best way to implement this function is to return the Combine publisher result from your data repository. To do this, you will have to convert the data/error output of the repository to the state/never output of the action. This can be done like so: @@ -151,18 +151,18 @@ func toggleFavorite() async -> ProductViewState ## No State ```swift -func toggleFavorite() -> Void +func toggleFavorite() ``` Sometimes you need to kick off a process or call a function on a repository without needing a direct view state result. This action type is usable in a VSM feature in situations where a direct view state change is _not_ required. You can't use the `observe()` function to call this function from the view because the `observe()` function's only purpose is to track view state changes from action invocations. ```swift -func toggleFavorite() -> Void +func toggleFavorite() sharedFavoritesRepository.toggleFavorite(for: productId) } ``` -Usually, this action type is used in conjunction with an observable repository. A `load()` action would map the data from the repository's data publisher into a view state for the view to render. The ``StateContainer`` will hold on to that view state subscription (and, consequently, the repository data subscription) indefinitely unless another action is observed. Any future changes to the data will translate instantly into a change in the view. +Usually, this action type is used in conjunction with an observable repository. A `load()` action would map the data from the repository's data publisher into a view state for the view to render. The ``ViewState`` will hold on to that view state subscription (and, consequently, the repository data subscription) indefinitely unless another action is observed. Any future changes to the data will translate instantly into a change in the view. For example, if you used the `load` action below, the view would always be updated when the data changes, even though the `toggleFavorite` action doesn't return any new view states. @@ -177,7 +177,7 @@ func load() -> AnyPublisher } // In LoadedModel model: -func toggleFavorite() -> Void +func toggleFavorite() sharedFavoritesRepository.toggleFavorite(for: productId) } ``` diff --git a/Sources/VSM/Documentation.docc/Reference/ViewCommunication.md b/Sources/VSM/Documentation.docc/Reference/ViewCommunication.md index eb216c3..05db415 100644 --- a/Sources/VSM/Documentation.docc/Reference/ViewCommunication.md +++ b/Sources/VSM/Documentation.docc/Reference/ViewCommunication.md @@ -22,7 +22,7 @@ In the above situations, SwiftUI views should communicate with each other using For example, if you have a presented modal that needs to be able to close itself, you would write: ```swift -struct ProfileView: View, ViewStateRendering { +struct ProfileView: View { ... @State var changePasswordIsPresented = false @@ -36,7 +36,7 @@ struct ProfileView: View, ViewStateRendering { } } -struct ChangePasswordView: View, ViewStateRendering { +struct ChangePasswordView: View { ... @Binding var isPresented: Bool @@ -58,7 +58,7 @@ However, there may be instances where `UIViews` and `UIViewControllers` should b One example of this would be if a `UITabBarController` needed to be notified when the first of its children appeared so that it could then trigger a modal to display. ```swift -final class TabBarController: UITabBarController, ViewStateRendering { +final class TabBarController: UITabBarController { ... var subscriptions: Set = [] @@ -80,7 +80,7 @@ final class TabBarController: UITabBarController, ViewStateRendering { } } -final class TabViewController: UIViewController, ViewStateRendering { +final class TabViewController: UIViewController { ... private var eventSubject = PassthroughSubject() var eventPublisher: AnyPublisher { eventSubject.eraseToAnyPublisher() } @@ -103,10 +103,10 @@ For example, if we have a feature that loads the user info that is required by t ### SwiftUI ```swift -struct LoadUserView: View, ViewStateRendering { - ... +struct LoadUserView: View { + @ViewState var state: LoadUserViewState let dependencies: Dependencies - + ... var body: some View { HStack { switch state { @@ -125,11 +125,11 @@ struct LoadUserView: View, ViewStateRendering { ### UIKit ```swift -final class LoadUserViewController: UIViewController, ViewStateRendering { - ... +final class LoadUserViewController: UIViewController { + @RenderedViewState var state: LoadUserViewState let dependencies: Dependencies - - func render(_ state: LoadUserProfileViewState) { + ... + func render() { switch state { case .initialized, .loading: ... diff --git a/Sources/VSM/Documentation.docc/Reference/ViewStateExtensions.md b/Sources/VSM/Documentation.docc/Reference/ViewStateExtensions.md index 0b938c5..5f2c257 100644 --- a/Sources/VSM/Documentation.docc/Reference/ViewStateExtensions.md +++ b/Sources/VSM/Documentation.docc/Reference/ViewStateExtensions.md @@ -32,7 +32,7 @@ A view's location is its identity. Since SwiftUI flow control statements are tra The following simplified example highlights one of these situations: ```swift -struct EditUserProfileView: View, ViewStateRendering { +struct EditUserProfileView: View { ... var body: some View { switch state.editingState { @@ -52,7 +52,7 @@ While you may think these two view's identities are the same, they are in fact n The problem we have here is that our view state is an enum and our view code will become much more complicated and verbose if we try to convert the enum to a boolean like so: ```swift -struct EditUserProfileView: View, ViewStateRendering { +struct EditUserProfileView: View { ... var body: some View { let isSaving: Bool @@ -87,7 +87,7 @@ extension EditUserProfileViewState { Now that we have this extension, our view code becomes much cleaner. ```swift -struct EditUserProfileView: View, ViewStateRendering { +struct EditUserProfileView: View { ... var body: some View { TextField("Username", $username) @@ -104,7 +104,7 @@ SwiftUI now sees this TextField as a single view with a disabled modifier. Any t When working with the SwiftUI framework, you will often come across views or view modifiers that require a `Binding` of some kind. For example, you may have an error view that you want to display if the user gets into an error state. Without view state extensions, your view will be cluttered with code that translates the view state enum into a `Binding` and a confusing `.sheet()` view modifier implementation. ```swift -struct EditUserProfileView: View, ViewStateRendering { +struct EditUserProfileView: View { ... var hasError: Binding @@ -157,7 +157,7 @@ extension ErrorModel: Identifiable { This allows us to build the following view code, which is much more clean and concise. ```swift -struct EditUserProfileView: View, ViewStateRendering { +struct EditUserProfileView: View { ... var body: some View { VStack { @@ -198,13 +198,13 @@ extension EditUserProfileViewState { In order to get view code that looks like this: ```swift -struct EditUserProfileView: View, ViewStateRendering { +struct EditUserProfileView: View { ... var body: some View { TextField("Username", $username) .disabled(state.isSaving) Button("Save") { - observe(state.saveUsername(username)) + $state.observe(state.saveUsername(username)) } } } @@ -217,14 +217,14 @@ Obfuscating the action's behavior in a view state extension will confuse future The following approach is a best practice. It is worth noting that there is no `observe` function overload for an optional action result, and this is by design for the reasons stated above. ```swift -struct EditUserProfileView: View, ViewStateRendering { +struct EditUserProfileView: View { ... var body: some View { TextField("Username", $username) .disabled(state.isSaving) Button("Save") { if case .editing(let editingModel) = editingState { - observe(editingModel.save(username: username)) + $state.observe(editingModel.save(username: username)) } } } diff --git a/Sources/VSM/Documentation.docc/VSM.md b/Sources/VSM/Documentation.docc/VSM.md index b3b6dfd..ea1969d 100644 --- a/Sources/VSM/Documentation.docc/VSM.md +++ b/Sources/VSM/Documentation.docc/VSM.md @@ -8,7 +8,7 @@ VSM is a reactive, unidirectional, type-safe, behavior-driven, clean architectur ![VSM Overview Diagram](vsm-diagram.png) -This package provides helpful types for implementing VSM, such as the ``StateContainer`` type which manages the current `State`. `UIViews`, `UIViewControllers`, or SwiftUI `Views` that conform to ``ViewStateRendering`` can easily react to changes in `State` by rendering the current `State`. +This package provides helpful types for implementing VSM, such as the ``ViewState`` property wrapper (or the ``RenderedViewState`` property wrapper for UIKit views) which manages and renders the current `State`. ## Topics @@ -37,10 +37,15 @@ This package provides helpful types for implementing VSM, such as the ``StateCon ### Primary Types -- ``ViewStateRendering`` -- ``StateContainer`` +- ``ViewState`` +- ``RenderedViewState`` ### Supporting Types +- ``StateContainer`` - ``StateSequence`` - ``MutatingCopyable`` + +### Deprecated Types + +- ``ViewStateRendering`` diff --git a/Sources/VSM/StateContainer/StateBinding.swift b/Sources/VSM/StateContainer/StateBinding.swift index a3a8280..bee4ceb 100644 --- a/Sources/VSM/StateContainer/StateBinding.swift +++ b/Sources/VSM/StateContainer/StateBinding.swift @@ -10,52 +10,86 @@ import Combine import Foundation import SwiftUI +/// Provides functions for converting a view state into a SwiftUI two-way `Binding` public protocol StateBinding { associatedtype State - /// Creates a unidirectional, auto-observing `Binding` for the `ViewState` using a `KeyPath` and a basic closure. - /// **Not intended for use when `ViewState` is an enum.** + /// Creates a two-way SwiftUI binding using a `KeyPath` and a _closure_ for simple (non-enum) view states. + /// + /// Example Usage + /// ```swift + /// TextField("Username", text: $state.bind(\.username, to: { $0.update(username: $1) })) + /// ``` + /// /// - Parameters: - /// - stateKeyPath: `KeyPath` for a `Value` of the `ViewState` - /// - observedSetter: Converts the new `Value` to a new `ViewState`, which is automatically observed + /// - stateKeyPath: `KeyPath` for a `Value` of the `State` + /// - observedSetter: Converts the new `Value` to a new `State`, which is automatically rendered by the view /// - Returns: A `Binding` for use in SwiftUI controls - func bind(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State, Value) -> State) -> Binding + func bind(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State, Value) -> AnyPublisher) -> Binding - /// Creates a unidirectional, auto-observing `Binding` for the `ViewState` using a `KeyPath` and a *method signature* - /// **This doesn't work when `ViewState` is an enum** - /// Example usage: `bind(\.someModelProperty, to: ViewState.someModelMethod)` + /// Creates a two-way SwiftUI binding using a `KeyPath` and a _function type_ for simple (non-enum) view states. + /// + /// Example Usage + /// ```swift + /// TextField("Username", text: $state.bind((\.username), to: ProfileState.update)) + /// ``` + /// /// - Parameters: - /// - stateKeyPath: `KeyPath` for a `Value` of the `ViewState` - /// - observedSetter: A **method signature** which converts the new `Value` to a new `ViewState` and is automatically observed + /// - stateKeyPath: `KeyPath` for a `Value` of the `State` + /// - observedSetter: A _function type_ which converts the new `Value` to a new `State` and is automatically rendered by the view /// - Returns: A `Binding` for use in SwiftUI controls - func bind(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State) -> (Value) -> State) -> Binding + func bind(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State) -> (Value) -> AnyPublisher) -> Binding - /// Creates a unidirectional, auto-observing `Binding` for the `ViewState` using a `KeyPath` and a basic closure. - /// **Not intended for use when `ViewState` is an enum.** + /// Creates a two-way SwiftUI binding using a `KeyPath` and a _closure_ for simple (non-enum) view states. + /// + /// Example Usage + /// ```swift + /// TextField("Username", text: $state.bind(\.username, to: { $0.update(username: $1) })) + /// ``` + /// /// - Parameters: - /// - stateKeyPath: `KeyPath` for a `Value` of the `ViewState` - /// - observedSetter: Converts the new `Value` to a new `ViewState`, which is automatically observed + /// - stateKeyPath: `KeyPath` for a `Value` of the `State` + /// - observedSetter: Converts the new `Value` to a new `State`, which is automatically rendered by the view /// - Returns: A `Binding` for use in SwiftUI controls - func bind(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State, Value) -> AnyPublisher) -> Binding - - /// Creates a unidirectional, auto-observing `Binding` for the `ViewState` using a `KeyPath` and a *method signature* - /// **Not intended for use when `ViewState` is an enum.** - /// Example usage: `bind(\.someModelProperty, to: ViewState.someModelMethod)` + func bind(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State, Value) -> State) -> Binding + + /// Creates a two-way SwiftUI binding using a `KeyPath` and a _function type_ for simple (non-enum) view states. + /// + /// Example Usage + /// ```swift + /// TextField("Username", text: $state.bind((\.username), to: ProfileState.update)) + /// ``` + /// /// - Parameters: - /// - stateKeyPath: `KeyPath` for a `Value` of the `ViewState` - /// - observedSetter: A **method signature** which converts the new `Value` to a new `ViewState` and is automatically observed + /// - stateKeyPath: `KeyPath` for a `Value` of the `State` + /// - observedSetter: A _function type_ which converts the new `Value` to a new `State` and is automatically rendered by the view /// - Returns: A `Binding` for use in SwiftUI controls - func bind(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State) -> (Value) -> AnyPublisher) -> Binding -} - -struct HashedIdentifier: Hashable { - let uniqueValues: [AnyHashable] - - /// Prevents accidental key collisions between auto-generated identifiers and manually generated identifiers - private static let uniqueKey: AnyHashable = UUID() + func bind(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State) -> (Value) -> State) -> Binding - init(_ values: AnyHashable ...) { - uniqueValues = [Self.uniqueKey] + values - } + /// Creates an asynchronous two-way SwiftUI binding using a `KeyPath` and a _closure_ for simple (non-enum) view states. + /// + /// Example Usage + /// ```swift + /// TextField("Username", text: $state.bind(\.username, to: { $0.update(username: $1) })) + /// ``` + /// + /// - Parameters: + /// - stateKeyPath: `KeyPath` for a `Value` of the `State` + /// - observedSetter: Converts the new `Value` to a new `State`, which is automatically rendered by the view + /// - Returns: A `Binding` for use in SwiftUI controls + func bindAsync(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State, Value) async -> State) -> Binding + + /// Creates an asynchronous two-way SwiftUI binding using a `KeyPath` and a _function type_ for simple (non-enum) view states. + /// + /// Example Usage + /// ```swift + /// TextField("Username", text: $state.bind((\.username), to: ProfileState.update)) + /// ``` + /// + /// - Parameters: + /// - stateKeyPath: `KeyPath` for a `Value` of the `State` + /// - observedSetter: A _function type_ which converts the new `Value` to a new `State` and is automatically rendered by the view + /// - Returns: A `Binding` for use in SwiftUI controls + func bindAsync(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State) -> (Value) async -> State) -> Binding } #endif diff --git a/Sources/VSM/StateContainer/StateContainer+Binding.swift b/Sources/VSM/StateContainer/StateContainer+Binding.swift index f480bc2..9363e8e 100644 --- a/Sources/VSM/StateContainer/StateContainer+Binding.swift +++ b/Sources/VSM/StateContainer/StateContainer+Binding.swift @@ -5,7 +5,7 @@ // Created by Albert Bori on 5/10/22. // -#if canImport(SwiftUI) && canImport(Combine) +#if canImport(SwiftUI) import Combine import SwiftUI @@ -14,12 +14,7 @@ import SwiftUI public extension StateContainer { - /// Creates a unidirectional, auto-observing `Binding` for the `State` using a `KeyPath` and a basic closure. - /// **Not intended for use when`State` is an enum.** - /// - Parameters: - /// - stateKeyPath: `KeyPath` for a `Value` of the `State` - /// - observedSetter: Converts the new `Value` to a new `State`, which is automatically observed - /// - Returns: A `Binding` for use in SwiftUI controls + // See StateBinding for details func bind(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State, Value) -> State) -> Binding { return Binding( get: { @@ -30,20 +25,7 @@ public extension StateContainer { }) } - /// Creates a `Binding` for SwiftUI views by binding a `State`'s Value to an `Action` - /// - Parameters: - /// - stateKeyPath: `KeyPath` for a `Value` of the `State` - /// - observedSetter: A **method signature** which converts the new `Value` to a new `State` and is automatically observed - /// - Returns: A `Binding` for use in SwiftUI controls - /// - /// Creates a unidirectional, auto-observing `Binding` for the `State` using a `KeyPath` and a *method signature*. - /// This function is best suited for a `State`s that is a `struct`. - /// - /// Example Usage - /// ```swift - /// TextField("Username", bind(\.username, to: ViewState.changeUsername)) - /// ``` - /// + // See StateBinding for details func bind(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State) -> (Value) -> State) -> Binding { return Binding( get: { @@ -59,13 +41,8 @@ public extension StateContainer { public extension StateContainer { - /// Creates a unidirectional, auto-observing `Binding` for the `State` using a `KeyPath` and a basic closure. - /// **Not intended for use when`State` is an enum.** - /// - Parameters: - /// - stateKeyPath: `KeyPath` for a `Value` of the `State` - /// - observedSetter: Converts the new `Value` to a new `State`, which is automatically observed - /// - Returns: A `Binding` for use in SwiftUI controls - func bind(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State, Value) async -> State) -> Binding { + // See StateBinding for details + func bindAsync(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State, Value) async -> State) -> Binding { return Binding( get: { return self.state[keyPath: stateKeyPath] @@ -75,14 +52,8 @@ public extension StateContainer { }) } - /// Creates a unidirectional, auto-observing `Binding` for the `State` using a `KeyPath` and a *method signature* - /// **Not intended for use when`State` is an enum.** - /// Example usage: `bind(\.someModelProperty, to: ViewState.someModelMethod)` - /// - Parameters: - /// - stateKeyPath: `KeyPath` for a `Value` of the `State` - /// - observedSetter: A **method signature** which converts the new `Value` to a new `State` and is automatically observed - /// - Returns: A `Binding` for use in SwiftUI controls - func bind(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State) -> (Value) async -> State) -> Binding { + // See StateBinding for details + func bindAsync(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State) -> (Value) async -> State) -> Binding { return Binding( get: { return self.state[keyPath: stateKeyPath] @@ -97,12 +68,7 @@ public extension StateContainer { public extension StateContainer { - /// Creates a unidirectional, auto-observing `Binding` for the `State` using a `KeyPath` and a basic closure. - /// **Not intended for use when`State` is an enum.** - /// - Parameters: - /// - stateKeyPath: `KeyPath` for a `Value` of the `State` - /// - observedSetter: Converts the new `Value` to a new `State`, which is automatically observed - /// - Returns: A `Binding` for use in SwiftUI controls + // See StateBinding for details func bind(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State, Value) -> AnyPublisher) -> Binding { return Binding( get: { @@ -113,13 +79,7 @@ public extension StateContainer { }) } - /// Creates a unidirectional, auto-observing `Binding` for the `State` using a `KeyPath` and a *method signature* - /// **Not intended for use when`State` is an enum.** - /// Example usage: `bind(\.someModelProperty, to: ViewState.someModelMethod)` - /// - Parameters: - /// - stateKeyPath: `KeyPath` for a `Value` of the `State` - /// - observedSetter: A **method signature** which converts the new `Value` to a new `State` and is automatically observed - /// - Returns: A `Binding` for use in SwiftUI controls + // See StateBinding for details func bind(_ stateKeyPath: KeyPath, to observedSetter: @escaping (State) -> (Value) -> AnyPublisher) -> Binding { return Binding( get: { diff --git a/Sources/VSM/StateContainer/StateContainer.swift b/Sources/VSM/StateContainer/StateContainer.swift index 37397d0..5633502 100644 --- a/Sources/VSM/StateContainer/StateContainer.swift +++ b/Sources/VSM/StateContainer/StateContainer.swift @@ -68,7 +68,8 @@ final public class StateContainer: ObservableObject, StateContaining { // MARK: - Observe Function Overloads public extension StateContainer { - /// Observes the state publisher emitted as a result of invoking some action + + // See StateObserving for details func observe(_ statePublisher: AnyPublisher) { cancelRunningObservations() stateSubscription = statePublisher @@ -77,7 +78,7 @@ public extension StateContainer { } } - /// Observes the state emitted as a result of invoking some asynchronous action + // See StateObserving for details func observeAsync(_ nextState: @escaping () async -> State) { cancelRunningObservations() // A weak-self declaration is required on the `Task` closure to break an unexpected strong self retention, despite not directly invoking self ¯\_(ツ)_/¯ @@ -90,7 +91,7 @@ public extension StateContainer { } } - /// Observes the states emitted as a result of invoking some asynchronous action that returns an asynchronous sequence + // See StateObserving for details func observeAsync(_ stateSequence: @escaping () async -> SomeAsyncSequence) where SomeAsyncSequence.Element == State { cancelRunningObservations() // A weak-self declaration is required on the `Task` closure to break an unexpected strong self retention, despite not directly invoking self ¯\_(ツ)_/¯ @@ -104,7 +105,7 @@ public extension StateContainer { } } - /// Observes the state emitted as a result of invoking some synchronous action + // See StateObserving for details func observe(_ nextState: State) { cancelRunningObservations() setStateOnMainThread(to: nextState) @@ -115,12 +116,14 @@ public extension StateContainer { public extension StateContainer { + /// A type-erased, unique action debouncer private struct DebounceableAction { var identifier: AnyHashable var dueTime: DispatchQueue.SchedulerTimeType.Stride var action: () -> Void } + /// Debounces the type-erased, unique action private func debounce(action: DebounceableAction) { debounceSubscriptionQueue.sync { if debounceSubscriptions[action.identifier] == nil { @@ -135,13 +138,7 @@ public extension StateContainer { debouncePublisher.send(action) } - /// Debounces the action calls by `dueTime`, then observes the `State` publisher emitted as a result of invoking the action. - /// Prevents actions from being excessively called when bound to noisy UI events. - /// Action calls are grouped by the provided `identifier`. - /// - Parameters: - /// - statePublisher: The action to be debounced before invoking - /// - dueTime: The amount of time required to pass before invoking the most recent action - /// - identifier: The identifier for grouping actions for debouncing + // See StateObserving for details func observe( _ statePublisher: @escaping @autoclosure () -> AnyPublisher, debounced dueTime: DispatchQueue.SchedulerTimeType.Stride, @@ -153,13 +150,7 @@ public extension StateContainer { debounce(action: debounceableAction) } - /// Debounces the action calls by `dueTime`, then asynchronously observes the `State` returned as a result of invoking the action. - /// Prevents actions from being excessively called when bound to noisy UI events. - /// Action calls are grouped by the provided `identifier`. - /// - Parameters: - /// - nextState: The action to be debounced before invoking - /// - dueTime: The amount of time required to pass before invoking the most recent action - /// - identifier: The identifier for grouping actions for debouncing + // See StateObserving for details func observeAsync( _ nextState: @escaping () async -> State, debounced dueTime: DispatchQueue.SchedulerTimeType.Stride, @@ -171,13 +162,7 @@ public extension StateContainer { debounce(action: debounceableAction) } - /// Debounces the action calls by `dueTime`, then observes the async sequence of `State`s returned as a result of invoking the action. - /// Prevents actions from being excessively called when bound to noisy UI events. - /// Action calls are grouped by the provided `identifier`. - /// - Parameters: - /// - stateSequence: The action to be debounced before invoking - /// - dueTime: The amount of time required to pass before invoking the most recent action - /// - identifier: The identifier for grouping actions for debouncing + // See StateObserving for details func observeAsync( _ stateSequence: @escaping () async -> SomeAsyncSequence, debounced dueTime: DispatchQueue.SchedulerTimeType.Stride, @@ -189,13 +174,7 @@ public extension StateContainer { debounce(action: debounceableAction) } - /// Debounces the action calls by `dueTime`, then observes the `State` returned as a result of invoking the action. - /// Prevents actions from being excessively called when bound to noisy UI events. - /// Action calls are grouped by the provided `identifier`. - /// - Parameters: - /// - nextState: The action to be debounced before invoking - /// - dueTime: The amount of time required to pass before invoking the most recent action - /// - identifier: The identifier for grouping actions for debouncing + // See StateObserving for details func observe( _ nextState: @escaping @autoclosure () -> State, debounced dueTime: DispatchQueue.SchedulerTimeType.Stride, diff --git a/Sources/VSM/StateContainer/StateObserving.swift b/Sources/VSM/StateContainer/StateObserving.swift index 121c4e6..65b646d 100644 --- a/Sources/VSM/StateContainer/StateObserving.swift +++ b/Sources/VSM/StateContainer/StateObserving.swift @@ -8,70 +8,70 @@ import Combine import Foundation -/// Provides functions for observing an action for potentially new states +/// Provides functions for observing VSM actions to render new states on the view. public protocol StateObserving { associatedtype State - /// Observes the state publisher emitted as a result of invoking some action + /// Renders the states emitted by the publisher on the view. + /// - Parameter statePublisher: The view state publisher to be observed for rendering the current view state func observe(_ statePublisher: AnyPublisher) - - /// Observes the state emitted as a result of invoking some synchronous action + + /// Renders the next state on the view. + /// - Parameter nextState: The next view state to render func observe(_ nextState: State) - /// Observes the state emitted as a result of invoking some asynchronous action + /// Asynchronously renders the next state on the view. + /// - Parameter nextState: The next view state to render func observeAsync(_ nextState: @escaping () async -> State) - /// Observes the states emitted as a result of invoking some asynchronous action that returns an asynchronous sequence + /// Asynchronously renders the sequence of states on the view. + /// - Parameter stateSequence: The sequence of states to render func observeAsync(_ stateSequence: @escaping () async -> StateSequence) where StateSequence.Element == State // MARK: - Debounce - /// Debounces the action calls by `dueTime`, then observes the `State` publisher emitted as a result of invoking the action. - /// Prevents actions from being excessively called when bound to noisy UI events. - /// Action calls are grouped by the provided `identifier`. + /// Renders the states emitted by the publisher on the view. + /// Calls to this function are debounced to prevent excessive execution from noisy events. /// - Parameters: /// - statePublisher: The action to be debounced before invoking /// - dueTime: The amount of time required to pass before invoking the most recent action - /// - identifier: The identifier for grouping actions for debouncing + /// - identifier: (optional) The identifier for grouping actions for debouncing func observe( _ statePublisher: @escaping @autoclosure () -> AnyPublisher, debounced dueTime: DispatchQueue.SchedulerTimeType.Stride, identifier: AnyHashable ) - /// Debounces the action calls by `dueTime`, then observes the `State` returned as a result of invoking the action. - /// Prevents actions from being excessively called when bound to noisy UI events. - /// Action calls are grouped by the provided `identifier`. + /// Renders the next state on the view. + /// Calls to this function are debounced to prevent excessive execution from noisy events. /// - Parameters: /// - nextState: The action to be debounced before invoking /// - dueTime: The amount of time required to pass before invoking the most recent action - /// - identifier: The identifier for grouping actions for debouncing + /// - identifier: (optional) The identifier for grouping actions for debouncing func observe( _ nextState: @escaping @autoclosure () -> State, debounced dueTime: DispatchQueue.SchedulerTimeType.Stride, identifier: AnyHashable ) - /// Debounces the action calls by `dueTime`, then asynchronously observes the `State` returned as a result of invoking the action. - /// Prevents actions from being excessively called when bound to noisy UI events. - /// Action calls are grouped by the provided `identifier`. + /// Asynchronously renders the next state on the view. + /// Calls to this function are debounced to prevent excessive execution from noisy events. /// - Parameters: /// - nextState: The action to be debounced before invoking /// - dueTime: The amount of time required to pass before invoking the most recent action - /// - identifier: The identifier for grouping actions for debouncing + /// - identifier: (optional) The identifier for grouping actions for debouncing func observeAsync( _ nextState: @escaping () async -> State, debounced dueTime: DispatchQueue.SchedulerTimeType.Stride, identifier: AnyHashable ) - /// Debounces the action calls by `dueTime`, then observes the async sequence of `State`s returned as a result of invoking the action. - /// Prevents actions from being excessively called when bound to noisy UI events. - /// Action calls are grouped by the provided `identifier`. + /// Asynchronously renders the sequence of states on the view. + /// Calls to this function are debounced to prevent excessive execution from noisy events. /// - Parameters: /// - stateSequence: The action to be debounced before invoking /// - dueTime: The amount of time required to pass before invoking the most recent action - /// - identifier: The identifier for grouping actions for debouncing + /// - identifier: (optional) The identifier for grouping actions for debouncing func observeAsync( _ stateSequence: @escaping () async -> SomeAsyncSequence, debounced dueTime: DispatchQueue.SchedulerTimeType.Stride, @@ -80,10 +80,9 @@ public protocol StateObserving { } public extension StateObserving { - - /// Debounces the action calls by `dueTime`, then observes the `State` publisher emitted as a result of invoking the action. - /// Prevents actions from being excessively called when bound to noisy UI events. - /// Action calls are automatically grouped by call location. Use `observe(_:debounced:identifier:)` if you need custom debounce grouping. + + /// Renders the states emitted by the publisher on the view. + /// Calls to this function are debounced to prevent excessive execution from noisy events. /// - Parameters: /// - statePublisher: The action to be debounced before invoking /// - dueTime: The amount of time required to pass before invoking the most recent action @@ -96,9 +95,8 @@ public extension StateObserving { observe(statePublisher(), debounced: dueTime, identifier: HashedIdentifier(file, line)) } - /// Debounces the action calls by `dueTime`, then observes the `State` returned as a result of invoking the action. - /// Prevents actions from being excessively called when bound to noisy UI events. - /// Action calls are automatically grouped by call location. Use `observe(_:debounced:identifier:)` if you need custom debounce grouping. + /// Renders the next state on the view. + /// Calls to this function are debounced to prevent excessive execution from noisy events. /// - Parameters: /// - nextState: The action to be debounced before invoking /// - dueTime: The amount of time required to pass before invoking the most recent action @@ -111,9 +109,8 @@ public extension StateObserving { observe(nextState(), debounced: dueTime, identifier: HashedIdentifier(file, line)) } - /// Debounces the action calls by `dueTime`, then asynchronously observes the `State` returned as a result of invoking the action. - /// Prevents actions from being excessively called when bound to noisy UI events. - /// Action calls are automatically grouped by call location. Use `observe(_:debounced:identifier:)` if you need custom debounce grouping. + /// Asynchronously renders the next state on the view. + /// Calls to this function are debounced to prevent excessive execution from noisy events. /// - Parameters: /// - nextState: The action to be debounced before invoking /// - dueTime: The amount of time required to pass before invoking the most recent action @@ -126,9 +123,8 @@ public extension StateObserving { observeAsync(nextState, debounced: dueTime, identifier: HashedIdentifier(file, line)) } - /// Debounces the action calls by `dueTime`, then observes the async sequence of `State`s returned as a result of invoking the action. - /// Prevents actions from being excessively called when bound to noisy UI events. - /// Action calls are automatically grouped by call location. Use `observe(_:debounced:identifier:)` if you need custom debounce grouping. + /// Asynchronously renders the sequence of states on the view. + /// Calls to this function are debounced to prevent excessive execution from noisy events. /// - Parameters: /// - stateSequence: The action to be debounced before invoking /// - dueTime: The amount of time required to pass before invoking the most recent action @@ -141,3 +137,14 @@ public extension StateObserving { observeAsync(stateSequence, debounced: dueTime, identifier: HashedIdentifier(file, line)) } } + +struct HashedIdentifier: Hashable { + let uniqueValues: [AnyHashable] + + /// Prevents accidental key collisions between auto-generated identifiers and manually generated identifiers + private static let uniqueKey: AnyHashable = UUID() + + init(_ values: AnyHashable ...) { + uniqueValues = [Self.uniqueKey] + values + } +} diff --git a/Sources/VSM/StateContainer/StatePublishing.swift b/Sources/VSM/StateContainer/StatePublishing.swift index 1ff96e6..b6f6bad 100644 --- a/Sources/VSM/StateContainer/StatePublishing.swift +++ b/Sources/VSM/StateContainer/StatePublishing.swift @@ -11,6 +11,6 @@ import Combine public protocol StatePublishing { associatedtype State - /// Publishes the State changes on the main thread + /// Publishes the state changes on the main thread var publisher: AnyPublisher { get } } diff --git a/Sources/VSM/StateSequence.swift b/Sources/VSM/StateContainer/StateSequence.swift similarity index 90% rename from Sources/VSM/StateSequence.swift rename to Sources/VSM/StateContainer/StateSequence.swift index a7c7e61..bc7b618 100644 --- a/Sources/VSM/StateSequence.swift +++ b/Sources/VSM/StateContainer/StateSequence.swift @@ -9,7 +9,7 @@ import Foundation /// Emits multiple `State`s as an `AsyncSequence` /// -/// Usable with ``StateContainer/observe(_:)-4pebt`` (found in ``StateContainer``) +/// Usable with ``StateObserving/observeAsync(_:)-44uer`` (found in ``StateContainer``) /// /// Example Usage /// diff --git a/Sources/VSM/StateObject+StateInit.swift b/Sources/VSM/StateObject+StateInit.swift index cbd374c..e161ab1 100644 --- a/Sources/VSM/StateObject+StateInit.swift +++ b/Sources/VSM/StateObject+StateInit.swift @@ -10,7 +10,7 @@ import SwiftUI @available(macOS 11.0, *) @available(iOS 14.0, *) -@available(*, deprecated, message: "Use the ViewState property wrapper instead") +@available(*, deprecated, message: "Use the @ViewState property wrapper instead.") public extension StateObject { /// VSM convenience initializer for creating a `StateObject>` directly from a `StateContainer.State` value. diff --git a/Sources/VSM/ViewStateRendering.swift b/Sources/VSM/ViewStateRendering.swift index 3a55204..56dd9d2 100644 --- a/Sources/VSM/ViewStateRendering.swift +++ b/Sources/VSM/ViewStateRendering.swift @@ -167,7 +167,7 @@ public extension ViewStateRendering where Self: View { /// - observedSetter: Converts the new `Value` to a new `ViewState`, which is automatically observed /// - Returns: A `Binding` for use in SwiftUI controls func bind(_ stateKeyPath: KeyPath, to observedSetter: @escaping (ViewState, Value) async -> ViewState) -> Binding { - container.bind(stateKeyPath, to: observedSetter) + container.bindAsync(stateKeyPath, to: observedSetter) } /// Creates a unidirectional, auto-observing `Binding` for the `ViewState` using a `KeyPath` and a *method signature* @@ -178,7 +178,7 @@ public extension ViewStateRendering where Self: View { /// - observedSetter: A **method signature** which converts the new `Value` to a new `ViewState` and is automatically observed /// - Returns: A `Binding` for use in SwiftUI controls func bind(_ stateKeyPath: KeyPath, to observedSetter: @escaping (ViewState) -> (Value) async -> ViewState) -> Binding { - container.bind(stateKeyPath, to: observedSetter) + container.bindAsync(stateKeyPath, to: observedSetter) } }