Many iOS developers are familiar with the problem of the “Massive View Controller” - a component that clearly oversteps its role within the Model-View-Controller architecture. The community has come up with different approaches to avoid it, including the usage of alternative architectures. In this talk, Benjamin Encz explores using the popular architecture of Redux and Flux in Swift. Flux enforces a unidirectional data flow that reduces unnecessary code complexity, and a Swift implementation of that concept leads to fascinating results.
Introduction (0:00)
Hi, my name is Benjamin Encz and I’m an engineer at PlanGrid in San Francisco. I want to discuss unidirectional data flow in Swift which is an architecture that I’ve been working with in the last couple of weeks. Along with that, I also want to talk about the end of model-view-controller. We will likely use MVC in the future, but I’ve seen a lot of problems in existing, growing codebases over the last couple of years, likely caused by Apple’s vision of model-view-controller. I hope that even if you’re an MVC fan, I can offer you some insight into alternatives that we can maybe use in iOS.
The Origin of MVC (0:54)
I want to start and go back in time to the inception of MVC, in 1979. Back then, the folks there were pioneering what graphical user interfaces would look like. Before that people were working with computers on terminals, and then they invented what we now know as the typical graphical user interface.
As part of the work they were doing there, they were not only solving the problem of building an operating system, but also of teaching developers how to build applications for the system. One of the problems was how do we present information on the screen so that a user can intuitively interact with it. A Norwegian researcher Trygve Reenskaug visited Xerox PARC for a year, and invented MVC to solve the problem of making data in the computer accessible to humans. He was actually tackling that problem from a user perspective, not from a developer perspective.
It was not intended to be an architecture, but, instead to give developers insight into how they can develop user interfaces that are intuitive. The core part of it was that there might be different ways that users want to interact with the same model. As an architecture, MVC is vaguely defined. However, for us as Cocoa developers, it’s most important what Apple thinks MVC is, and how they incorporate it into their frameworks with view controllers at their core. The view controller communicates with the view and the model, taking events from the view, passing to the model, waiting for a response, and updating the views. As a result, it’s easy for view controllers to contain most of the app’s logic.
Problems with MVC (3:47)
MVC doesn’t address many issues of modern iOS and Mac app development such as handling complex UI, persistence, networking, and navigation. MVC is not an holistic application architecture, even though we treat it sometimes as that. I think it’s a model that you use for your view layer and it’s definitely a good idea to separate the view from the model, but it’s not a good way to structure an entire application.
Two concrete problems I identified are that view controllers server as micromanagers of the app, and complex state management.
Problem 1: View controllers as micromanagers (4:38)
The view controller sits at the center of your application and can be seen as a micro-manager that has to communicate with every single part of your app. For beginners it can be really hard to draw a line between what your controller should do and what should be in the model or the view layer. However, even if you get good at weeding out the code that is unnecessary out of your view controllers, the view controllers still state a part that have to tie together all these different calls. They will have to talk to your views layer, they will have to wait for a response, they have to know what that response means and they also have to know which views care about that response.
If you got really good at designing expressive APIs, then you might have a method like this in your view controller:
func userLoggedInWithUsername(username: String, password: String) {
apiClient.authenticateUser(username, password: password) { response, error in
if (error == nil) {
let nextViewController = ...
navigationController.pushViewController(nextViewController)
} else {
showErrorMessage(error)
}
}
}
This here would be a callback that gets called from the view, as soon as the user enters a user name and a password. In response, you want to log-in a user. You would take the user name and password and then you would talk to something in your model layer like an API client. You call authenticateUser()
, passing in username and password, and we sit inside of your controller and we wait for the callback block. That is typically how you would implement it when you don’t think about it further.
This means your controller has to know what are the possible responses of this API call and how does it affect each view. We have to do error handling here, and we have to present and hide different views based on these events. That means we have a view controller that touches many different domains in your app, and even if you separate out the code, you’re still going to have a lot of glue code in your controllers.
Problem 2: Where is state? (6:19)
The second, even bigger problem is state management. When you think about where we have state in our applications, I think we have more or less, these four categories:
- Currently Active Views
- Currently Active View Controllers
- Database
- Singletons?
The first two are states that we have on the view stack. Then we have some state in the database where your core logic lives. Sometimes you also have state in some outside singletons, which is a side effect of the lack of opportunities for state management within MVC.
With this structure, we run into a problem I get asked a lot of “how do I pass information between view controllers?” Intermediate data you don’t want to write into your database, such as signup details before the flow is complete. Accumulating intermediate state like this leads to singleton abuse and other problems. These arise from multiple copies of the state and if you allow your app to navigate back-and-forth, then you have to make sure that the latest state in the latest view controller is available in other view controllers as well. Maintaining the state across all of your controllers that are involved through delegation or callbacks becomes messy.
MVC Problem Summary (8:22)
The first one is that view controllers need to know business logic details. They have to really know what are the different response types for all the different operations I can have in my app and how does it affect the views. View controllers also need to manage a significant amount of state – almost anything that is not in the database ends up in view controllers. That is a highly complex view state, but also any kind of intermediate state that you don’t want to store but you want to drag along until you have something that you can put into the database or that you can send to a server. That state of communication that leads to a lot of additional code in view controllers.
State management and state propagation happens ad-hoc, so you have to decide between callback blocks, delegation, or some other method to pass state between view controllers. If you’ve built iOS apps before, you probably have seen effects where you have outdated state when you go back from navigation stack, an effect of incorrectly handled state-management.
Lastly, it is really difficult to build a mental model of how an application works with these large view controllers. When I went to learn about a web application and I’m entirely new to a codebase, I can check the API endpoints and learn what operations the web application supports, what are the routes, which methods are called in response to which HTTP requests. Inside of an iOS application, all of that is hidden inside of view controllers and it’s really hard to get a high-level understanding of the features. With all that said, what is now turned code into, and a couple of weeks ago, a friend of mine who does web development, introduced me to a framework called Redux.
Meet Redux (9:56)
Redux is an alternative or a variation of the flux framework that was developed at Facebook, and that Facebook now uses for most of their web applications. The main idea is that information always only flows in one single direction. We don’t have communication between individual view controllers, we don’t have individual delegator callback blocks. Information flow is structured and set in one very specific way.
There are a couple new, important concepts to learn with redux. The first one is that state is actually a data structure. You have only one data structure that defines the entire application state including the UI state and any model state that you use in your app. This state is stored inside of the store, then you have views and other subscribers that will get notified every single time that state updates into the entire app. Thus, whenever a state change happens the view will get the latest state and it can update it’s appearance based on that state. The state can’t be changed anywhere along this chain except by emitting actions. The state is always entirely immutable, and the only way you can trigger a state change is sending an action to the store.
These actions describe the change that you want to perform. For example, deleting a user, adding a user, changing a user name. There are some more components that go into it: reducers and observers. Reducers are the piece of code that actually perform the state changes. When an action comes into the store, it sends it to all of these reducers and each of these reducers takes the current application state and the action and generates a new application state. These multiple reducers are responsible for one certain slice of your application state. For instance, one will take care of users, one will take care of view states, for a particular view controller, and so on.
(State, Action) -> State
These actions go into the reducer which produces a new state. That new state gets submitted to the views and to other observers. In most cases, these observers will be views, but you can actually use this pattern, for example, to implement analytics as an observer. That way you can have your analytics code as a separate component that listens to actions as opposed to inside of the view controller.
An important aspect about these reducers is that they’re pure functions, and we’ll talk about why that’s really important a little bit later on. They essentially get their current application state, they get the action, then they look at what the action should do based on its description, and calculate the new state and return it. They don’t do anything outside of this. Actions, to summarize it again, are declarative descriptions of a state change. So they’re not methods, they don’t have any code inside, they are just a message that tells the reducer what state change should be performed.
SwiftFlow: Redux in iOS by Example (13:09)
To keep it less abstract, I’m going to show you some code based on an open-source framework that I built. This framework is named SwiftFlow, bringing Redux to iOS. I want to show you the example based on a very simple example application: a counter app. A counter app allows you to increase and decrease the counter, and it has a navigation bar.
Application state (13:50)
How would you build this app with this framework? Let’s first take a look at the shape of the application state. The application state is one data structure and it always should be a struct to be immutable and unique copies of state as soon as they leave the store.
struct AppState: StateType, HasNavigationState {
var counter: Int = 0
var navigationState = NavigationState()
}
The two important components inside of the state are the business logic and the navigation state. The business logic that is really specific to this application is just a counter variable that is set to zero at the beginning. In a more complex app, you would have a ton of more stuff. Secondly, this app supports navigation, so we have a tab bar controller and I actually built a component called SwiftFlow router, which is built specifically for this framework that allows you to use navigation with this unidirectional data flow. Therefore, instead of presenting view controllers directly, you actually send out actions that change the route of the application and then the view controllers are presented in response to that. That lives inside of a separate component and that separate component offers this navigation state.
By adding this navigation state to your data structure you can now use that router that I programmed. You can read more about that in the documentation. The last thing I want to point out is that the structure conforms to two protocols, the first one is StateType
, which is just a marker protocol, and the second one is HasNavigationState
. The concept here is that the router doesn’t really know what the structure of our application state is going to look like – we could choose any state and have as many fields as we want, but a router has to know that our app supports the navigation state.
As a result, anything that this protocol does is requires us to have a variable under state, called navigationState
. The router can store it’s state inside of it, allowing us to build outside components that can inject and work together with our application state without having to know the exact shape of the application.
View controller example (15:45)
This leaves us with a simple view controller:
func newState(state: AppState) {
counterLabel.text = "\(state.counter)"
}
@IBAction func increaseButtonTapped(sender: UIButton) {
mainStore.dispatch(
Action(CounterActionIncrease)
)
}
@IBAction func decreaseButtonTapped(sender: UIButton) {
mainStore.dispatch(
Action(CounterActionDecrease)
)
}
The first line is the method that gets called when you subscribe to the store. As soon as you subscribe to the store you have to implement this new state method and it is a callback that gets called every single time the application state updates. As an argument you get the latest application state and here it’s your responsibility to update the view to reflect the changes that happened. In this very simple app, all we do is set the counter label text to the latest value.
There are two cases in which we want to change the state of the application, that is when we hit the increase or decrease button and in these cases we dispatch an action. We do that by referencing the store, calling the dispatch method and then passing an action inside of it. That action, again, is just a description of the state change. Those actions all get sent to a store, and from there it goes to a reducer.
Reducer example (16:40)
The reducer is very, very simple:
struct CounterReducer: Reducer {
func handleAction(state: AppState, action: Action) -> AppState {
var state = state
switch action.type {
case CounterActionIncrease:
state.counter += 1
case CounterActionDecrease:
state.counter -= 1
default:
break
}
return state
}
}
It implements a protocol Reducer
and that requires it to have one function, called handleAction()
. Inside of handleAction()
, it gets the latest application state and the action that should be handled. Here you will switch over to action type to see which kind of action you need to perform and then in response you will change the state accordingly. If you have a more complex reducer you won’t have the method or the state change directly inside of the case statement, you’d rather call out to a utility method but the idea is pretty much the same. If we want to increase, then we increase the counter by one, if we want to decrease, we decrease the counter by one. Then we return the new state to the application.
Redux time traveling demo (17:26)
All of this is not too exciting yet, but it has a very, very cool side-effect. Using this architecture, if you actually have only one source of truth for your entire application that is only modified from this one interface, then you can travel back in time through all the different application states that you’ve had and your UI will actually look exactly the same as it looked initially. You can imagine that state and these state transitions is similar to a state machine.
As part of the framework I have a separate store that you can add to your app and that store records the actions, and that supports the time-travel actions. That store injects a little slider that allows me to go back in time through the navigation stack. I haven’t animated the transitions, but that is possible as well. This make it remarkably easy to implement state restoration, and also find crashes easily as the state that caused the app to crash will be reproduced every time you build.
Applying to Real-World Apps (20:31)
The time travel stuff is neat, but simpler than most real-world applications. There are a few issues that I’d like to point out. The first one is that you can’t really store the entire app state in just one data structure. It seems a little bit absurd to store all the information we need within one struct. To tame the state object, there are two different strategies.
The first one is that you can divide the state into different sub-states. I already pointed it out with the navigation state, which is itself a struct that is injected into our app state. But it can have its own structure, so the app state can be a high-level compilation of a lot of sub-states. The state structure of a complex app would then amount to maybe 50 lines of code, which is still understandable.
The second aspect is that inside of the state, you really only store information that cannot be derived. For example, for the router we don’t store view controllers in there. We don’t have to serialize them and store them inside of the store, we can just store information that allows us to recreate the view controllers in exactly the same arrangement that we had before. The router can be just a unique string, similar to a URL in a browser, that describes the sequence of view controllers. Then our outside systems can take this latest state and can reproduce the side effects that comes from it. Thus, anything that can be derived can be recreated without directly putting it in the store. For instance, to restore an image, all we need is a URL to download it or to get it from a cache drawer from our file system. That reduces a lot of the information that has to go in there.
Handling Async Gracefully (22:17)
To handle async network requests, we have the concept of action-creators. In a simple case, like the counter example, you’re going to dispatch an action as soon as you want to perform a state change. However, sometimes the state change will be deferred such as with network requests. An action-creator is a method that performs some kind of operation, async or not, and at the end, might or might not, dispatch an action. What we can do is we perform that network request and we wait until we get a response back, and then we send that as an action to the store.
Taking these insights, I actually starting working on a real-world application that uses the Twitter API to search, open sourced here. I can use the application state to go back in my search history like a time machine, restoring the text I searched for, the results, and the navigation stack. This demonstrates that network requests are possible through routing, with the right states such as images working efficiently.
Challenges of Redux in iOS (23:52)
There are still some challenges, however. The first one is that sometimes UIKit makes it tough to cooperate with this approach. For example, the router in the example wants that every time you want to change which view controller is presented, an action does that, not an immediate presentation of a view controller. That can be a problem when you use components such as tab bar view controller. A tab bar view controller immediately changes the active view controller upon tap without without dispatching an action. What I had to do there is become the delegate of the tab bar view controller, tell it that it should not present a view controller, then dispatch an action and then wait for the action to actually trigger the presentation. There are some points at which we have to work a bit against the framework, but I think all that I’ve done so far is pretty acceptable.
The second step is encoding and decoding. This is only necessary if you want to persist your state to disk and want to have that hot reloading, where you can go back to the latest state as soon as you run the app again. To do that, every action has to be serializable to some data structure that you can store on disk. I chose JSON for that and that means, internally, these actions are actually untyped. They have a type string and they have a JSON payload that is basically an arbitrary collection of JSON elements. I had a little bit of a struggle building a nice API around there, but, I think I got to a point where it works pretty well. Now you can use typed actions on the outside, they are then converted to these plain actions before they store to disk.
Unfortunately, at the moment, you still have to write that conversion code yourself. I’m hoping to get some code generation going for that to avoid boilerplate code and handwriting serialization and deserialization.
Lastly, we need to restrict access to global state. This is because in this architecture, every component that listens to the store will get the entire latest application state. One way to do that is adding protocols to the state and then only accessing the state through this one specific protocol that only exposes a very small set of the entire state. That is what the router does, for example. The router has navigation state protocol and it looks at the app state through this protocol and it can only see that one entry in the state. It’s totally unaware of all the other states that you have accumulated.
The best strategy here is to have all the different sub-states define a protocol that only allows these components to see only their specific sub-state. That will help you avoiding building dependencies on states in other view controllers, or other global states. I also documented that in the documentation.
Why Swift Flow? (26:35)
In summary, here is why I think this framework is worth trying out. First, there’s a good separation of concerns, which is one of the important things about building software that scales. This is the case because now views have a clear role in this architecture. Firstly, they get a state and they have to adopt themselves to represent the latest state on screen. It’s done through a simple interface – the newState()
method – and that’s the only place where you change the view state based on the latest application state. No more callbacks, no more state variables that you have to keep track of over time.
Additionally, view controllers don’t have to listen for responses, they don’t have to know which type of errors could come back from your API, they fire off an action and then they wait until they get the latest state, and they forget about the action until they get that response. As a result, instead of your view controller sitting there and waiting for a callback, they fire an action and then they go silent until there’s a new state and then they represent it. If you would want to represent an error, you would inject it into the state object and then you would have one simple line in your newState()
function that says, if error, present view. The updating of the view and taking action is thus well separated.
We also have a decoupling of intent and implementation. Actions are really just intents, they don’t have any code. For example, in an action, we claim that we want to delete a user or update a user; there’s no code that goes along with it and that is pretty powerful. It allows us to extend what that intent means after the fact. Without having to go in all the places where this method is called and change the implementation, we can now have different behavior and different reducers that respond to the same action and enhance our application based on that.
For example, if you want to add analytics or you want to add a log entry for every entry that has been deleted from a database, then you could add another reducer to your app that just responds to all deletion actions. You could add this feature without the entire application being aware of it or modifying other, existing code.
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.
Lastly, I think this one is also important, we get state propagation for free. We don’t have to think about callbacks, delegates, or intermediate copies of state that need to be updated manually. We follow this one, very straightforward pattern of how information flows through the system without unexpected behavior.
Credits (30:26)
I would like to thank Gerald Monaco (@devknoll), a former coworker of mine who introduced me to Redux. I would also like to thank Dan Abramov (@dan_abramov), who implemented Redux, which inspired all the code of SwiftFlow. Lastly, thanks to Jake Craige (@jakecraige) from Thoughtbot for giving me feedback on this implementation.
Q&A (30:47)
Q: We used a similar architecture in an Android app and struggled with the animation policy. What are your thoughts on that?
Benji: I implemented animations using the router by blocking and waiting until a completion block gets called. I hand in a completion block into that whole routing system and then at one point, you present a view and then when the animation’s completed and call the completion block. Before that, no other actions on the router are propagated.
I queued up animations as a backlog of actions. If you fire five animated actions, then it’s just going to take 10 seconds till they load up and any other actions will be added to the end of the queue, so it just will take a time until the animation state, basically, catches up to the actions that you fed in. Animation state lags behind the actual internal application state.
Q: What are the performance implications of getting state updates for the entire view controller hierarchy when small changes occur?
Benji: For now, I ignored most performance implications for now, which I believe to be realistic for a lot of apps that are on the App Store. The amount of state changes that actually happen is typically relatively small. Also, view controllers unsubscribe and subscribe to state updates depending on whether they are on the screen or not, so at any given time you will only have your navigation stack of maybe three or four view controllers that are actually listening. I decided to try out this architecture cleanly first, and then profile it to optimize its performance.
Q: ReactJS uses multiple stores that interact with their own components. What are the merits of the Redux implementation with one big store that exposes a facade through protocols instead?
Benji: Redux is basically an answer to flux with the goal of reducing some of the complexities that comes with multiple stores. Many developers realized that multiple stores lead to dependencies between stores and we, again, have cascading updates, which is one of the things that people try to avoid. In Flux applications you have situations where you have three or four different stores that are waiting on different actions to happen and at some point you’re surprised that these stores are blocking longer than expected, complicating your understanding of the information flow. The idea for Redux was put it all in one store and simplify the information flow for this system.
Q: Would you encounter the same problem with multiple reducers depending on each other?
Benji: Reducers can’t depend on anything; they always perform synchronously. All the asynchronous action happens outside of the system, so the reducers will always run through in a couple of milliseconds and there’s no dependency or waiting.
Receive news and updates from Realm straight to your inbox