In my previous series of articles I explored how to implement the Redux pattern in Kotlin, on Android. The reality is that the same can be applied in Swift, on iOS, without much hassle.
Of course, there are already a few libraries out there that implement a unidirectional data flow. One that I’ve tried is ReSwift. But the good thing about Redux is that it’s pretty simple to implement and thus you can easily roll out your own version, one that’s better suited to your needs.
This is what we’re going to do in this article, using Swift5 and XCode 11 Beta 4. Which means we’re going to use some of the new features in Swift 5 — like SwiftUI — to make our code even better. The example we’ll finally implement is the classic counter app.
Before we can build it, we’ll first need to create a few building blocks.
Let’s start from the very abstract and move our way down to the concrete.
Redux operates with a few basic elements: the Action, the State, the Reducer and the Store, with each one playing a different, distinct, part.
We can define the Action and the State as nothing more than two empty marker protocols:
The Reducer is just a function that takes in an Action and a State in order to return a new State.
Reducers are the only entities allowed to change the app’s state. They play an important role in handling the app’s business logic.
Finally, there’s the Store. We want our simple Store to have three basic characteristics:
- to hold the only reference to the whole app State
- to intercept actions and pass them to Reducers
- to notify listeners of State changes
Thus, an abstract Store will look like this:
A few key points here:
We’ve made the Store implement Swift5’s new BindableObject protocol. This will allow us to remove a lot of the subscription and notification boilerplate down the line.
Secondly, we’ve defined the Store as being generic of a type “S conforming to ReduxState” so we can reuse this code with any State we might want, in different apps.
A default simple implementation might look like this:
Notice we’ve hidden away the mutable state property to avoid any temptation to ever change it manually.
Notice also the PassthroughSubject called willChange. This comes from another new Apple framework, called Combine, and is the only requirement the BindableObject protocol imposes on its implementers. Using it we can send out an update each time the internal State changes.
Now that we have our building blocks in place, it’s time to use them to create the logic for our simple counter app.
In order to do all of the above, we’ll need a State to hold the counter value and two actions.
So far, so good. Next we’ll also need to add business logic in the mix by defining a CounterStateReducer:
Notice the Reducer always returns a new instance of CounterState if the increment or decrement actions pass through. This is a key point for all Reducers: they must never simply mutate existing state, but rather enforce immutability by returning new State instances.
Side effects of having such strict rules are that most reducers will look similar to the one above and that they’re quite straight forward to test:
Finally, we can initialise our app’s Store with all the entities we’ve built:
Since we’ve built the DefaultStore to also be generic, we can use it with different states in different apps and it would all work the same way.
Finally we get to the app side. As seen in the previous screenshot, it’s pretty simple: two buttons, one to increment and one to decrement, and a label presenting the current counter value.
The basic code for it is shown below:
Of course, it doesn’t yet do anything. We first need to connect it to the Store by adding an ObjectBinding reference to it. ObjectBinding is the reverse of BindableObject, which our Store implements. SwiftUI will automatically link these two together so that each time the Store’s internal PassthroughSubject calls send(), the View will be redrawn.
Next, in order to update the value of the counter Text we can create a computed property, called “props”, that binds to the current Store State. The name “props” is pretty common in React and ReactNative to describe the properties of a certain view.
Finally, we can replace the empty actions our increment and decrement buttons have right now with appropriate calls to the store’s dispatch method:
And that’s all there is to it. When taken together, the Store, State, Action and Reducer code as well as the CounterView code form a unidirectional system.
The View dispatches Actions to the Store following a user interaction, the Store intercepts these Actions and produces a new State with the help of a Reducer and finally the View gets notified of changes and redraws itself.
This article is one of a multi-part series exploring State, UI and Redux and how we can leverage knowledge of these topics to write more modular and testable code in both iOS and Android.