Managing SwiftUI State Using A Redux-Like Framework

Michael Collins
Neudesic Innovation
16 min readFeb 21, 2022

Model View Controller was a game changer pattern in the development of user experiences for customers. Applying a pattern with specific roles between the Model, the View, and the Controller helped to clarify and make simpler the planning and development of user interfaces. It also helped to make it possible to unit test UI behavior by moving testable code out of the View and into the Controller. MVC also led to the creation of similar patterns such as Model View ViewModel (MVVM) and Model View Presenter (MVP) which further clarified roles and enhanced the MVC pattern.

But with every positive thing, MVC also had its downsides. As multiple view user interfaces developed, there were subviews and view transitions and navigation paths that needed to be followed, which meant that state had to flow from one view to the next, and in the case of modal views, that state also had to flow back. Then with the rise of multi-core CPUs, asynchronous code and multithreading brought new challenges to state management and keeping views up to date. Lots of different solutions came about for how to handle that. Facebook ran into this issue head-on and came up with a pattern named Flux to resolve it.

Flux introduced the concept of a unidirectional data flow in user interfaces. Instead of passing state around between views, they created a singular state store that was their single source of truth. Views would read from the state store to get the current state and present it. User interaction would result in dispatching actions that would mutate the application state. The result of the mutation was that the views would get an updated state to use to update the contents of the view.

Flux has also led to what is called the Data Down, Actions Up architecture. In this architecture, data flows logically down from the state store to the application. When the application has updates to the state, actions are dispatched up to the store which is responsible for mutating and updating the state and notifying the application of the new state.

Data Down, Actions Up Pattern

With Flux’s single source of truth and the unidirectional data flow, the user interface architecture can become easier to manage. User interface development now becomes similar to a pipeline. Components do not need to manage multiple references to other components in most cases. They only need to be aware of the next component in the pipeline.

A unidirectional flow of data and actions for a UI

This architecture can be further simplified by removing the need for components to know about each other at all when combined with a coordinator pattern that packages up all of the components into a single unit.

The unidirectional architecture with a Coordinator wrapping the components

Since the introduction of the Flux pattern, there have been many refinements and implementations of the pattern such as Redux and MobX. In this article, I will present how to implement a pattern such as Redux in Swift for use with a SwiftUI application.

Redux as an Abstract Data Type

If you have gone through any kind of academic computer science course, you have undoubtedly heard the phrase Abstract Data Type. For those that have not, an Abstract Data Type presents a model of an algorithm or data structure from the perspective of the user or consumer. I am going to start my exploration of my Redux implementation by defining the implementation as an Abstract Data Type.

Using the images in the last section, we know that there are three immediate parts to Redux:

  • A store that holds the state and is responsible for mutating the state in response to the receipt of actions. The store notifies observers when a new state is available.
  • A state value which is data that controls how the application behaves. Application state could be a single value or an envelope data structure containing smaller data structures and other data.
  • Actions are events that are sent to the store to indicate how the state must change.

The store has additional rules that we must follow:

  1. Actions need to be interpreted in the order in which they are received. Two or more actions cannot update the state at the same time.
  2. Mutating the state must be an atomic operation. An atomic operation means that the application can’t read or change the state while the state is being mutated.
  3. We must recognize that the majority of modern mobile applications are concurrency beasts. They use pools of threads to perform asynchronous operations, all of which may cause updates to the application’s state. We need to ensure that the state is updated safely.

We can model our pattern as shown:

Class diagram showing the Store interface

In this diagram, we have a State class, a Store class, and an Action interface. The Store class maintains the current state. Store exposes a dispatch method that will accept an Action value that will result in the state value to be updated with a new state. Store also maintains a list of observers that it will notify when changes to state occur.

Implementing a Basic State Store

Given the Abstract Data Type, how should I define this in Swift and meet the requirements of the pattern? Fortunately, Swift 5.5 has made this much easier with the introduction of actors. Prior to Swift 5.5, serializing access to the state and serializing the execution of the actions would have had to be done using a serial DispatchQueue. However, with Swift actors, we have a better way of implementing this.

Swift actors are similar to classes, but they have additional features. Actors in Swift serialize access to its properties and methods, meaning that an actor can only be accessed by code from one thread at a time. This feature of actors allows me to model the store as an actor and ensure that one thread cannot be reading the state while another thread is updating it.

