GoodReactor is an adaptation of the Reactor framework that is Redux inspired. The view model communicates with the view controller via the State and with the Coordinator via the navigation function. You communicate to the viewModel via Actions Viewmodel changes state in the Reduce function Viewmodel interactes with dependencies outside of the Reduce function not to create side-effects
Link to the original reactor kit: https://github.com/ReactorKit/ReactorKit
Create a Package.swift
file and add the package dependency into the dependencies list.
Or to integrate without package.swift add it through the Xcode add package interface.
import PackageDescription
let package = Package(
name: "SampleProject",
dependencies: [
.package(url: "https://github.com/GoodRequest/GoodReactor" .upToNextMajor("2.0.0"))
]
)
In your ViewModel define Actions, Mutations, Destinations and the State
- State defines all data of a View (or a ViewController)
- Action represents user actions that are sent from the View.
- Mutation represents state changes from external sources.
- Destination represents all possible destinations, where user can navigate.
@Observable final class ViewModel: Reactor {
enum Action {
case login(username: String, password: String)
}
enum Mutation {
case didReceiveAuthResponse(Credentials)
}
enum Destination {
case homeScreen
case errorAlert
}
@Observable final class State {
var username: String
var password: String
}
}
You can provide the initial state of the view in the makeInitialState
function.
func makeInitialState() -> State {
return State()
}
Finally in the reduce
function you define how state
changes, according to certain event
s:
typealias Event = NewReactor.Event<Action, Mutation, Destination>
func reduce(state: inout State, event: Event) {
switch event.kind {
case .action(.login(...)):
// ...
case .mutation:
// ...
case .destination:
// ...
}
}
You can run asynchronous tasks by using run
and returning the result in form of a Mutation
.
func reduce(state: inout State, event: Event) {
switch event.kind {
case .action(.login(let username, let password)):
run(event) {
let credentials = await networking.login(username, password)
return Mutation.didReceiveAuthResponse(credentials)
}
// ...
case .mutation(.didReceiveAuthResponse(let credentials)):
// proceed with login
}
}
You can listen to external changes by subscribe
-ing to event Publisher
-s.
You start the subscriptions by calling the start()
function.
// in ViewModel:
func transform() {
subscribe {
await ExternalTimer.shared.timePublisher
} map: {
Mutation.didChangeTime(seconds: $0)
}
}
// in View (SwiftUI):
var body: some View {
MyContentView()
.task { viewModel.start() }
}
You add the ViewModel as a property wrapper to your view:
@ViewModel private var model = MyViewModel()
To access the current State
you use:
// read-only access
Text(model.username)
// binding (refactored to a variable for better readability)
let binding = model.bind(\.username, action: { .setUsername($0) })
TextField("Username", text: binding)
To send an event to the ViewModel you call:
model.send(action: .login(username, password))
model.send(destination: .errorAlert)
From UIViewController
(in UIKit, or any other frameworks) you can send actions to ViewModel via Combine:
myButton.publisher(for: .touchUpInside).map { _ in .login(username, password) }
.map { .action($0) }
.subscribe(model.eventStream)
.store(in: &cancellables)
Then use Combine to subscribe to state changes, so every time the state is changed, ViewController can be updated as well:
reactor.stateStream
.map { String($0.username) }
.assign(to: \.text, on: usernameLabel, ownership: .weak)
.store(in: &cancellables)
GoodReactor repository is released under the MIT license. See LICENSE for details.