iOS Architecture: A State Container based approach

Luis Recuenco
Job&Talent Engineering
16 min readOct 16, 2017

--

This article is the first in a three-part series. You can find the second one here and the third, and final one here.

A warning about architecture topics

I find it incredibly difficult to talk about architecture. Unlike other topics on computer science, architecture has a major component of subjectivity, which makes it really difficult to justify why some architectures are better than others.

There is no such concept as best architecture. There is an architecture that best fit your needs for a very specific context and problem space. A reactive architecture for instance, might be a great choice when you can describe your domain as streams of data than can be easily transformed and composed in a declarative manner to get a specific state output. A VIPER architecture might be good one for a long-term project in a mid-sized team. Otherwise, it might add unnecessary complexity and would not be a good fit. What did work for you in the past, under specific circumstances, might not work any longer in your new current condition.

It is very common to look up to what some big corporations are doing and try to mimic their way of doing things. That is not necessarily a bad idea, but in case you are not in one of those big corporations, you might not have their needs. In that regard, we have tried to be pragmatic and find an architecture solution that best solves our engineering and business problems.

The goal of this blog post is to show the architecture that we have been developing during the last months on the iOS team at Jobandtalent. It is an architecture that leverages Swift, value types, generics, sum types and some React Native experiences. It works for us and it might work for you as well.

Current state of architectures

During the last eight years, I have had the chance to use most of the common architectures patterns to develop iOS apps. As most iOS engineers, I started my career working with MVC. Then, Model View Presenter (MVP), Model View View Model (MVVM), clean architecture approaches like VIPER, or the new unidirectional architectures like Flux, Redux or Model View Intent (MVI) came along. Here’s a secret: I have seen quite bad and good code in any of those.

MVC has been attacked for the last years. As if you were condemned to failure only by the fact that you chose that architecture. The problem is not that MVC is a bad way to structure your code. The real problem is that MVC is a broad concept. You usually need more rules and constraints about how you should structure your code for it to scale nicely in the future. But even if you can write good code using MVC, it is very easy to end up having the so-called Massive View Controller.

MVP or MVVM tried to solve some of the problems that MVC had. They solved the problem about Massive View Controllers, but they created others, like the massive presenters or view models. Once more, MVP or MVVM are broad concepts. They only tell you how your data should be separated from your view and how they should communicate. VIPER, the MVP clean architecture iOS implementation, did solve some of those problems. It added quite a few more actors like interactors or routers, and there is just less uncertainty about how to structure your code.

The problem about multiple, scattered, out-of-sync outputs

The real problem about MVP or MVVM is in the view layer: you have multiple, scattered, easily out-of-sync-with-domain outputs. In MVP, we will define an output as a command that the presenter invokes on the view. In MVVM, an output will just be a property set in the implementation of the view model that will be reflected on the view using bindings.

Let’s try to use a simple example that shows the problem. The following view model will download all the recruitment processes where a candidate can apply.

As we can see, the view model has three outputs: loading, processes and errors. These outputs could also change as a result of calling other methods, or even as a response to business logic callbacks, which is specially dangerous in a multithreading environment. This will inevitably lead to conflicting states in the view.

MVVM, unlike MVP, has the big problem of invisible outputs. In MVP, you perfectly know when your view renders, as you are calling methods on the view, or the abstraction of it. In MVVM you just have some properties and you cannot be sure of what is going to trigger view updates unless you check implementation details. Some of those properties might be observed by the view, some not. That is unneeded uncertainty about how your view layer behaves.

Also, there is no way we can track the state changes of the view. We do not have traceability whatsoever about the different state changes that happened in the view model. We can’t reproduce a state history, and that is something that could be very interesting when some problem arises.

There is another important problem that has to do with the consistency of the state. Imagine there has been an error and we try to fetch the processes again. We would have loading = true and error != nil for a little while. Does that make sense? It depends, but having consistency across the different outputs of your view model can be quite hard.

Let’s introduce a new concept, the ViewState. A model snapshot that represents the current state of the view. Instead of setting different outputs in different places of the view model, let's just set a new view state snapshot each time.

