Swux is a Swiftier implementation of Redux inspired by ReSwift.
Add this to your Podfile:
pod 'Swux', :git => 'https://github.com/wircho/Swux.git'
Add this to your Cartfile:
github "wircho/Swux"
Make sure to specify the platform (iOS or MacOS) when updating the Carthage builds. Use either carthage update --platform ios
or carthage update --platform macos
accordingly.
The application state is a structure that should uniquely define the state of your app's UI at any point in time. For example, an app that displays a single integer counter could have the following state:
import Swux
struct AppState {
var counter: Int
}
The AppState
above may be mutated by incrementing or decrementing the counter, for example, so we could define those actions as follows:
struct IncrementCounter: ActionProtocol {
func mutate(_ state: inout AppState) { state.counter += 1 }
}
struct DecrementCounter: ActionProtocol {
func mutate(_ state: inout AppState) { state.counter -= 1 }
}
You could also add an action that sets the counter to a specific value, for example:
struct SetCounter: ActionProtocol {
let value: Int
func mutate(_ state: inout AppState) { state.counter = value }
}
The store is responsible for storing the application state and queuing up actions. You must initialize it with an initial state:
let store = Store(AppState(counter: 0))
You may submit actions to the store using the dispatch
method as follows:
store.dispatch(IncrementCounter())
store.dispatch(SetCounter(value: 5))
You can subscribe objects implementing SubscriberProtocol
to receive state change updates from the stateChanged(to newState: AppState)
method. Use let disposable = store.subscribe(self)
and retain the returned Disposable
object until you no longer need to listen to state updates. From a view controller you could do:
class ViewController: UIViewController, SubscriberProtocol {
var disposable: Disposable?
override func viewDidLoad() {
super.viewDidLoad()
disposable = store.subscribe(self)
}
func stateChanged(to newState: AppState) {
/* Use newState to update the UI */
}
}
If your application state is optional, for example,
struct AppState {
/* some properties */
}
let store = Store<AppState?>(nil)
you may dispatch actions whose state type is the unwrapped type AppState
. These actions' mutators are only performed internally when the state is not nil
.
Because of Swift's excluse ownership feature and copy-on-write types, you may be able to completely avoid copying your state by always performing mutations in place and avoiding multiple copies of the same structure. This is difficult to avoid for enum
states with associated types, since Swift does not currently provide in-place mutation of enum
types. One solution is to temporarily erase values while you mutate their local copies. You may conveniently define and extend one helper protocol for each enum
case that has associated values. The following example illustrates this technique:
/* STATE */
enum AppState {
case uninitialized
case point(CGPoint)
case segment(CGPoint, CGPoint)
}
/* HELPER PROTOCOLS */
protocol PointActionProtocol: ActionProtocol where State == AppState {
func mutatePoint(_ point: inout CGPoint)
}
extension PointActionProtocol {
func mutate(_ state: inout AppState) {
switch state {
case .point(var point):
state = .uninitialized
mutatePoint(point)
state = .point(point)
default: return
}
}
}
protocol SegmentActionProtocol: ActionProtocol where State == AppState {
func mutateSegment(_ first: inout CGPoint, _ second: inout CGPoint)
}
extension SegmentActionProtocol {
func mutate(_ state: inout AppState) {
switch state {
case var .segment(first, second):
state = .uninitialized
mutateSegment(first, second)
state = .segment(first, second)
default: return
}
}
}
/* USAGE */
struct MovePoint: PointActionProtocol {
let by: CGVector
func mutatePoint(_ point: inout CGPoint) {
point.x += by.dx
point.y += by.dy
}
}
struct MoveSegment: SegmentActionProtocol {
let by: CGVector
func mutateSegment(_ first: inout CGPoint, _ second: inout CGPoint) {
first.x += by.dx
first.y += by.dy
second.x += by.dx
second.y += by.dy
}
}
When you subscribe to the store's state updates, you may specify an optional DispatchQueue
as follows:
disposable = store.subscribe(self, on: DispatchQueue.global(qos: .background))
This way, the stateChanged
method is asynchronously dispatched to the specified queue. If you do not speficy a queue, the stateChanged
method is called on the main thread.
You may dispatch actions asynchronously (to the store's action queue) by specifying a DispatchMode
as follows:
store.dispatch(SomeAction(), dispatchMode: .async)
By default all actions are performed synchronously.