Skip to content

VSM v1.1 Release Notes

Compare
Choose a tag to compare
@albertbori albertbori released this 06 Mar 19:48
· 10 commits to main since this release
3c2cbb3

VSM v1.1 contains several bug fixes and improvements to the framework with one minor (rename only) breaking change.

New Features

Event-Specific State Publishers

This release introduces a new, preferred way of observing state changes for miscellaneous view-logic (animations, etc.) through event-specific state publishers. The willSetPublisher and didSetPublisher replace the (deprecated) publisher property on the StateContainer object and the @ViewState and @RenderedViewState property wrappers. These publishers publish state changes in the willSet and didSet view state events, respectively. This solves an issue with excessive body calls in SwiftUI and provides more flexibility to both SwiftUI and UIKit views for working with and comparing a view's view state. (#31)

Example Usage

// SwiftUI
struct MyView: View {
    @ViewState var state: MyViewState
    @State var progress: Double = 0
    
    var body: some View {
        ProgressView("Loading...", value: progress)
            .onAppear {
                if case .initialized(let loaderModel) = state {
                    $state.observe(loaderModel.load())
                }
            }
            .onReceive($state.willSetPublisher) { newState in
                switch (state, newState) {
                case (.loading(let oldLoadingModel), .loading(let newLoadingModel)):
                    guard oldLoadingModel.loadedBytes < newLoadingModel.loadedBytes else { return }
                    print(">>> Animating progress from \(oldLoadingModel.loadedBytes) to \(newLoadingModel.loadedBytes) bytes")
                    withAnimation() {
                        progress = newLoadingModel.loadedBytes / newLoadingModel.totalBytes
                    }
                default:
                    break
                }
            }
    }
}

State Comparison in RenderedViewState

The @RenderedViewState property wrapper can now be initialized with alternative render function signature that allows engineers to compare the current and future state just before the new state is set. As of this release the two acceptable render function signatures are: render() and the new render(_ newState: SomeViewState). This allows engineers to perform any necessary view-logic where the current and future view states need to be compared. (Animations, conditional view updates, etc.) This new option will increase both the performance and accuracy of VSM views in UIKit. (#32)

Example Usage

// UIKit
class MyViewController: UIViewController {
    @RenderedViewState
    var state: MyViewState
    ...
    init() {
        _state = .init(wrappedValue: .initialized(LoaderModel()), render: Self.render)
    }
    ...
    func render(_ newState: MyViewState) {
        if state.saveProgress < newState.saveProgress) {
            animateSaveProgress(from: state.saveProgress, to: newState.saveProgress)
        }
    }
}

Manual Rendering Kickoff

When using the @RenderedViewState property wrapper, any access to the wrapped value (aka the view state) after initialization will trigger the automatic view state rendering to begin. There are situations where this is insufficient. To provide a solution for alternative cases where automatic rendering needs an explicit kickoff point, a new function was added to the property wrapper's projected value that will begin rendering the view state without the need to access the view state property. This new function is called like so: $state.startRendering(on: self). It can be invoked in any UIKit view lifecycle event (or equivalent). (#29)

Example Usage

// UIKit
class MyViewController: UIViewController {
    @RenderedViewState
    var state: MyViewState
    ...
    init() {
        _state = .init(wrappedValue: .initialized(LoaderModel()), render: Self.render)
    }
    ...
    func viewDidLoad() {
        super.viewDidLoad()
        $state.startRendering(on: self)
    }
    ...
}

Bug Fixes

None

Documentation Updates

  • The documentation, guides, and examples have been updated to promote the new features above. (#34)

Breaking Changes

  • In the last release, the @autoclosure parameter decorator was accidentally omitted from a function signature for observing a debounced action. This has been corrected in this release, but it may require callers to add function call indicators to invocation points. e.g. $state.observe(state.foo, debounced: 0.5) -> $state.observe(state.foo(), debounced: 0.5). This is a spelling-only breakage and has no impact on the functionality. (#33 | PR Comment)

Internal Framework Changes

  • The demo Shopping app has been fully converted to use these new features and best practices. (#34)
  • Unit tests were reorganized by public API and expanded to test every concrete implementation of public APIs. These concrete implementations are StateContainer, ViewState, and RenderedViewState. (#33)
  • CI tasks will now build asynchronously, allowing for faster build times. (#21)

Migration Instructions

All Frameworks

Note: The $state.publisher property has been deprecated. It will be removed in a future version of the VSM framework. Its usage will produce compiler warnings.

  1. Resolve any compiler errors resulting from the breaking changes.

SwiftUI

  1. Replace any uses of $state.publisher with $state.willSetPublisher.
  2. Test features to detect and fix any view-logic regressions.

UIKit

  1. Replace any uses of $state.publisher with either $state.willSetPublisher or $state.didSetPublisher, depending on the requirement.
  2. (Optional) Implement the new render(_ newState: SomeViewState) in place of other mechanisms where new and old state were needing to be compared to support special view-logic while rendering.