In this post I am going to introduce a library for architecting iOS apps, called ArchitectureKit:
“Simplest architecture for FunctionalKit”
ArchitectureKit is a library that tries to enforce correctness and simplify the state management of applications and systems. It helps you write applications that behave consistently, and are easy to test. It’s strongly inspired by Redux and RxFeedback.
I have been trying to find a proper way and architecture to simplify the complexity of managing and handling the state of mobile applications, and also, easy to test.
I started with Model-View-Controller (MVC), then Model-View-ViewModel (MVVM) and also Model-View-Presenter (MVP) along with Clean architecture. MVC is not as easy to test as in MVVM and MVP. MVVM and MVP are easy to test, but the issue is the UI state can be a mess, since there is not a centralized way to update it, and you can have lots of methods among the code that changes the state.
Then it appeared Elm and Redux and other Redux-like architectures as Redux-Observable, RxFeedback, Cycle.js, ReSwift, etc. The main difference between these architectures (including ArchitectureKit) and MVP is that they introduce constrains of how the UI state can be updated, in order to enforce correctness and make apps easier to reason about.
Which make ArchitectureKit different from these Redux-like architectures is it uses feedback loops to run effects and encodes them into part of state (we will see this in point 3) and uses monads from FunctionalKit to wrap the effects.
ArchitectureKit runs side effects for you. Your code stays 100% pure.
ArchitectureKit itself is very simple.
Each screen of your app (and the whole app) has a state itself. in ArchitectureKit, this state is represented as and object (i.e. Struct). For example, the state of a TO-DO app might look like this:
This State object, representes the state of the “List of TO-DOs screen” in a TO-DO app. The “todos” var contains all the to-dos that might be drawn in the screen and “visibilityFilter” tells what todos should appear in the list.
With this approach of having an object that actually represents the state of a screen, views are a direct mapping of state:
view = f(state)
That “f” function will be the UI binding function that we will see later on.
To change something in the state, you need to dispatch an Event. An event is an enum that describes what happened. Here are a few example events:
Enforcing that every change is described as an event lets us have a clear understanding of what’s going on in the app. If something changed, we know why it changed. Events are like breadcrumbs of what has happened. Finally, to tie state and actions together, we write a function called a reducer. A reducer it’s just a function that takes state and action as arguments, and returns the next state of the app:
(State, Event) -> State
We write a reducer for every state of every screen. For the list of todos’ screen:
Notice that the reducer is a pure function, in terms of referencial transparency, and for state S and event E, it always return the same state, and has no side effects.
This is basically the whole idea of ArchitectureKit. Note that we haven’t used any ArchitectureKit APIs. It comes with a few utilities to facilitate this pattern, but the main idea is that you describe how your state is updated over time in response to events, and 90% of the code you write is just plain Swift, so the UI logic can be tested with ease.
But what about asynchronous code and side effects as API calls, DB calls, logging, reading and writing files?
AsyncResult data structure is used for handling asynchronous operations. AsyncResult is just a
typealias to a
Reader<Future<Result>> monad stack. These monads (and its monad transformers) are available in FunctionalKit, which is the only dependency in ArchitectureKit.
FunctionalKit provides basic functions and combinators for functional programming in Swift, and it can be considered a extension to
Foundation. We mainly use the Reader monad along with the Future and Result.
We use monad transformers to create AsyncResult as a stack of these three monads. An AsyncResult is a monad that represents an asynchronous operation that returns either a successful value or an error, and also provides a mechanism for dependency injection.
We can see in the following snippet an example of Facebook login using AsyncResult:
To create an AsyncResult we use its static method
unfoldTT (the TT stands for transformer, since it is a monad transformer). It expects a function as a parameter which has two inputs: an environment or context and a continuation or callback. The environment parameter comes from the Reader monad and it is an object that contains the injected dependencies. The continuation parameter is a callback function that must be called with the
Result value returned from de async operation. In the example the Result returns an
string when it succeeds. When the login succeeds, we call the continuation method with a successful
Result with the token from Facebook. It the login fails, we call the continuation method with a failure
Result that contains the error.
The AsyncResult must me parameterized with 3 values. First one is the Environment type (which contains the dependencies), second one is the actual value expected from the async operation (in the example we use
string because we expect the Facebook login to return the login token), and the last one is the error type the
Result will return if something goes wrong.
Every asynchronous operation and side effect must be performed using the AsyncResult monad and we will use Feedbacks from ArchitectureKit to execute the their side effects. Also, we will see how to work with AsyncResults in the full example.
Let’s add a new feature to our previous TO-DO app! We want let users to save their TO-DOs to the cloud. That would require an network call, which is a side effect and it is asynchronous, so to achieve it, we will use feedback loops. The way of dealing with effects in ArchitectureKit is encode them into part of state and then design the feedback loop.
A feedback loop is just a computation that is triggered in some cases, depending on the current state of the system, that launches a new event, and produces a new state.
A whole ArchitectureKit loop begins from a UserAction that triggers an event. Then the reducer function computes a new state from the event and previous state. ArchitectureKit checks if any feedback loop must be triggered from the new state. If so, the feedback produces a new event asynchronously (by executing side effects) and a new state if computed from the feedback’s event.
So, we can see a whole ArchitectureKit loop as the following sequence:
In the following code snippet, we can see a Feedback example of how to store the user’s TO-DOs in the cloud:
For implementing this feature, two new events have been added
todosStored(Bool) and there is a new Bool variable in the state:
storeUserTodos(todos:[Todo]) function is the function executed in the feedback loop, which returns an AsyncResult monad that returns the
todosStored(Bool) event when side effects are executed. This function is in charge of storing the user’s TO-DOs.
A Feedback object is composed by two functions that receive the current state as parameter. The first function is the actual AsyncResult to be executed, and the second one checks when the feedback loop must be executed, depending on the state. In the example, the user’s TO-DOs feedback will be executed when
mustStoreTodos variable is true.
In the new reducer, the
storeTodos() event is setting
mustStoreTodos to true, and
todosStored(Bool) is setting it back to false. The
storeTodos() event will be triggered by an UserAction, like tapping a button.
The following diagram illustrates the steps for storing the user’s TO-DOs:
UserAction is the object from ArchitectureKit that represents any action from the User or the iOS framework that triggers an event that changes the state (and from that state change it could trigger a feedback loop).
It has two methods:
init: creates the UserAction and specifies what event will be triggered when user actions is executed
execute: executes the user action.
We can see here a simple example of how ArchitectureKit’s code would look like:
It’s a simple counter with an increment and decrement buttons. The State is just an integer that contains the current count.
For Dependency Injection we are gonna be using the Reader monad that is at the top level in our AsyncResult monad stack.
The Reader wraps a computation with the type
(D) -> A , and enables composition over computations with that type.
D stands for the “Reader context”, and it represents the dependencies needed for the computation to run. It also automatizes dependency passing all the way down, since it does it by itself thanks to the way
Readers compose all together.
So, it defers computations at all levels, since it wraps computations and also injects dependencies by automatically passing those across the different function calls.
The approach is to create a Swift struct (that I call Context) with all needed dependencies and pass that object when running the
Reader (AsyncResult) computation. In the following example we can see how to inject dependencies in ArchitectureKit:
As you can see in the previous snippet, a
BaseContext protocol is defined with the required dependencies that the
AppContext class will have. In that class, we add the app’s global dependencies. Then we create a context class for each screen, like
TodoListContext . It extends from the AppContext and includes specific dependencies for the TO-DOs list screen, such the view.
If we want to test the
deleteAllTodosFromDatasource feedback, we can create a test context with a mock datasource and a mock view. With this approach we can also configure different contexts for each server environment: production, staging, test, development.
In the next section, we will see a full example where we are using different context for testing the app and the actual code.
You can see more about Dependency Injection with Reader Monad (in Kotlin for Android) in this Jorge Castillo’s article:
I created a simple but fully working app using both ArchitectureKit and FunctionalKit to illustrate how to use the library and how to architect the app itself. You can find the app’s source code here.
The app is about jokes about Chuck Norris, retrieved from this API. The first screen shows a list with joke categories. When user taps on one category, a new screen appears with a random joke of that category. We can see some screenshots (disclaimer: I didn’t put any effort on the UI):
In the following image we can see the layers the app is divided in:
It is divided in:
If you took a look into the tests folder you will see how we are injecting dependencies, and how easy is to test the whole app using a very simple mock view.
I would use ArchitectureKit as architecture for the View layer in a Clean architecture approach, replacing Model View Presenter. Also, I would architect the app with Functional Clean Architecture instead of OOP Clean Architecture. Trying to use functions over classes when possible, using the AsyncResult monad to keep your code pure, encoding side effects, injecting dependencies, take advantage of functional transformations… I think FunctionalSwiftArchitecture is a good example of how I would make an app from scratch.
The advantages I see in ArchitectureKit are:
I am planning to improve the UserAction mechanism. And creating a specific UserAction for every UIKIt control.
I will refactor the library and change it if new uses cases appear.
Create your free account to unlock your custom reading experience.