There is some buzz going around about using Redux-Style application architecture for iOS apps. The pattern is definitely worth a closer look and has some big advantages, but how well does it work in practice?
Many introductions to ReSwift are already out there, however, I haven’t seen great examples of how to apply ReSwift in an actual shippable application.
In this article, I attempt to demonstrate how an almost “real-world” application can be build using ReSwift while keeping everything testable and integrated well with system APIs and network services.
Introduction to ReSwift
As mentioned, there is already an abundancy of information introducing ReSwift out there — I’m trying to keep it short.
A unidirectional data flow means not having your state of the app not kept in many different places. Instead, there is storage component keeping all state at a central location. Views react to changes of this state instead of handling it internally. Actions are the only way to trigger a state change. Actions don’t perform the state change by them self, rather think of them more like a message that signals that something shall change.. These “messages” are issued against a Store object which uses Reducers that perform the actual state change. There is also Middleware to handle side effect, which will get introduced later on.
Some big benefits to this approach:
- Business logic is kept separate from your UI
- Reducers are pure-function — great for testing
- Simple logic inside the views — just react to state changes and send actions to the store
- State can be easily shared between multiple views (or other components)
- Overall much cleaner approach compared to standard MVC
Part 0: Getting Started
There is a Counter-Example that exposes the architecture described above in a very basic form. In the example, everything looks great and clean, but usual application development is a lot more complex. There are asynchronous operations (e.g. networking) and functionality provided by the OS or third-party frameworks that need to be integrated.
The rest of this article describes how to set up a fully tested (except UI) sample application integrating CoreLocation and a network service from Google that requires the user’s location. Here is a short list of requirements:
- Handle location permissions and changes to it.
- Get the users locations and update on user movement.
- Display restaurants nearby after receiving the user’s location.
You can check out all the source code on GitHub. For each step there is at least one commit. Links to the corresponding source code will be provided for each step.
As a starting point, there is already an Xcode project with ReSwift as a dependency. Additionally, a simple network abstraction around the Google Places API with corresponding model classes for retrieving places (better restaurant) information is already there.
The first step towards displaying restaurants is setting up the required ReSwift objects: Store, Reducer and State.
The state is kept extremely basic for now. After places have been fetched successfully, the optional places object will hold them. During loading or error the value will be
nil. This, of course, will be improved later on.
If you want to see the example in action or check all the details have a look at this commit.
Part 1: Networking
The first step towards displaying restatuants to the user is performing a network request. Fake coordinates will be used for now since fetching the users geo-location will be handled later. Fetching takes place on
viewDidAppear in the responsible ViewController. In ReSwift speak, this means dispatching a fetch places action.
Actions are defined as an empty protocol. Any type can just conform to it and act as an Action. So the following would work:
This is nice but won’t actually perform a network request. Thankfully there is another way to define/dispatch an action. The store object allows supplying an action by passing a function (or closure) that returns an optional action. Those functions are called action creators. Here is the code snippet for it:
fetchPlaces function can be
dispatched using the store that will internally perform the network request to fetch places. Since this happens asynchronously
nil (no action at all) is returned. Once the network request finishes successfully, another action is dispatched for updating the store with the received places.
This works just fine and you can find many examples that do networking in ReSwift applications like this, but this approach has a major issue:
As described earlier, an action should be viewed as a message that triggers a state change performed by a reducer. This is not the case here anymore. Instead, the action itself performs a network call as a side effect and dispatches another action on the response of the network call. The action executes now as a part of the business logic instead of being just a piece of information.
Also, note that testing such an action is not as simple as it could be. There is no easy way to inject/replace the network service while testing.
If you want to see the snippet above in action, have a look at this commit. There is also a simple table view displaying the data when the state is updated.
Middleware To The Rescue
How to actually do proper asynchronous operations based on actions is not really defined in ReSwift — for example, read this thread. There are multiple ways to do so. This article will show you an approach using middleware which is especially nice since it separates the UI or action dispatching parts of your application completely from the parts performing network operations.
I found this Example to be a great inspiration for this approach.
In ReSwift, middleware is just a function that is executed by the store before the reducer is hit. It is not allowed to alter the state, but can execute asynchronously while changing the current action or dispatch additional actions. Also, note that you can have multiple middleware functions. Common use cases for middleware are logging, caching and networking.
Middleware is defined as follows:
A middleware function gets supplied with a dispatch function for emitting additional actions and a closure that will return the current state. The middleware function is responsible for returning a
DispatchFunction having another
DispatchFunction as a parameter. So a function within a function. While the actual middleware code will live inside the nested function, the
DispatchFunction supplied as a parameter is signaled with an action once the middleware is done.
Here is an example that just logs the action dispatched to the console that hopefully makes it easier to understand how middleware is working.
I’m not sure why the API for middleware is defined that way — I think this is much more complicated than it should be. I hope there will be a better way to declare middleware in the future. (There is a PR open for improving middleware. Hope it will change things for the better.)
Moving forward in this application there is a little wrapper making it somewhat easier to create middleware. (This is still far from being a perfect API for declaring middleware. But it makes defining middleware, somewhat more convenient in the scope of the sample application.)
Have a look here to see how I implemented this and how
MiddlewareContext can be used.
Using Middleware for Networking
Instead of calling a network service directly in the action, a fetch places action will be dispatched by the view controller and intercepted by the middleware. The middleware now is responsible for performing networking as a side effect. The next paragraphs describe how network requests can be integrated using middleware.
Update Actions — as you can see in the graphic above two actions are required now for fetching and setting places. (Setting here means updating the state.)
Write the middleware — the new middleware function should do something like this:
But this again would have the downside of not being able to test the code easily. There still is a way missing to inject the network service. Fortunately, middleware just needs to be created once at app start. So it is possible to write a function that will create our middleware using the dependency as a parameter.
In the implementation, a
PlacesServing is passed into the function. The function itself returns the actual middleware that can reference the injected dependency and perform the work. This looks like the following:
And this is how it is passed to the store:
The main advantage here, of course, is that the middleware is now easily testable. Also, the definition and the dispatching of actions is now completely separate from the logic responsible for executing the network request.
Have a look at commit f97e4a7 if you want to see this implication and the related tests in action.
Clean Project Setup and State Improvements
There are still a few unsound pieces in the app that should get cleaned up before moving on to integrating a system provided service into the app.
Currently, the app store is defined as a global variable in
AppStore.swift. This is a code smell since it makes it easy to just reference the global variable in some piece of the code without thinking about dependency injection or reusability when things get moved around at a later state of the project.
One possible solution, shown here, is to set up the appStore inside the
AppDelegate. This works great as long as the setup code is kept small. (A different solution should be found if there is a lot of setup code.) From now on the AppStore file has just a single line for the
AppState while the
AppDelegate now looks like this:
The only thing left is to now inject the now private
appStore into the view controller:
Loading State for Places
So far, the app state just has an optional
PlacesSearchResult, but what if the UI should display some sort of loading indication or an error when fetching restaurants failed? The solution is sort of simple, but requires some changes in the existing code. For each state, there is a case of an enum called
initialindicating that no loading ever took place
You can find the full changes in commit 70d31f4. Here are the most important parts:
This concludes the work on adding networking to this application. When using the application now you should be able to already see some restaurants at the “fake” user location.
Part 3: CoreLocation
After having working network operations and a well working ReSwift setup ,it is time to start looking into integrating a system service — CoreLocation — into the sample application.
Keeping Track of State
Let’s get started by looking into how authorization state and the current user location can be represented by the app state. There is a need for an object that retrieves data from CoreLocation (
CLLocationManager) and emits actions. For the lack of a better name,
LocationEmitter will be used for this object. Two more actions are also required: one for setting the new location and one for updating the authorization state. Writing the code for this is relatively straightforward. Here is a snippet for it:
LocationEmitter gets a store and location manager object injected in the initializer so that the object is easy to test. The emitter must stay the delegate of the location manager and must be kept alive through the whole lifecycle of the app so that it can update the store when delegate calls are triggered. For these reasons the emitter is stored and set up in the app delegate:
Again, omitted are the changes required to the app state, reducers and tests here. If you want to dig into the details have a look at this commit.
User Opt-In and UI
Instead of requesting user permission for accessing GPS at app start, this app should ask the user at a more convenient time maybe after explaining why the app needs the user’s location. The UI will “simulate” this by presenting a button on the main UI when a user has not yet given permission. There will also be an error view when the user denied the permission request. Both views are shown and hidden depending on the
authorizationStatus of the app state.
To request authorization, the corresponding method of
CLLocationManagercould be just called inside the
IBAction of the button. However this would not be a good approach since the UI should not perform such a side effect. Just like before with networking, a good place for this is middleware where the view can just dispatch another action.
Again, like before, there is a commit with all the changes and test.
Fetch Places based on Significant Location Change
Now is the time to put everything together. The first step is to call
CLLocationManageronce the user granted permissions or when the app becomes active. This should also be a side effect, so another middleware is the best solution here. A new action must also be dispatched inside the
Note that the information about the application becoming active will be completely ignored by the reducer. In addition to starting significant location updates, there is also a call to request the current location since Core Location will only call the delegate method once the user moved.
Last, but not least, the app should fetch new places once the user location changes. The action for this already exists and is currently dispatched inside
viewDidAppear of the view controller. That must go away. Instead, the fetch places action has to be dispatched once the application state has a new location object set.
Unlike before, no side effects need to be executed and therefore no middleware is required. Instead, a
Location Observer is required. On the outside, it behaves almost like some UI component, but instead of sending actions based on user input, it sends actions when the state has a specific change.
The snippet above shows the implementation of observing location changes. Again integration and tests are omitted here. You can see the full implementation here: 65bef0d
This finishes the work at the sample application. You should be able to build and run the application. After granting location permission, you should be able to see restaurants around you and while moving those should update automatically.
In this article I went further into details of writing an application with ReSwift than most other articles out there. When handling side effects I showed the heavy use of middleware. This goes somewhat against the sample projects provided (e.g. GitHubBrowserExample), but separates actions and network logic and improves testability.
The good news is that ReSwift is flexible enough to allow adjusting app architecture to fit your needs. On the other hand, I’m not very happy that the standard patterns don’t separate side effects from actions and don’t provide a good strategy testing. Also, a bit more guidance and introduction from project side would be useful.
It tried showing as much as possible of actual app development, but still there is much left uncovered. Big things like routing, good integration with UIKit classes (UITableView, UICollectionView, …), splitting up big applications into modules and making the presentation layer independent of system and external APIs, are missing in the example.
Anyway, I hope I showed some useful patterns in the article. If you liked what you have seen here, please also have a look at the source code. Overall, I think the sample app exposes a pretty sound architecture for a small to medium sized application especially, with a high test-coverage of 80%.
Thank you for reading!