Redux in Swift Pt. 1: What, why, and how
Learn the basics of the Redux software engineering paradigm, and how to build your own Swift implementation of it, with all the code you’ll need to get started.
May 2019 update:
MVC has served us well over the years, but it has also failed us. I won’t blather on about how MVC gets bad in complex software; for more on that watch Benjamin Encz’s fantastic talk on Redux on iOS. Suffice to say — MVC becomes spaghetti in large software.
Our upcoming Mail Pilot 3 release is built exclusively using Redux in Swift. It was clear to me, since learning the ins and outs of Redux, that this paradigm was exactly what our email client needed. So in our first Engineering Notes column, I wanted to share with you what we’ve learned about using Redux in Swift.
In this article, I’m doing to discuss:
- How Redux works
- How to build your own (simple) implementation of Redux in Swift (with the code you’ll need!) to deepen your understanding of the paradigm. In production, use open source libraries like the fantastic ReSwift.
- The advantages of Redux, and why you would want to use it in your own projects
I’m going to go in that order, so feel free to skip around if you want to jump through it in a different order.
How Redux Works
First, what is Redux? It is a unidirectional state management paradigm. It is surprisingly simple, and therefore even more surprisingly powerful.
State does not exist anywhere but one single store. Views and view controllers do not manage their own state — they only ask the store to update (via actions), and they deterministically update their interface whenever state has changed to visually represent it.
Actions are just data — in Swift, Structs that usually have no functions. When they are dispatched to the store, the reducer is called to take in the prior state and the action, and it spits out a new state. The store then replaces its state with the new state and updates subscribers — views / view controllers.
The reducer is a deterministic, pure function — it will always return the same thing when given the same result. It contains no asynchronous or external calls, and has no side effects. As much of the application’s business logic as possible goes into the highly testable reducers.
Store — contains the state, receives actions, uses the Reducer to replace the state, and fires off the new state to subscribed views. Sounds like a lot, but since it’s unidirectional, and only on a single thread, things are extremely predictable and easy to reason about.
State — a constant struct that defines and contains the entire application’s state in memory. In large applications, it’s usually made up of sub-states — so you could have one struct for
InterfaceState, and another for
MessagesState, for example. One single super-state would be composed of those sup-states.
Reducer — a pure function that is given old state and an action, and it returns a new state struct that’ll replace the old one in the store. The reducer is literally just a pure function, making unit testing a breeze. It can be broken down into sub-reducers to tidy code per sub-state.
Actions — simple structs that the Reducer uses to update state. They are, fundamentally, requests to update state.
Action Creators — you may be wondering how async calls are handled. That’s where action creators come in. They are dispatched and can run asynchronous code that eventually may dispatch an action.
Subscribers/Views — views subscribe to the store to receive new state when it’s updated. Views simply visualize state exactly as it’s received from the store. They don’t manage their own state in any capacity.
Build & use your own Redux framework in Swift
The best way to understand how it works is to build your own Redux framework. Eventually in this series, we’ll use an open source one with lots of bells and whistles, but for now, building and using our own shows us how simple and how powerful this paradigm is.
Let’s build a dead simple implementation of Redux in Swift so we can see how it all works. Avoid copying & pasting; reading and writing each line (it’s only 32 lines) will force you to consider the reasoning and implications of everything being done.
Start a new project in Xcode, create a file called
SimpleRedux.swift, and type out these 32 lines:
This is the framework. It’s not specific to any one program, so don’t modify it with app-specific logic.
What’s happening in here?
- We’ve stubbed out protocols for
State. Neither have any requirements.
- We’ve defined our
Reduceras a pure function that receives an action and the old state, and returns the new state.
- We’ve defined a
StoreSubscriberprotocol for our view controllers to use to receive updates when state has changed.
- We’ve created a
Storeclass that sits at the center of this whole thing. When we use it to dispatch our actions, it will use our pure reducer function to create the new state object, replacing the old one and notifying subscribers.
Now, let’s use our Redux framework. In your new project, let’s add our own application’s implmentation:
In here, we’re defining app-specific implementation of the Redux framework:
- We’ve instantiated
Storewith our own reducer function
- We’ve implemented our own state struct called
- We’ve defined some actions that have one parameter each
- Finally, we’ve implemented our own pure reducer function that, when passed any of the actions we’ve defined, will create a new state object with the expected changes.
Once this is in your project, you’ve got everything you need to take on this challenge:
Use the view to show the current counter value with buttons to increase and decrease that value.
- When you want to fire off an action, you call
store.dispatch(AnAction(itsParams: "Hello, World!"))
- In order to display a value from state (don’t forget — you have to display what’s in state in your view; you should not be updating your view any other way — it’s a simple deterministic rendering of state), your
ViewControllershould subscribe to the store for updates using
store.subscribe()and adhere to the
Answers: Once you’ve got it working, or if you get stuck, check your result against the answer here.
The advantages of Redux
Of course this example is insultingly simple, but let’s talk about what we’ve done here, because it’s bigger than simply meets the eye. Because Redux’s power comes from its simplicity, it can be easy to overlook its genius and impact.
You’ve now got a program that allows the user to hit some buttons. When they do, your
ViewController fires off an
Action to your
Store . The
Store uses your pure
Reducer function to compute a new
State, replacing its current state (not modifying it), and sending that new
State to your
StoreSubscribers, which is your
ViewController. When your
ViewController receives updated
State, it completely recalculates every part of its display that needs to change based on state, using the new
So what’s so damn powerful? Think about all of these pieces:
Reducer is a pure function. There’s very little complexity to maintaining and testing it to ensure it’ll always do the right thing. Business logic is not handled by view controllers — ever. It’s in the highly testable and maintainable reducer function.
Same with your view — it’s entirely deterministic; the “pure function” version of an interface. It is, essentially, recalculated and redrawn every time state changes based solely on that new state. The power here is astounding.
Let’s say you wanted to use your counter value in another part of the interface as well as your
ViewController — say you were going to display that many images. Instead of having to pipe a call from one view controller to another, both of them simply observe the
counter value in
AppState. That’s it — no protocols and delegates and callbacks or the like. It’s dead simple, and far less likely to break when simple changes are made in the future.
Or let’s say months from now during development, you realize you might need to trigger an increase in the counter value another way, in addition to the button. Maybe it’s coming from another view controller, or a menu item, or a keyboard shortcut. Instead of wiring that other view controller, menu item, or keyboard shortcut to
ViewController, you now simply dispatch an
IncreaseAction from wherever you want to your
Store and you’ll know, since any views that are based on the
counter value will receive the new state, that everything will work as you expect, without having to write any additional code. All the right views will update on their own. You don’t have to worry about a bunch of edge cases or wiring up a bunch of view controllers to each other.
Or let’s say you needed to debug an issue. It’d be nearly effortless to save a stack of the actions that were fired off and their resulting states, or to print out the entire application’s state to the debug console. Debugging becomes a breeze; nothing is a surprise anymore.
In fact, the open source ReSwift project has a module that allows for two amazing things:
- Hot reloading — since the entire application’s state is stored in a single, constant data structure, you can load a prior state into your application, and the entire thing will update appropriately. Hot reloading means when you quit your app, then launch it again later, it’ll be able to load everything — from the interface state to your in-memory models, and therefore all of your interfaces — back just the way they were.
- Time travel — literally scrubbing backwards in time, hot loading how the state changed after each action. It’s a jaw dropper when you do it in your own application for the first time.
The power of this paradigm becomes greater and greater the longer your application uses it, the bigger your application becomes, and the more maintenance you are tasked with. It’s easy to like it when you’re first building with it, but it quickly becomes impossible to live without it once you’re months-deep in an intricate, production app that has to stand up to many different users and use cases.
I think one of the best perks is you can easily visualize how your program is working in your head, no matter how complex it grows. And you can easily visualize how new functionality should work — instead of every ‘module’ of your program working its own way, they all work with one unidirectional flow of state, keeping things very simple to reason about and engineer. Add on top of that the action stack and state tree that you can capture and view to debug, and the whole thing is wildly easy to reason about, even for complex applications. You can even record and replay a series of actions in order to reproduce a bug when you can’t figure out how to reproduce it manually.
In his talk, Benjamin puts it this way:
You also get a clearer, declarative API with this structure. Your actions describe every single way the state of the application can be changed. If you follow the pattern well, there is no function that performs any side effect, there’s no other place where someone could inject a piece of code that is hard to track down — there’s just these list of actions. It doesn’t matter how many there are and they clearly describe which mutations can happen.
If you want to see how these actions are responded to, you go into exactly one place and that are the reducers. That way you end up with predictable and explicit state. If I were to ask you what is the state of your app at this moment in time, you would have no idea. With this architecture, you actually have one data structure that you can print out to the console and you can see exactly what the current application state is.
Furthermore, with explicit application state and actions that describe the changes, our program now has a shape. Now, if I bring on a new developer on my team and they want to see what are the existing features, they take a look at the application state, the reducers, and the actions, and they don’t have to dive into hundreds of view controllers to see what is going on.
He discusses the many more benefits of Redux towards the end of his talk, which I highly recommend.
In our Engineering Notes on Mail Pilot 3, we’re going to continue to discuss how Redux in Swift (and Cocoa) works, so subscribe here to get the next article.
If you want to keep diving into Redux, the next things you’ll want to check out are:
- Action Creators (as well as Sagas and Loops if you want to really dive in deep) which allow for asynchronous method calls
- Creating a project with a Store and a Reducer composed of many sub-stores and sub-reducers
- Time travel & Hot reloading