As you can see, we now have perfect traceability of the state. Every state change can now be easily logged, saved to disk, or even discarded if the new state is equal to the previous one. We can even have undo and redo capabilities if needed. It is also protected against race conditions by making changes via write transactions inside a serial queue. Also, by having a view state as a value object, your view will receive a state snapshot whose only owner will be the view. No race conditions whatsoever. We have deterministic render updates. And that is big news.

Another advantage is that we have moved the outputs consistency problem to the value layer, having different methods that return the different possible exclusive view states. As a general rule, is good to move business logic to the value layer. Due to the inertness nature of value objects, business logic inside them tend to be inside pure function that can be easily tested.

Also, testability is greatly simplified, specially when using MVP. We no longer have to check if the view has received a specific message after exercising the presenter. We can simply check the correct state. Testing state equality will lead us to more robust testing, unlike testing interaction via mocks, coupling to the implementation details.

The problem about exclusive states

Once we have properly modelled our view state, it is time for the view to consume.

Some questions come to my mind when I look at that render function:

  • Should I always check state.error before state.loading?
  • Can state.processes be valid when state.loading == true? Or when in state.error != nil?
  • Should I always access state.processes after having checked state.error and state.loading?

This is where the aforementioned outputs consistency problem makes special sense. If we can be sure about the consistency, then the order really doesn’t matter. I can even forget about an else sentence there, and everything will work as expected. In any case, from an API point of view, nothing is telling me all that. We should aim to make our APIs convey the proper usage.

Sum types

Sum types are one of the most overlooked features when starting developing in Swift. As product types, sum types are a type of Algebraic Data Type, also known as tagged or disjoint unions. An example of product types in Swift are tuples. Enums with associated values are the Swift version of a sum type.

Sum types, alongside pattern matching and exhaustive switches can model exclusive states in a simple, yet powerful way. Let’s go back to ProcessListViewState and see how it might look with sum types.

Not only have we achieved exclusive state semantics, forbidding access to properties that do not belong to the current state, but, thanks to the exhaustive switching, compile will fail when adding a new extra case to ProcessListViewState that is not properly handled on the view. That is a big deal.

Most of the times, you can think of your UI as a state machine. In a lot of your screens, I am sure you always have states like loading, emptyCase, error and loaded. Each of those states have slight different concerns about the piece of state the should access. Sum types are a great way to avoid mistakes and convey, from an API point of view, which subset of the state should be consumed in each of the cases. You could even have checks about proper transitions.

Imagine that we want to know now if the processes have finished loading from a totally different screen. At the moment, we have all the state deep down in our view layer. There is no way that other views can access that information. The solution is easy: let’s move view state down to domain state and let’s turn the former in a function of the latter.

View state vs Domain state

In the last year, I have enjoyed working in some React Native projects. That is what made me change my mind and think so thoroughly about states in the first place. If I had to choose one thing over the many cool features that React Native has, I would choose the fact that your view rendering is a pure function of the state:

App = UI(state)

We usually have two kind of states: Domain State and View State. View state will usually be a function of the domain state:

App = UI(viewState(domainState))

Domain State is the most important of the two. That is the single source of truth of your app and where most of the business logic lives. It should hold the minimum amount of state possible and then derive all the rest that your views need.

Value semantics tend to be a really good fit to model the domain layer. Not only will your mutation functions be pure and easily testable, but it will also avoid the implicit coupling that reference types usually involve. Different actors of the app willing to consume a piece of state will have their own snapshot of it and they will be the only owners of them.

Here’s a typical domain model layer:

As you can see, we have two important new concepts: queries and commands.

Queries

They are responsible for computing derived state. A lot of times, state can be a complex thing to manage. It can be a huge tree with a lot of nodes and accessing a piece of it will not always be as easy as it sounds. Queries are functions that return some parts of the state for easy consumption. They are composable, as they can use other queries as well.

One important feature of queries is that they decouple the domain state shape from our views, making it feasible to refactor domain state to other different shape in the future without huge impact.

This is what is called Selectors in the React world.

Commands

The mutating functions are our commands. They represent how our state can be changed. It is where most of our business logic lives and the only place where the state is mutated.

