Build a Foursquare clone iOS app — Part 6: State management
- Part 1: Introduction and setup
- Part 2: Location data and managing dependencies
- Part 3: Continuous Integration
- Part 4: Streaming location
- Part 5: Network layer
- Part 6: State management
So far we have created a reliable source of data for our app, and we now we will work towards creating a communication between data and view.
Application architecture basics
The MVC architecture proposed by Apple official documentation is the most intuitive for new developers. This architecture is sufficient for simple application and small teams, but the tendency when following MVC is to centralize all the code into
UIViewController classes, which turns your application hard to understand and susceptible to bugs.
What makes a good architecture
First of all I want to make clear this is a very subjective concept, influenced by factors like: complexity of the features, team size, definition and restrictions of quality, level of reusability of components, technical skills of the developers, and also the developer experience with the architecture.
Personally, I had prioritized 3 basic principles when defining the architecture of an app:
- Component responsibility: the project contains predefined roles, avoiding massive classes and it’s relatively easy to add new features.
- Simplicity: a new developer reading the code at first time, should be able to understand the basic principles without much difficulty. It should also be able to understand the patterns defined and start developing new features or fixing bugs in short time.
- Testability: as the name says, the code should be easier to unit test.
A Better Approach
The solution I will propose here is not supposed to be considered a silver bullet, but it’s definetely better than the common MVC and relatively easy to learn.
By choosing to use ReSwift I’ve obtained an architecture and also a data flow pattern.
The components of ReSwift are:
- Store: a global variable that represents the current state of your application. In our case it should contain an array of locations and the state of the data fetch (loading, finished or errored).
- View: responsible for observing state changes and displaying the correct UI based on the new state. In our case it will be the main
structsthat contains the payload data necessary for a state change. Example: a struct named
SetPlacesActioncontaining an public array of locations.
- Reducer: methods that receives two parameters, an
State, and return a new
State. All state changes should be centralized on reducers.
Show me the code
The code below defines the "State":
The main struct here is the
FetchedPlacesState , which has to conforms to the
StateType protocol of ReSwift. This struct contains an enum representing all possible three states of the app:
- Loading: the default state, meaning the app is waiting for all the data to be fetched.
- Failed: we use this state to display a human readable message for the user in case any error occurs.
- Finished: contains an array of the successful fetched locations
Equatable extensions are necessary for unit tests only.
Actions structs are represented below:
The only piece missing now is the
Now that we have all the code in place, it's now clear to observe that the returned
State is a function of an
Action and the previous
There are a few details I'd like to point in the
- Specifically in our application the new state doesn't depend on the previous state. So the reduce method only uses the
- When you call the
subscribemethod of an
Observable, it's created a circular memory reference between the
Disposable(the return type of the
subscribemethod). To break the retain cycle and avoid memory leaks, you just need to call the
disposed(by:)method like I've shown. More details about dispose bags here.
- Something you probably noticed that doesn't smell so good in this code is the fact the
reducemethod is not a pure function in this case. Because of the asynchronous network request, we had to create a side effect by dispatching a new action when the request returns success or error.
Elaborating the test cases
To test our state management, I've decided to write some
Reducer tests to ensure the state transitions are done correctly:
- When dispatching the action equivalent to start fetching data, the new state of the application should be the "loading" state. We can use this information later to display a network indicator in the status bar.
- When dispatching the action representing the data was fetched successfully, the new state should contain the respective list of places so we can display it to the user (ex.: using a
UITableView) and hide the network indicator.
- If the action representing an error in the data request was dispatched, the new state should be the "failed" state. In this state we could show an error message and a "try again" button.
Translating into code, this is how it looks like:
Now with the state management implemented, we have all the data and its transitions needed to display into an user interface. The automated tests in conjunction with the continuous integration tools make our application more robust and the developers more confident to ship this code into production.
I hope you could’ve learned something and improved yourselves as developers!