Skip to content

johnsusek/fluxus

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

64 Commits

Repository files navigation

Fluxus

⚠️Fluxus is no longer maintained, and may not be using latest SwiftUI best practices.

👉 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.

Requirements

Xcode 11 beta on MacOS 10.14 or 10.15

Installation

In Xcode, choose File -> Swift Packages -> Add Package Dependency and enterthis repo's URL.

Concepts

  • 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

Obligatory Flux Diagram

When should I use it?

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.

Example apps

Articles

Usage

Create state

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()
}

Create mutations/committers

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
}
}

Create actions/dispatchers

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))
})
}
}

Create store

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!")
}
}
}

Add store to environment

We now provide the store to our views inside SceneDelegate.swift.

window.rootViewController=UIHostingController(rootView:ContentView().environmentObject(rootStore))

Use in views

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

Simulator Screen Shot - iPhone Xs - 2019-06-17 at 15 32 11

💡 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.

Where to go from here

Check out thelandmarks example appto see fluxus used in a more complex app environment.

Troubleshooting

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.

Feedback

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.

Other SwiftUI Flux implementations

About

Flux for SwiftUI, inspired by Vuex

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages