VSM v1.1 Release Notes
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
, andRenderedViewState
. (#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.
- Resolve any compiler errors resulting from the breaking changes.
SwiftUI
- Replace any uses of
$state.publisher
with$state.willSetPublisher
. - Test features to detect and fix any view-logic regressions.
UIKit
- Replace any uses of
$state.publisher
with either$state.willSetPublisher
or$state.didSetPublisher
, depending on the requirement. - (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.