👉 I encourage you to look at thesource of Fluxus.If you do, you'll realize this issimply a patternmore than a framework, so please study and you can roll your own Vuex-style SwiftUI store.
Fluxus is an implementation of the Flux pattern for SwiftUI that replaces MVC, MVVM, Viper, etc.
- Organize all your model data into a store and easily access in your views.
- Use mutations to modify your app's state.
- Use actions to perform asynchronous operations.
- Keep your models and views as simple as possible.
Xcode 11 beta on MacOS 10.14 or 10.15
In Xcode, choose File -> Swift Packages -> Add Package Dependency and enterthis repo's URL.
- Stateis the root source of truth for your app
- Mutationsdescribe a synchronous change in state
- Committersapply mutations to the state
- Actionsdescribe an asynchronous operation
- Dispatchersexecute asynchronous actions and commit mutations when complete
Fluxus helps us deal with shared state management at the cost of more concepts and boilerplate. If you're not building a complex app, and jump right into Fluxus, it may feel verbose and unnecessary. If your app is simple, you probably don't need it. But once your app grows to a certain complexity, you'll start looking for ways to organize shared state, and Fluxus is here to help with that. To quote Dan Abramov, author of Redux:
Flux libraries are like glasses: you’ll know when you need them.
Using Fluxus doesn't mean you should putallyour state in Fluxus.If a piece of state strictly belongs to a single View, it might be fine to just use local @State. Check out the landmarks example to see how local @State and Fluxus state can work together.
- Theminimal example appincludes all the below code in a ready to run sample.
- Thelandmarks example appis a reimplementation of the official landmarks tutorial app using fluxus.
- Thetodo example appis a very simple implementation of a todo list.
State is the root source of truth for the model data in your app. We create one state module, for a counter, and add it to the root state struct.
import Fluxus
structCounterState:FluxState{
varcount=0
varmyBoolValue=false
varcountIsEven:Bool{
get{
returncount%2==0
}
}
funccountIsDivisibleBy(_ by:Int)->Bool{
returncount%by==0
}
}
structRootState{
varcounter=CounterState()
}
Mutations describe a change in state. Committers receive mutations and modify the state.
import Fluxus
enumCounterMutation:Mutation{
caseIncrement
caseAddAmount(Int)
caseSetMyBool(Bool)
}
structCounterCommitter:Committer{
funccommit(state:CounterState,mutation:CounterMutation)->CounterState{
varstate=state
switch mutation{
case.Increment:
state.count+=1
case.AddAmount(letamount):
state.count+=amount
case.SetMyBool(letvalue):
state.myBoolValue=value
}
returnstate
}
}
Actions describe an asynchronous operation. Dispatchers receive actions, then commit mutations when the operation is complete.
import Foundation
import Fluxus
enumCounterAction:Action{
caseIncrementRandom
caseIncrementRandomWithRange(Int)
}
structCounterDispatcher:Dispatcher{
varcommit:(Mutation)->Void
funcdispatch(action:CounterAction){
switch action{
case.IncrementRandom:
IncrementRandom()
case.IncrementRandomWithRange(letrange):
IncrementRandom(range:range)
}
}
funcIncrementRandom(range:Int=100){
// Simulate API call that takes 150ms to complete
DispatchQueue.main.asyncAfter(deadline:.now()+.milliseconds(150),execute:{
letexampleResultFromAsyncOperation=Int.random(in:1..<range)
self.commit(CounterMutation.AddAmount(exampleResultFromAsyncOperation))
})
}
}
The store holds the current state. It also provides commit and dispatch methods, which route mutations and actions to the correct modules.
import SwiftUI
import Combine
import Fluxus
letrootStore=RootStore()
finalclassRootStore:BindableObject{
vardidChange=PassthroughSubject<RootStore,Never>()
varstate=RootState(){
didSet{
didChange.send(self)
}
}
funccommit(_ mutation:Mutation){
switch mutation{
caseisCounterMutation:
state.counter=CounterCommitter().commit(state:self.state.counter,mutation:mutationas!CounterMutation)
default:
print("Unknown mutation type!")
}
}
funcdispatch(_ action:Action){
switch action{
caseisCounterAction:
CounterDispatcher(commit:self.commit).dispatch(action:actionas!CounterAction)
default:
print("Unknown action type!")
}
}
}
We now provide the store to our views inside SceneDelegate.swift.
window.rootViewController=UIHostingController(rootView:ContentView().environmentObject(rootStore))
ContentView.swift:
import SwiftUI
structContentView:View{
@EnvironmentObjectvarstore:RootStore
varbody:someView{
NavigationView{
Form{
// Read the count from the store, and use a getter function to decide color
Text("Count:\(store.state.counter.count)")
.color(store.state.counter.countIsDivisibleBy(3)?.orange:.green)
Section{
// Commit a mutation without a param
Button(action:{self.store.commit(CounterMutation.Increment)}){
Text("Increment")
}
// Commit a mutation with a param
Button(action:{self.store.commit(CounterMutation.AddAmount(5))}){
Text("Increment by amount (5)")
}
// Dispatch an action without a param
Button(action:{self.store.dispatch(CounterAction.IncrementRandom)}){
Text("Increment random")
}
// Dispatch an action with a param
Button(action:{self.store.dispatch(CounterAction.IncrementRandomWithRange(20))}){
Text("Increment random with range (20)")
}
}
// Use with bindings
Toggle(isOn:myToggleBinding){
Text("My boolean is:\(myToggleBinding.value?"true":"false")")
}
}.navigationBarTitle(Text("Fluxus Example"))
}
}
// Use computed properties to get/set state via a binding
varmyToggleBinding=Binding<Bool>(
getValue:{
rootStore.state.counter.myBoolValue
},
setValue:{valuein
rootStore.commit(CounterMutation.SetMyBool(value))
})
}
#if DEBUG
structContentView_Previews:PreviewProvider{
staticvarpreviews:someView{
returnContentView().environmentObject(rootStore)
}
}
#endif
💡 You should now have an app that demonstrates the basics of the flux pattern with Fluxus & SwiftUI. If you're having trouble getting this running, download the example app, or file a Github issue and we'll try to help.
Check out thelandmarks example appto see fluxus used in a more complex app environment.
Swift/SourceKit are using 100% CPU!
This is a bug in Xcode 11 beta, it usually means something is wrong with your @EnvironmentObject, make sure you are passing.environmentObject() to your view correctly.
If you are presenting a new view (e.g. a modal) you will have to pass.environmentObject(store) to it, just like your root view controller.
Please file an issue if you spot a bug or think of a better way to do something.
Follow me on twitter@jsusekfor random thoughts on SwiftUI.