Do not be misled by the mutating word. It is not really mutating anything in place, it will recreate the whole value layer tree. Wow!, what about performance?, you might think. Most of the times, this is cheap. But in case you have a huge data set, you might bump into performance issues due to the fact that we do not have persistent data structures in Swift yet.

This is what is called Actions in the React world.

Stores

Stores are the missing piece of the puzzle. They are domain state holders and coordinators. They communicate with other collaborators, like services to do network requests, or the persistence layer to save data. They are responsible for side effects in a way. They also make sure that the state is only mutated inside transactions in the proper serial queue. The goal is always the same: mutate the state appropriately once the specific job is done and let the world know, if interested. A typical store is like follows:

With that store in place, now the view state can be a pure function of the state.

A State Container based architecture

What is a state container then? View Models are. Stores are. A state container is simply an object that is able to hold a single state property and knows how to tell the world when it changes. They are quite smart though about mutation and propagation of the state.

  • They will only allow mutation via a transaction. Other mutations will wait for the current mutation to finish. This will avoid unpleasant race conditions.
  • They will only propagate changes when it is needed. State mutations that result in the very same state will not be broadcasted.

Stores can subscribe to other stores. View models can subscribe to other view models and stores. Views and view controllers can subscribe to a view model. Multiple views can subscribe to the very same view model if needed.

View models that subscribe to other view models is not the most common thing, but it is a handy pattern when communicating superviews with subviews. Imagine that a superview needs to know about some specific detail in the subview (if a button was pressed for instance). Even if we could move this information to a separate object (like a store) so it can be listened to, it would be way more inconvenient and cumbersome than being able to communicate via view model subscription. Superview model would simply subscribe to subview model. That’s it.

Before digging into the implementation details and how leveraging swift generics allowed us to automate most of the tasks, let’s try to sum up the main components of the architecture.

  • View/ViewController: it only knows about a view model. It renders a View State when that view model commands.
  • View Model: View State Container that subscribes to other stores and apply pure transformations DomainState -> ViewState when the domain model changes.
  • Store: Domain State Container in charge of coordinating other actors (like network services) to perform business logic and side effects. It usually sets a new state as the final result of those side effects.

A note about testing

As we mentioned before, a good architecture should be easy to test, and there is no better way to test than data in, data out testing. In fact, XCTAssert(actual == expected) is usually enough for most of our testing needs. One of the most common problems that people come across when trying to test their business logic is how difficult is to exercise it. And that is mainly because it tends to be hidden deep down in some hard-to-reach code path. In order to be able to exercise it, we need to mock some collaborators to create the proper environment for the test to run. Swift, unfortunately, is not very friendly about mocking.

One of the great advantages of the architecture in this regard is that most of the important business logic is in the value layer. Value objects are easy to create and exercise. There is no need to create cumbersome environments or mocking a lot of dependencies. Just create the value object, exercise it and assert the output. And that’s as easy as it sounds.

Actors like view models and stores are simply coordinators. We can test the interaction of those different modules, but we would be coupling somehow to the implementation details without really getting much value out of those tests. This does not mean that they should not be tested, this only means that those are not the tests with the greater return of investment (ROI).

Let’s see how each of the architecture layers are usually tested:

  • View/ViewController: UI Testing.
  • View Model: We test DomainState -> ViewState transformation that lives in the ViewState value object. In case there is any important interaction with stores, we mock them and check that the proper methods are being called.
  • Store: We test the DomainState value object where most of the business logic lives. Queries and Commands are easily tested via data in, data out testing. As with view models, we also test some important interaction. For instance, we may test that the data is being sent to the persistence layer appropriately.

Finally, integration tests are really valuable. Asserting against the final view model state after having exercised both view model and store code paths, gives us a lot more confidence in our testing suit.

Implementation

We won’t cover the full implementation, you can check GitHub for that, but we will highlight some interesting parts.

The first thing is defining our state.

We use Equatable to avoid sending the very same state to subscribers. After some time using the architecture, we figured three things:

  1. Making every state conform to Equatable can be quite annoying. Conforming sum types states to Equatable is a lot of unnecessary noise.
  2. UIKit is quite optimized and rendering the very same state several times will not be a problem in most cases.
  3. When testing, there is no real need to compare the whole state tree: XCTAssert(expectedState == actualState). We can just test that a specific part of the tree is correct: XCTAssert(expectedState.expectedSubState == actualState.actualSubState)

