Build a Foursquare clone iOS app — Part 6: State management

Fabio Hiroki
iOS App Development
4 min readApr 16, 2018

Content

Introduction

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.

ReSwift concept

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 UIViewController .
  • Action: structs that contains the payload data necessary for a state change. Example: a struct named SetPlacesAction containing an public array of locations.
  • Reducer: methods that receives two parameters, an Action and a State , and return a new State . All state changes should be centralized on reducers.

Show me the code

The code below defines the "State":

State representation

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

The Equatable extensions are necessary for unit tests only.

The Actions structs are represented below:

The only piece missing now is the Reducer :

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 State .

There are a few details I'd like to point in the Reducer code:

  • Specifically in our application the new state doesn't depend on the previous state. So the reduce method only uses the action parameter.
  • When you call the subscribe method of an Observable , it's created a circular memory reference between the Observable and the Disposable (the return type of the subscribe method). 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 reduce method 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:

State management tests

Conclusion

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!

--

--