TBD: updated description
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.3.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 = GoodReactor.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 var model: AnyReactor = MyViewModel().eraseToAnyReactor()
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)
struct SampleLogger: ReactorLogger {
func logReactorEvent(_ message: Any, level: LogLevel, fileName: String, lineNumber: Int) {
print("[\(level)] \(message) (\(fileName):\(lineNumber))")
}
}
ReactorConfiguration.logger = SampleLogger()
You can easily mock state for Xcode Previews by using Stub
reactor implementation:
#Preview("Empty") {
NavigationStack {
HomeScreen(viewModel: Stub<HomeScreenViewModel> {
let state = HomeScreenViewModel.State()
state.items = [] // use empty array to mock empty state
return state
}.eraseToAnyReactor())
}
}
GoodReactor repository is released under the MIT license. See LICENSE for details.