Thus, we decided not to force the Equatable implementation, but you can provide a custom one if needed.

Let’s see how the store is implemented.

Some highlights:

  • Stores are always created with a initial state. For safety reasons, there is no option that a store does not have a valid state.
  • State changes are forced to be done via the write function in a transaction.
  • Whenever a state change is detected via property observers, we check if the state is different from the previous one. If that is the case, we notify observers.

Subscription is a core part of any store. It will use closures as the callback mechanism that will be attached to a StateSubscription object, a token that will be hooked to the lifecycle of the object interested in receiving notifications. When this object is released, so will be the token and the closure will be nullified (RAII: Resource acquisition is initialization).

The store maintains a collection of the subscription tokens, weakly referenced to avoid retain cycles. It is important to point out that the real implementation of the store has helper methods to subscribe to other stores without forcing store subclasses to maintain those tokens. Really convenient.

Let’s now focus on the view layer. As we talked before, we want our views to have a render(state:) method that will be invoked by the view model when the view state changes.

The renderPolicy will be very helpful, as it will avoid that we try to render a view when it is not ready yet. For instance, a view controller can't be rendered until all their outlets are fully set. This usually mean that we should wait till viewDidLoad to subscribe to the view model and be rendered.

Now, the first approach when implementing the View Model would be to maintain an array of weak references with the views interested in the view model state. Well, it is not that easy. Swift cannot know at compile time the proper types of the objects in that views array, due to the fact that they conform to a protocol with an associated type. Compiler will complain with the following error:

Protocol ‘StatefulView’ can only be used as a generic constraint because it has Self or associated type requirements

We have two solutions at this point:

  1. Ditch the associated type in the protocol and simply have render function. This has the drawback that the view is forced to maintain a reference to the view model around to access the state. Views maintaining view models will be the most common case for sure, but will not always be true.
  2. Type erasure.

First option was not really an option for us, so we went with the second one and created our AnyStatefulView class, wrapping weakly our view object.

View Model implementation looks like this.

The handlePossibleRender method will take care of switching to the main thread if necessary, so View Model subclasses do not need to care about this. The handleNotPossibleRender method will assert promptly in case the view cannot be rendered properly. Crash early, crash often.

Simple usage example

Now, let’s rewrite some of the code examples we saw in the first part of the post. Remember the use case: we want to download a list of recruitment processes where the prospect can enter.

Let’s define the recruitment model domain first.

Now, we will define the store state.

To simplify the example, we have skipped the specific error or the fact that the loading state could have associated data when reloading, for instance.

The store would look like this:

Let’s move now to the view layer. Let’s define the view state and the transformations that it applies to the domain state.

The view model look like this:

And finally, the view controller:

Drawbacks

As every architecture, it comes with some drawbacks. The most noticeable ones are:

  • Not being able to subscribe to specific parts of the state tree. It is all or nothing. Queries and pure DomainState -> ViewState transformations ease the pain.
  • Not persistent data structures or structural sharing can cause performance problems with huge data sets.
  • No guarantee that only subclasses change the state: no protected access control in swift.
  • Mutating data can be a little bit cumbersome sometimes. As we are recreating the state tree in every change, we usually need to leverage composition to send mutating messages to all the different tree nodes involved in the mutation. This is similar to reducer composition in React.

Conclusion

Thanks a lot for reading. I would like to go back to what I first wrote at the beginning of the article: architectures really are a very complex topic to talk about.

There are trade-offs just about everything. Ease of development, valuable testing, simple code to reason about or long term scalability is some of the core points that you should think about.

Know your problem space, know your domain, know your business and try to come up with the solution that best fits your current context.

There is no silver bullet.

We are hiring!

If you want to know more about how is work at Jobandtalent you can read the first impressions of some of our teammates on this blog post or visit our twitter.

Thanks Ruben Mendez, Victor Baro and Daniel García for all the valuable feedback while developing the architecture.

You can check the full implementation in our GitHub repository.

--

--

Staff Engineer at @onuniverse. Former iOS at @jobandtalent_es, @plex and @tuenti. Creator of @ishowsapp.