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
letpackage=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.
@ObservablefinalclassViewModel:Reactor{enumAction{case login(username:String, password:String)}enumMutation{case didReceiveAuthResponse(Credentials)}enumDestination{case homeScreen
case errorAlert
}@ObservablefinalclassState{varusername:Stringvarpassword:String}}You can provide the initial state of the view in the makeInitialState function.
func makeInitialState()->State{returnState()}Finally in the reduce function you define how state changes, according to certain events:
typealiasEvent=GoodReactor.Event<Action,Mutation,Destination>func reduce(state:inoutState, 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:inoutState, event:Event){switch event.kind {case.action(.login(let username,let password)):run(event){letcredentials=await networking.login(username, password)returnMutation.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{awaitExternalTimer.shared.timePublisher
} map:{Mutation.didChangeTime(seconds: $0)}}
// in View (SwiftUI):
varbody:someView{MyContentView().task{ viewModel.start()}}You add the ViewModel as a property wrapper to your view:
@ViewModelvarmodel:AnyReactor=MyViewModel().eraseToAnyReactor()AnyReactor does not start the wrapped reactor automatically. Start it explicitly:
.task { model.start()}To access the current State you use:
// read-only access
Text(model.username)
// binding (refactored to a variable for better readability)
letbinding= 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)structSampleLogger: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>{letstate=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.