I am going to use Swift generics to implement my store. This will allow me to hold off on giving an actual shape to either the state or the actions until I’m a little further down. Using generics, I can get by with just knowing that there will be a state and action type eventually.

This first attempt isn’t going to work though because I’m already seeing a compiler error. There’s nothing initializing the state property. I need an initializer for Store. I can add one to fix this error:

This fixes the compiler error. Now I need to turn my attention to the dispatch(action:) method to update the state. I can implement dispatch(action:) to interpret the action and update state, but then my Store really wouldn’t be generic and reusable because it would have too much knowledge of State and Action. What I need is to provide a generic means for the Store to interpret the Action and mutate the State. Using Redux for inspiration, Redux uses a concept called a reducer.

In Redux, a reducer is a pure function that takes the current state and the action to execute as parameters and produces a new state. A pure function is a function that, when given the same inputs, produces the same outputs and has no side effects. A reducer will receive everything that it needs as parameters. It has no ties to any outside entities. It does not change the existing state. It only produces a new State value. I’ll add a reducer to my Store type and will implement the dispatch(action:) method to use it:

So far, this looks good, but we have not seen it in action yet. To test the action, I will implement a simple counter demo. When the application starts, the user will see the current count, which will be zero, and a button to tap to increment the count by one. Tapping the button will dispatch an action to the store to increment the counter. Because my Store actor implements ObservableObject, SwiftUI will subscribe to changes in the state and will update the UI with the new value of the count property.

Immediately, I run into two compiler errors with this code:

  • Actor-isolated property 'state' can not be referenced from the main actor
  • Actor-isolated instance method 'dispatch(action:)' can not be referenced from the main actor

These errors mean that because of the new concurrency model introduced in Swift 5.5, I cannot just directly access the Store actor. Because Store serializes access to its properties and methods, all access to Store needs to be asynchronous because calls may be blocked while other code is accessing Store.

The first error is the more problematic one. I need to be able to access the current state from the main thread. I cannot use the await keyword in SwiftUI to access the Store.state property. What I need is a way that safely exposes Store.state to the main thread of the application. Fortunately, SwiftUI provides us with the @MainActor attribute.

The @MainActor attribute, when applied to either a property or function, will force the application to read or write a property or invoke a method or function on the main thread of the application. If I apply @MainActor to my Store actor, this should fix these errors:

The application showing that dispatching an action increments the counter

This change fixes my errors and the applications works, but it’s not ideal. Forcing all calls to update the state to the UI thread isn’t realistic and defeats the purpose of using an actor. An actor is intended to be called by multiple threads. Additionally, a reducer may take a long time to run and it would be better for the reducer to be able to run asynchronously if necessary.

The other problem is that the initializer for Store also needs to have @MainActor to initialize the state property. This would mean that we can only create a Store instance on the main thread. This actually isn’t a significant problem though because in most cases Store will be created from the main thread, but I for some reason still do not like putting the @MainActor constraint on my initializer. In order to remove @MainActor from the initializer, I need to find a way to initialize the state property. What I am going to do is change State into a protocol and require that the State type have a default initializer. I am also going to change the reducer to be an asynchronous function:

I then have to update my application by conforming ApplicationState to the State protocol and dispatching the button’s action within a Task:

This works, but it’s still not good. Going back to my first requirement above, actions need to be executed in the order that they run. Because my dispatch(action:) method has two await calls in it, this could technically allow other threads to get in an update the state while one action is being executed. This brings me to a tough decision. Either I have to be ok with this, or I have to run the reducer and update the state on a single thread, such as the main thread. And if I do it on the main thread, long running code in the reducer could block the main thread. If I push the reducer execution onto the main thread, then I have to keep the reducer fast and simple, which means no execution of asynchronous code in response to an action:

With this latest iteration, my Store actor can be initialized on any thread, called on any thread, and is safe to be run from the UI. The only drawback is that the reducer must run on the main thread to update the state atomically and therefore it must be kept simple and fast without any asynchronous logic.

So what happens if I want to run asynchronous logic? There are a couple of approaches that we can take to that. First, I can create an overload that takes a factory method that can run asynchronously and produce an Action to be evaluated by the reducer:

