SwiftUI — MVVM with a StateMachine

BIT - OFIT - FOITT
7 min readJun 8, 2022

--

Photo by John Anvik on Unsplash

Introduction

UIKit was introduced with the MVC architecture. Quickly, the Massive View Controllers problem was raised and we started to use better patterns such as MVVM, MVP, VIPER, etc.

The great advantage of MVVM is its learning curve and its compatibility with a lot of technologies. It will bring you the separation of concerns you need between the view and the logic. It will also let you increase your test coverage simply with unit tests of your ViewModels.

With UIKit, MVVM is a bit painful because the bindings have to be managed manually. That’s where SwiftUI changed the game by providing declarative programming for the UI. It also provided property wrappers like @State, @Binding or @ObservedObject that work perfectly with MVVM when they’re associated with Combine.

Problematic

MVVM lets you define all the properties your view needs inside your ViewModels. It can be a text result to be displayed, the status of a form validation, or just the visibility of an activity indicator when the view is loading.

It means that for complex views, you quickly have to maintain at least half a dozen of properties. In some situations you will be required to deal with a lot of boolean properties just to decide which components must be displayed or hidden.

Typical kind of properties your ViewModel may contain

Maybe you could refactor that code to remove some properties depending on the application logic. But you see the point:

  1. There are too many properties to maintain
  2. The State of the view is not clearly defined, even though, it is a foundation of declarative programming.

Solution proposal

When looking at state machine literature we found there were a lot of possibilities matching every kind of need. Nevertheless, after several discussions and setting boundaries to try avoiding pain points and useless parts we settled on a few simple rules.

What is a State Machine?

The state pattern is a behavioral software design pattern that allows an object to alter its behavior when its internal state changes. This pattern is close to the concept of finite-state machines. The state pattern can be interpreted as a strategy pattern; it is able to switch a strategy through invocations of methods defined in the pattern’s interface.

The state pattern is used in computer programming to encapsulate varying behavior for the same object, based on its internal state. This can be a cleaner way for an object to change its behavior at runtime without resorting to conditional statements and can thus improve maintainability.

What a State Machine solves?

A finite state machine gives some context and structure to your code and feature behaviors. It structures code in a way that at a defined point in time you know exactly what will be presented or done and with what exact condition for a specific state. This allows your app or environment to behave in the expected way and minimise unwanted side-effects. In other words: You get rid of hard coding conditions in your code. State machine abstracts all logic regarding states & transitions on behalf of you.

The requirements

Before diving into proper code and implementations, here are the set of rules we wanted to apply to our State machine.

  • Use the power of enums in Swift
  • A State must be Equatable
  • Only events can change the state
  • Data must pass through events
  • StateMachine will be part of a VM in SwiftUI

Now that the overall picture is set we should think of some ways of implementation.
Before you continue reading, you need to take into consideration the base class and its protocol we will use in the next example.

The Context

For the sake of this article we are taking a simple example to show the application of our StateMachine.

The example will be made around a car list where the user will be able to fetch data, see the data and — if there is no data available — just be prompted with an empty state. We might also want to handle some errors and reloading via pull to refresh.

Let’s build a simple graph of the possible states and events. This will help us create a simple and visual picture of the whole feature from a state to the other, and even help us to partially build our future code.

States and events in action
States and events in action

The next step will be to translate this proper graph representing the whole feature into code. For the sake of the example, we will stay focused on the functionality of the StateMachine and not on the real application logic.

The States and events

Let’s start with defining our State and Event for our future ViewModel:

States and Events

We get a whole bunch of States and Events. We can now set our view model.

The routes

Based on our graph, we can define our states routes inside the method handleStateUpdate(_,_):of our ViewModel:

handleStateUpdate method implementation

As you can see above, the whole graph is defined in this switch. This will help us to have absolute control over all states and all the VM values in each circumstance coming from a state A and going to state B.

As you can see, we have defined a default case which will make your app crash in case you try to access a not set path. This will help debug your app more easily with consistency, knowing that this series of event cannot lead to such route and state. Because it’s simply not defined in your route and graph (or maybe you just forgot a route?).

The events

Now that we have defined the routes based on our graph, we will aim towards the events. What we know is that the event will trigger a state change.

So let’s start with the basics. Our screen will be presented with it’s initial state set:

The first event triggered by our view, will likely be the onAppear when the screen shows up on the device. The onAppear event in the initial state (and only in this state) will trigger the query to fetch our data a first time. Let’s see how this translates into code:

Easy right?

As you can see, the flow of fetching data on screen appear will just be triggered when the state and event are initial & onAppear.

The state will now be loading as we return .loading on this case.

Great, now let’s see what happens next, when the query is done. How we switch the state from loading to results or something else.

After the network query, what happens?

As everything is event based, you might already have an idea. Our approach in the fetchData method will send internal events to trigger a state change.

Note: the fetchData method is just here to simulate a simple network call.

Let’s quickly analyse how the code behaves.

  1. Initial state is our entry point
  2. onAppear is called when the screen is seen on the device
  3. fetch the data is triggered
  4. state change from initial to loading
  5. fetch data ends with one of the result:
    .didFetchResultSuccess
    .didFetchResultFailure

The State changed?

Some states will require some internal changes. For example when you go from an error state to loading because the user hits the retry button, you might want to set the stateError to nil and be sure the data is empty. Let’s see some examples of how to do that.

Now we are all set from the view model point of view, we could try out our code and see if it behaves properly based on our requirements. But first we need a simple view which will handle our state machine.

The View

As you can imagine, our view will also make usage of the defined states and events from our view model. Therefore, the same pattern as the view model is used: a switch on the state to rule them all.

That’s it! We now have a simple view settled to try state machine in a real environment.

Download the whole example: Download ZIP

Conclusion

A State machine has of course its pros and cons and sometimes might even be an overkill for small feature/screen.

But whenever you have a state change with network calls or values to maintain across multiple states, having a state machine will help you maintain, simplify and straighten your code over time. It should keep the main state logic pretty simple and help you build your business logic faster by having a clear scope.

A critical area might be the fact that we chose to use a state machine in an event based way. That could be questionnable regarding the fact that other types of state machines are possible and might even allow better management in some cases.

We haven’t talked about testing the State machine in this article. But a State machine is quite easy to test as you have a strong context of states and events on which you can specifically test things depending on which state you currently are.

Overall, having a simple but powerful native state machine system was an important move for us, as most of the apps we will build in the future will be MVVM and SwiftUI oriented. A point regarding State machines and SwiftUI. A State machine doesn’t solve navigation issues, but can help your view model to settle things properly and work around a better navigation system for your view.

Nevertheless, our State machine still has some flaws as it’s for now strongly tightened to our view models. That’s some refactoring we have to do in the near future so it’s even easier to integrate into already existing projects.

This article was writen by Bryan Reymonenq, Raphael Guye and Marco Stähli.

--

--

BIT - OFIT - FOITT

Das Bundesamt für Informatik und Telekommmunikation BIT - L’Office fédéral de l’informatique et de la télécommunication OFIT