Swivel
Yet another architecture framework.
Inspired from Elm and Redux. Optimized for Swift.
Features
- Swifty interfaces: type-safe and conprehensive
- Fancy and efficient value observation with Swift 4 KeyPaths
- Architectural separation of asynchronicity and side effects with Effects
- Accessing from multiple threads are supported (not yet fully tested)
- No specific Reactive framework dependency (Reactive adapters for RxSwift/ReactiveSwift are coming!)
Installation
Carthage
github "akkyie/Swivel" ~> 0.1
CocoaPods
pod "Swivel", "~> 0.1"
SwiftPM
.package(url: "https://github.com/akkyie/Swivel.git", from: "0.1"),
Concepts
State describes a state. Think you are implementing a counter:
struct CounterState {
var count: Int
}
A counter should be incremented, and sometimes decremented. You can enumerate how the state is updated, as Message:
enum CounterMessage: Message {
case increment(amount: Int)
case decrement(amount: Int)
}
And now you can describe how a CounterState
is updated when it receives a message,
to make it conform to State
protocol.
struct CounterState: State {
var count: Int = 0
static func update(_ state: inout CounterState, with message: Message) -> Effect? {
guard let message = message as? CounterMessage else { return nil }
switch message {
case let .increment(amount):
state.count += amount
return nil
case let .decrement(amount):
state.count -= amount
return nil
}
}
}
States in Swivel can be described as Model+Update in Elm, or State+Reducer in Redux.
Messages are apparently Msg
in Elm, or Action in Redux.
The update
function of a state should be pure (similarly to Elm's updates and Redux's reducers.) You should not do anything asynchronous, e.g. API calls or database accesses over the network. Use Effect, a concept borrowed from Elm's Command, to make these side effects.
Subscription
You can watch for changes of a State with subscribe
method of the Store.
By default, the closures get called right after subscription, and everytime messages are dispatched, even when the state is not changed:
let store = Store(initialState: CounterState(count: 0))
let unsubscribe = store.subscribe(\CounterState.count) { count in
print("Count: \(count)")
}
store.dispatch(CounterMessage.increment(1))
store.dispatch(CounterMessage.increment(2))
store.dispatch(CounterMessage.increment(0))
store.dispatch(CounterMessage.increment(0))
// Count: 0
// Count: 1
// Count: 3
// Count: 3
// Count: 3
You can change this behavior by passing immediately: false
or skipRepeats: true
:
let store = Store(initialState: CounterState(count: 0))
let unsubscribe = store.subscribe(\CounterState.count, immediately: false, skipRepeats: true) { count in
print("Count: \(count)")
}
store.dispatch(CounterMessage.increment(1))
store.dispatch(CounterMessage.increment(2))
store.dispatch(CounterMessage.increment(0))
store.dispatch(CounterMessage.increment(0))
// Count: 1
// Count: 3
You can specify skipRepeats: true
only when the subscribed value (\CounterState.count
in the example above) is Equatable
.
Asynchronicity and Side Effects
Effect is what a state's update
function returns and describes the procedure to asynchronous dispatch or any side effects. Example:
enum CounterEffect {
static func requestAndIncrement() -> Effect {
return { dispatch in
request("https://pastebin.com/raw/uQ8LH0Gr") { (result: Int) in
dispatch(CounterMessage.increment(amount: result))
}
}
}
}
struct CounterState: State {
var count: Int = 0
var isLoading: Bool = false
static func update(state: inout CounterState, message: Message) -> Effect? {
guard let message = message as? CounterMessage else { return nil }
switch message {
case let .increment(amount):
state.count += amount
state.isLoading = false
return nil
case let .requestAndIncrement():
state.isLoading = true
return CounterEffect.requestAndIncrement() // <-- Returning an Effect
}
}
}
Separation of concerns: Substates and composition of updates
Your states may have other states as it's property in order to divide a state into a number of substates:
struct AppState: State {
var user: UserState
var auth: AuthState
var document: DocumentState
static func update(_ state: inout AppState, with message: Message) -> Effect? {
let update = makeSerialUpdate(\.user, \.auth, \.document)
return update(state, message)
}
}
makeSerialUpdate(states: State...)
constructs a new update function which updates given substates serially with a message. Effects returned from each states are called after all state changes are made.