The downside to this approach is that if I need to run the same asynchronous code from multiple places, it is not as convenient as dispatching an Action to the Store. We need something else that will help us to extend Store and allow us to handle more scenarios that include complex actions that perform asynchronous logic. More specifically, we need a way to dispatch an Action to the Store that executes asynchronous logic and produces another Action that causes a simple state update. We need middleware.

Enter Middleware

If you jumped on the early Node.js and Express bandwagon, then you are probably familiar with the concept of middleware. In Express, handling HTTP requests involves creating a pipeline of actions where the request and response objects are sent to the first item in the pipeline and are then passed down the pipeline to the next element until the end is reached. When the end is reached, the response is returned to the client. This is similar to what we need. When an Action is dispatched to the Store, we want a pipeline where we can send an Action to the first item in the pipeline. Each item in the pipeline can do one of the following:

  • Pass the action to the next item in the pipeline without further processing.
  • Operate on the action, and then pass the action to the next item in the pipeline.
  • Execute some behavior and replace the action with a new action that is sent to the next item in the pipeline.
  • Interpret the action and terminate the pipeline.

What we need to add to the Store implementation is extensibility that cannot be achieved only through the reducer. We need a middleware pipeline that can pre-process actions before they update the application’s state.

I am going to start off by defining the Middleware protocol:

The Middleware protocol uses Swift’s Callable Object feature to allow a Middleware instance to be called as if it were a Swift function. At execution time, the callAsFunction(action:) method will be invoked. The Middleware protocol has an associated Action type that describes the type of the action consumed and produced by the middleware. The output of callAsFunction(action:) is optional. If an Action is not returned, the pipeline will terminate without updating the state in the Store actor.

I can try to add a Middleware property to the Store actor:

This will fail, however, with the bane error of Swift: Protocol 'Middleware' can only be used as a generic constraint because it has Self or associated type requirements. Because Middleware has an associated type, we can only use it as a constraint, but not use Middleware directly. In order to support using Middleware, we have to use a technique called type erasure. I will define a new type that conforms to Middleware called AnyMiddleware<Action>:

AnyMiddleware<Action> wraps another Middleware instance, but provides a type safe wrapper that can be used to store references to Middleware instances. AnyMiddleware<Action> itself conforms to Middleware, and when called will invoke the wrapped middleware. I also defined a Middleware extension function named erasetoAnyMiddleware() that will wrap any Middleware instance and will return the AnyMiddleware<Action> wrapper.

I can now update my Store implementation to use middleware:

In this implementation, I have added a middleware property and a second initializer that accepts the Middleware to use. Specifying a Middleware is optional, and if not specified, an instance of EchoMiddleware<Action> will be used. I updated the dispatch(action:) method to invoke the middleware before invoking the reducer to update the state. This will allow the middleware to potentially replace the action.

The EchoMiddleware<Action> middleware type is used to return the same Action that is passed into it:

This solution now works for a single middleware, but what happens if there are multiple middleware that an application uses. Maybe an application wants a middleware for logging the actions that are executed and some other middleware that handle specific actions to perform asynchronous processing such as invoking web APIs? In this case, we need to be able to define a pipeline of middleware that can execute in order. In the first approach, I can pass an array of Middleware to Store to be executed when dispatch(action:) is called:

Now I can use a variadic parameter in the initialer to pass an array of AnyMiddleware<Action> objects to Store to be run in order. This is better because I now have the ability to run multiple middleware, but it’s still problematic. The implementation of dispatch(action:) is more complex and a little harder to understand because of the logic for evaluating the middleware. What would be nice would be to create a wrapper Middleware type that can wrap and execute an array of Middleware. I can create that by creating a new MiddlewarePipeline<Action> type:

The introduction of MiddlewarePipeline<Action> lets me simplify my Store implementation:

We’re making good progress. This is feeling better. But there’s one more problem: what happens if we want to conditionally enable middleware? We would have to build an array of middleware and then pass it to the Store initializer or dynamically build a MiddlewarePipeline<Action> at runtime. Is there anything we could do to make this simpler?

When SwiftUI was introduced, the Swift team introduced a new feature called result builders. If you have used SwiftUI, you have used result builders. You use a result builder when you define a View. The content that you define in the View is turned into a compile-time function that produces a View that renders text, buttons, or other UI elements on the screen. We can use result builders for other things as well, such as for defining a MiddlewarePipeline<Action>. This will allow us to do things such as conditionally enabling middleware, or running a for loop to generate multiple middleware at runtime.

