TestsTested | ✓ |
LangLanguage | SwiftSwift |
License | MIT |
ReleasedLast Release | Jan 2016 |
SPMSupports SPM | ✗ |
Maintained by Benjamin Encz.
If you landed here, you should also check out the great work over at ReduxKit, which brings the original Redux API to Swift. We all agree that collaboration and sharing of idea’s will be beneficial to both projects. Go check it out. Feedback on both projects would be appreciated.
This library is a pre-release. Expect missing docs and breaking API changes.
Swift Flow is a Redux-like implementation of the unidirectional data flow architecture in Swift. It embraces a unidirectional data flow that only allows state mutations through declarative actions.
It relies on a few principles:
For a very simple app, that maintains a counter that can be increased and decreased, you can define the app state as following:
struct AppState: StateType {
var counter: Int = 0
}
You would also define two actions, one for increasing and one for decreasing the counter. In the Getting Started Guide you can find out how to construct complex actions. For the simple actions in this example we can define empty structs that conform to action:
struct CounterActionIncrease: Action {}
struct CounterActionDecrease: Action {}
Your reducer needs to respond to these different action types, that can be done by switching over the type of action:
struct CounterReducer: Reducer {
func handleAction(var state: AppState, action: Action) -> AppState {
switch action {
case _ as CounterActionIncrease:
state.counter += 1
case _ as CounterActionDecrease:
state.counter -= 1
default:
break
}
return state
}
}
In order to have a predictable app state, it is important that the reducer is always free of side effects, it receives the current app state and an action and returns the new app state.
Lastly, your view layer, in this case a view controller, needs to tie into this system by subscribing to store updates and emitting actions whenever the app state needs to be changed:
class CounterViewController: UIViewController, StoreSubscriber {
@IBOutlet var counterLabel: UILabel!
override func viewWillAppear(animated: Bool) {
mainStore.subscribe(self)
}
override func viewWillDisappear(animated: Bool) {
mainStore.unsubscribe(self)
}
func newState(state: AppState) {
counterLabel.text = "\(state.counter)"
}
@IBAction func increaseButtonTapped(sender: UIButton) {
mainStore.dispatch(
CounterActionIncrease()
)
}
@IBAction func decreaseButtonTapped(sender: UIButton) {
mainStore.dispatch(
CounterActionDecrease()
)
}
}
The newState
method will be called by the Store
whenever a new app state is available, this is where we need to adjust our view to reflect the latest app state.
Button taps result in dispatched actions that will be handled by the store and its reducers, resulting in a new app state.
This is a very basic example that only shows a subset of Swift Flow’s features, read the Getting Started Guide to see how you can build entire apps with this architecture.
Model-View-Controller (MVC) is not a holistic application architecture. Typical Cocoa apps defer a lot of complexity to controllers since MVC doesn’t offer other solutions for state management, one of the most complex issues in app development.
Apps built upon MVC often end up with a lot of complexity around state management and propagation. We need to use callbacks, delegations, Key-Value-Observation and notifications to pass information around in our apps and to ensure that all the relevant views have the latest state.
This approach involves a lot of manual steps and is thus error prone and doesn’t scale well in complex code bases.
It also leads to code that is difficult to understand at a glance, since dependencies can be hidden deep inside of view controllers. Lastly, you mostly end up with inconsistent code, where each developer uses the state propagation procedure they personally prefer. You can circumvent this issue by style guides and code reviews but you cannot automatically verify the adherence to these guidelines.
Swift Flow attempts to solve these problem by placing strong constraints on the way applications can be written. This reduces the room for programmer error and leads to applications that can be easily understood - by inspecting the application state data structure, the actions and the reducers.
This architecture provides further benefits beyond improving your code base:
The Swift Flow tooling is still in a very early stage, but aforementioned prospects excite me and hopefully others in the community as well!
A Getting Started Guide that describes the core components of apps built with Swift Flow lives here. It will be expanded in the next few weeks. To get an understanding of the core principles I recommend reading the brilliant redux documentation.
Due to an issue in Nimble at the moment, tvOS tests will fail if building Nimble / Quick from source. You can however install Nimble & Quick from binaries then rebuild OSX & iOS only. After checkout, run the following from the terminal:
carthage bootstrap && carthage bootstrap --no-use-binaries --platform ios,osx
Using this library you can implement apps that have an explicit, reproducible state, allowing you, among many other things, to replay and rewind the app state, as shown below:
This repository contains the core component for Swift Flow, the following extensions are available:
Store
implementation that records all Action
s and allows for hot-reloading and time travelThere’s still a lot of work to do here! I would love to see you involved! Some design decisions for the core of Swift Flow are still up in the air (see issues), there’s lots of useful documentation that can be written and a ton of extensions and tools are waiting to be built on top of Swift Flow.
I personally think the best way to get started contributing to this library is by using it in one of your projects!
If you have any questions, you can find me on twitter @benjaminencz.