Unlock This Episode
Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.
Many, many weeks ago we built a moderately complex application in SwiftUI from first principles, using only what SwiftUI gave us out of the box (part 1, part 2, part 3). We were able to get really far, really quickly, but we noticed a few problems along the way. We distilled what we observed into 5 main problems that we think are crucially important for any application architecture to solve:
- The basic units of the architecture should be expressible as simple value types.
- Mutations to app state should be done in a single, consistent way, and the units of mutation and observation should be expressed in a way that is composable.
- The architecture should be modular, that is you should literally be able to put many units of your application into their own Swift module so that they are fully separated from everything else while still having the ability to be glued together.
- The architecture should tell you exactly where and how to execute side effects.
- And finally, the architecture should describe how one tests the various components, and ideally a minimal amount of setup work is needed to write these tests.
SwiftUI gets us really close to solving some of these, but didn’t get us all the way there.
This inspired us to embark on exploring an architecture that gave very strong opinions on how each of these problems should be solved, and we’ve fully solved 4 of the 5 problems.
- State and actions are modeled as value types
- Mutations are expressed as reducer functions, which are super composable.
- Observation to state changes are expressed using a store type, which is also composable and allows us to split all of the screens in our app into their own Swift modules that can be run on their own.
- And most recently we finally showed how side effects can be modeled in this architecture.
That leaves one last problem to solve, and perhaps the most important: testing. We claim that this architecture is super testable. Pretty much every facet of this architecture can be tested, and it requires very little setup work to write your first test. We will also be able to unlock lots of new ways of testing that are very difficult to achieve without a cohesive and pervasive architecture in your application.
So, let’s start by reminding ourselves what we have been building for the past many weeks.
Control the save and load effects in the favorite primes module by introducing an “Environment” for dependency injection, as covered in our episode on dependency injection.
Control the save effect inside
testSaveButtonTappedto assert that when the reducer is called, it returns the expected effect.
Making such an assertion involves introducing local, mutable state to the test that is changed in a predictable way if the controlled effect runs. For example, you could introduce a boolean that flips from
truewhen the effect runs. Then, after you run the effect returned from the reducer (you can use its
sinkmethod), you can assert that it changed as expected.
Further assert that the save effect:
Completes. You can use the overload of
sinkthat provides a
receivedCompletionhandler to hook into this event. Use the
waitmethods on the test case to handle this asynchrony.
Does not return an action to be fed back into the store. You can introduce an assertion to the
receivedValueblock that fails if it runs.
Control the load effect inside
testLoadButtonTappedto assert that when the reducer is called, it returns the expected effect.
Assert the load effect completes, as well.
Further, rather than manually feeding the expected
loadedFavoritePrimesaction back into the reducer, extract and feed the action returned by the load test effect instead. Again, use the
waitmethods to handle this asynchrony.
Continuing the previous exercises, control the nth prime API effect in the counter module such that you can test:
- That the effect completes
- That the effect feeds the correct action back into the reducer
Elm is both a pure functional language and framework for creating web applications in a declarative fashion. It was instrumental in pushing functional programming ideas into the mainstream, and demonstrating how an application could be represented by a simple pure function from state and actions to state.
The idea of modeling an application’s architecture on simple reducer functions was popularized by Redux, a state management library for React, which in turn took a lot of inspiration from Elm.
A talk that Brandon gave at the 2017 Functional Swift conference in Berlin. The talk contains a brief account of many of the ideas covered in our series of episodes on “Composable State Management”.