Building a custom result builder involves defining a struct type and tagging it with the @resultBuilder attribute. You need to define at least one buildBlock method. My first implementation looks like this:

This lets me change my Store implementation look like this:

This is better, but if you notice from the convenience initializer, the MiddlewareBuilder requires that all of the middleware be converted to AnyMiddleware<Action>. This is not going to be pretty. Fortunately, the result builder gives me another method that can do this conversion for me:

The buildExpression method of MiddlewareBuilder will now be called first for each element of the builder function. I can use buildExpression to convert each element to an AnyMiddleware<Action> type, and then the array of all of the middleware can be passed to buildBlock where it will be combined in a MiddlewarePipeline<Action>.

This further simplifies my Store implementation:

Let’s say that I have a LogMiddleware<Action> type:

With the new implementation, I can create a Store like this:

What happens if I only want to enable the LogMiddleware<Action> in a debug build? This will not work:

However, I can make it work by adding a new construct to my MiddlewareBuilder result builder type:

Now the if block will work. When the compiler interprets the body, it will first call buildExpression for the LogMiddleware<Action> item. Then it will call buildBlock to wrap the LogMiddleware<Action> in a MiddlewarePipeline<Action>. Next, the MiddlewarePipeline<Action> will be passed to buildOptional. Finally, buildBlock will be called again to produce the final MiddlewarePipeline. There’s another function that we can implement named buildFinalResult which is called at the end to output the final result of the middleware builder block. This simplifies the MiddlewareBuilder a little:

I next run into an issue if I try to use an if...else block:

To handle the if...else block, we need to implement the buildEither methods on the MiddlewareBuilder:

The final case that we’re going to handle is generating multiple middleware in a for block:

I can handle that by implementing the buildArray function:

Middleware Use Case

Now that we’ve gone through the effort of implementing support for middleware, how would we actually use it? I’ll give an example of logging in. Pretend our demo application prompts the user for a username and password that needs to be sent to an identity service. If the user is authenticated, the identity service issues an access token that can be used to authorize calls to REST APIs.

For the demo application, my state will look like this:

I next define two actions:

The login case takes the username and password as associated values. The setAccessToken case accepts the access token as the associated value.

I can then create a LoginMiddleware type that will perform the asynchronous login operation:

My application code then looks like this:

When the Log In button is tapped, the button will dispatch the login(username, password) action to the store. The store will pass the action to the LoginMiddleware which will intercept the action, perform the authentication process asynchronously, and will then forward the access token as a new output action. The setAccessToken(accessToken) action will reach the reducer which will update the application state.

What if an error occurs during authentication? There is no way to throw an exception using the Middleware. That is actually expected. If you are expecting that an error may occur, you should create an error action that will update the state to report the error. Either way, your middleware should output an action, unless there is nothing to be done from the result of the middleware’s operation, in which case it should return nil to terminate the pipeline.

Dispatching Asynchronous Sequences of Actions

Another great feature of the new concurrency model in Swift 5.5 is the ability to generate asynchronous sequences using the AsyncSequence protocol. I can extend my Store implementation to allow dispatching actions produced by an asynchronous generator:

Dispatching Actions from Combine Futures

What if you have older code that uses Combine’s Future type to implement an asynchronous promise? This framework can handle that too using an extension:

Dispatching Actions from Combine Publishers

If we can capture an action from a Combine Future, can we do the same from a Publisher? Yes, we can:

Where Have We Gone?

In this article, I laid out requirements for a Flux/Redux-like state management store to act as the single source of truth for an application. I designed the state store to also be compatible with SwiftUI and support the use of Combine publishers, ObservableObject, and a @Published state property. I then used the new Swift 5.5 concurrency model to implement the Store as an actor to serialize reads and updates to the application’s state. I next implemented a middleware layer to provide extensibility to my Store and allow for intercepting actions and performing background asynchronous operations for actions, possibly that result in replacing the action with another action. Finally, I extended my Store type to support Combine Future and Publisher types.

I hope that this journey makes sense to you and you find value in using this Store implementation in your applications.

Source Code

--

--

Michael Collins
Neudesic Innovation

Senior Director of Application Innovation at Neudesic; Software developer; confused father