"The UI is a function of the state" - Apple
Declarative UI frameworks like SwiftUI require us to rethink how we architect our apps.
Widely known design patterns like MVVM aren't cutting it anymore and most UDF libraries require us to write a lot of boilerplate code.
SwiftUDF provides a streamlined development experience for how to setup SwiftUI views using the unidirectional data flow pattern, support by code generation.
The responsibility of a view is to render data, referred to as State
, and pass through the inputs received from the user, referred to as Event
.
protocol BindableView: View {
associatedtype State
associatedtype Event
init(state: State, handler: @escaping (Event) -> Void)
}
Example
Imagine a simple counter app, displaying a total count and two buttons to increment or decrement that count.
The State
and Event
could look like this:
struct CounterState {
let count: Int // immutable fields for thread safety!
}
enum CounterEvent {
case increase
case decrease
}
The view would conform to BindableView
like this:
struct CounterView: BindableView {
let state: CounterState
let handler: (CounterEvent) -> Void
var body: some View { ... }
}
This simple setup enables us to harness the power of previews by building our UI with all the different states that might occur:
CounterView.preview(.init(count: 0))
CounterView.preview(.init(count: 3))
...
The counter part to BindableView
, providing the state and receiving the user input, referred to as Loop
.
protocol ViewProvider {
associatedtype State
associatedtype Event
var state: CurrentValuePublisher<State> { get }
func handle(_ event: Event)
func start() // called on view appear
func stop() // called on view disappear
}
CurrentValuePublisher
is a custom Combine.Publisher
providing an additional read-only value, representing the current state of the Loop
.
Example
Continuing with our counter app example, a basic Loop
implementation could look as followed:
/// @Loop(CounterState, CounterEvent)
final class CounterLoop: GeneratedBaseCounterLoop {
override func increase() {
updateCount(count + 1)
}
override func decrease() {
let updatedCount = max(0, count - 1)
updateCount(updatedCount)
}
}
Using the @Loop(State, Event)
annotation, SwiftUDF
will generate a "BaseLoop" class
including the following functionalities:
- first level read-only variables for each field of the
State
each field of theState
- update functions for each field of the
State
- a dedicated function for every user input aka every case of the
Event
enum
Instantiating and binding a view with the provider is straight forward:
CounterView.create(using: CounterLoop())
SwiftUDF
will wrap your view into a container, subscribing to the loop's state, calling start
and stop
of the loop and ensuring all state updates are dispatched on the main thread.
To see SwiftUDF
in action, please checkout the demo project. It contains a slightly evolved example of the counter app, compatible with iOS and macOS, including tests.
Contributions are welcomed and encouraged!
It is easy to get involved. Open an issue to discuss a new feature, write clean code, show some love using unit tests and open a Pull Request.
PS: Check the open issues and pull requests for existing discussions.
SwiftEvolution is available under the MIT license.