22 min read

Should We Bring Redux to iOS?

One of the things I've been puzzling over recently. Can we benefit much from bringing redux-like architectures to iOS? How? And how can we get most of it?
Should We Bring Redux to iOS?
Photo by Raagesh C / Unsplash

One of the things I've been puzzling over recently. Can we benefit much from bringing redux-like architectures to iOS? How? And how can we get most of it?

If you had a chance to watch one of the recent WWDC-2020 videos called Data Essentials in SwiftUI, you probably payed attention to the following image, describing the data flow in SwiftUI.

If you pointed out that image, I bet you also noticed single-source-of-truth phrase per second rate of that talk.

Actually this is the exact illustration of the idea of any unidirectional data flow (UDF) architecture with the only difference that basically, SwiftUI suggests that the whole loop happen right in the UI layer.

We've already suffered from "decade of massive view controllers" that we met in every second app. This time with the help of SwiftUI I bet we will suffer from fat views.

Luckily, we are wise already. So perhaps, we could decouple that UDF pattern from UI layer, decompose and leverage it up to an app scale?

Actually it has been done long ago in web apps. Furthermore, it has become so popular among web frontend devs that I have no idea how it managed to gather almost zero attention among iOS folks for so long time.

The Origin

Now it's hard to figure out how it all started (I personally don't care much). But 4 of May 2014, 19:19 of that talk at Facebook's conference seems to be a quite accurate candidate for the origin.

(Though similar ideas had already been used in Elm a couple of years before).

Originally, Facebooks's architecture design pattern was called Flux and later became the basement for tons of libs and frameworks for state management in web apps' frontend.

Among them Redux is one of the most famous. So famous that it is often used as the name of the pattern along with UDF (unidirectional data flow architectures) or MVI (Model-View-Intent), or even Elm which is actually a programming language with UDF in mind.

Well, naming is IT has always been a cumbersome.

The idea

In case you are not familiar, here is a short description of most commonly used Redux-like architecture components.

A list of buzzwords to start from:

  • View
  • Props
  • Actions
  • State
  • Dispatcher
  • Reducer
  • Store
  • Side Effects

Views and Props

View in Redux has one special feature: it's data driven with only one entry point, which is usually a render function with the only input parameter:

func render(_ props: Props) { ... }

In iOS terms we can have a View for SwiftUI or ViewController for UIKit because UIKit's Views are too much coupled to ViewControllers.

In case of SwiftUI we can have a view constructor instead of render function due to SwiftUI specific view lifecycle.

The main trick about data-driven View is that the entire view's state is represented by a single DTO that contains all prepared information that may be shown to user directly without changes. All View's possible states can be described by the state of DTO entirely.

We sometimes called them "view models", but in Redux world it's called Props.  

It's easy to confuse with MVVM's ViewModel where VM is a sort of a handy guy that is binded to a View, contains some presentation logic and is usually capable of doing many things. In contrast, Props is data. Props are meant to be just lightweight view models DTOs.

If we need to handle user inputs or touch events we do it by closures that are passed as Props's properties.

If we need to do animations it's up to the View entirely: it may diff the old state with new Props and behave accordingly.

Complex UI components may be a composition of simple ones, the same can be said about Props.

Actions

Actions work like transactions describing changes that happen in the app. User inputs and taps, server responses, etc. In MVI they are called Intents but it's still the same thing.  

State

In Redux, state is a data representation of the whole app. In broad sense the whole app can be considered to be a function of state.

Actually, Flux doesn't restrict the state to be single for the whole app and suggests having multiple independent states.

There are also debates about: "What exactly should we put into the state?".

Some developers suggest putting things into state depending on their lifecycle and scope of use. Other words we don't have to put everything there.

Other devs insist on putting exactly everything into the state and keep the rest of the app stateless.

Dispatcher

Upper layers of the app, like UI doesn't know how to mutate the state. Furthermore they simply can't do it as state is immutable for them. Instead, all what they can do is to dispatch some action to Dispatcher.

There are different approaches.

Originally, Flux meant to have multiple stores with corresponding multiple states, so Dispatcher was intended to pass actions later on to Stores.

It also allowed to set the dispatching priority for the stores and implement a kind of blocking dispatching: make some Store wait for another Store to handle the action.

Alternatively, Redux suggests a single store with a single state, so the dispatcher here simply passes actions to store and ensures that actions are handled in a serial way.

Reducer

Reducer is the only guy who knows how to mutate state. Traditionally, reducer is a pure function from state and action as input. It performs mutation and returns a new state.

At first sight it looks like reducer is a huge disgusting guy who works with another huge guy, known as State and knows how to change it.

The truth is that we can have a State that is a composition of substates. And we can make them as small as needed.

The same thing about reducer. It can be decomposed to multiple clean and tiny pure functions that are responsible for mutation of particular piece of state.

Store

The store is the guy to put together state, reducer, dispatcher and observers. When action is dispatched to the Store, the action is applied to the state through reducer, the new state is then passed to all store observers.

Side Effects

As you you might have already noticed,  it's not so obvious where we should put async work: network requests, database fetches and other I/O operations, timer events, location service callbacks etc.

Things, that in UDF world are called "side effects".

There are debates where to put them, no single correct opinion and several common patterns that are applied in different combinations in all UDF architectures libraries.

Async Actions

One of the ways to handle side effects is to introduce some special kind of executable actions, like AsyncAction that would perform some async work and then dispatch more actions when work is done.

When dispatched, such actions do not mutate the state through reducer, they are just executed at some point.

So, when we need to fetch something we just dispatch one of such async actions.


protocol AsyncAction: Action {
    func execute(dispatch: @escaping DispatchFunction)
}

struct FetchMoviesList: AsyncAction {
    let page: Int

    func execute(dispatch: @escaping DispatchFunction) {
        APIService.shared.fetchMoviesList(page: page) {    
            switch $0 {
            case let .success(response):
                dispatch(SetMovieMenuList(page: self.page,
                                        response: response))
            case let .failure(error):
                dispatch(MovieListFetchError(error: error))
            }
        }
    }
}

Action Creators

For decoupling purposes, we can encapsulate Actions init process with Action Creators.

We can treat them as factories. In original Redux it's just functions that init Actions.

If we go on with factories that may be rather handy if we also make them init both plain Actions and Async Actions depending on environment.


struct FetchMoviesList: AsyncAction {
    let apiService: APIService
    let page: Int

    func execute(dispatch: @escaping DispatchFunction) {
        apiService.fetchMoviesList(page: page) {       
            switch $0 {
            case let .success(response):
                dispatch(MoviesListFetchSuccess(page: self.page,
                                                response: response))
            case let .failure(error):
                dispatch(MovieListFetchError(error: error))
            }
        }
    }
}
    
struct ActionsFactory {
    let apiService: APIService
    
    func fetchMoviesList(page: Int) -> Action {
        FetchMoviesList(apiService: apiService, page: page)
    }
}
    
    

Async Actions + Dumb Middleware

When using async actions we need somebody who will execute them. For that purpose we can introduce Store Middleware and make it intercept and execute specific types of actions as soon as they are dispatched to the store and before they get into reducer.

We can make Middleware extremely simple and intercept AsyncActions simply by conditional typecasting:


struct AsyncActionsInterceptor: Middleware {
    let dispatchFunction: DispatchFunction 

    func intercept(_ action: Action) {
        guard let action = action as? AsyncAction else {
            return
        }
           
        action.execute(dispatch: dispatchFunction)
    }
}
 

Plain Actions + Intelligent Middleware

Another approach is to keep all Actions simple. Instead, we can make Middleware more intelligent.

We can allow middleware to intercept exact actions, decide which AsyncWork to perform and afterwards dispatch more actions as the result of its work.  

We can scale it up and have many Middlewares where every one will be responsible for its own work.


struct MoviesMiddleware: Middleware {
    let apiService: APIService
    let dispatch: DispatchFunction 
     
    func intercept(_ action: Action) {
        switch action {
        case let action as FetchMovies:
            apiService.fetchMoviesList(page: action.page) {       
                switch $0 {
                case let .success(response):
                    dispatch(MoviesListFetchSuccess(page: self.page,
                                                    response: response))
                case let .failure(error):
                    dispatch(MovieListFetchError(error: error))
                }
            }
         default:
             break
        }
    }
}
 

Super Intelligent Middleware

Middleware can be super intelligent. Instead of intercepting Actions, it may receive App's State and depending on it, decide which work to perform and dispatch Actions when it's done. This is how it works in ReSwift-Thunk.

State-Driven Side Effects

Another approach is almost the same as "Super intelligent middleware" that reacts to the state with only difference that we have no middleware at all.

Instead of middleware, we introduce Side Effect Drivers that are simply subscribers of the Store and receive state changes absolutely the same way as UI does.

If find this concept to be especially interesting.

By using Side Effects Drivers as Store observers we put them in the same row with UI. In a certain sense that means that we treat UI as a special special kind of side effect as well.

At the same time, it makes us implement side effects as a function of state meaning that at any point of time the amount of async work that our app produces is completely defined by its state.

That architecture approach draws an invisible boundary between the app's state and the outer world and that's just beautiful.

Reducers with Feedback Loops

Another approach to side effects suggests making reducers more complex. We can allow them to return new state + some feedback.

What is a feedback? It's up to implementation entirely. It can be anything, containing instructions for executing async work. Store should be capable to execute Feedback by means of middleware (or any other way) and dispatch result actions when task is done.


struct FetchMoviesList: Feedback {
    let apiService: APIService
    let page: Int

    func execute(dispatch: @escaping DispatchFunction) {
        apiService.fetchMoviesList(page: page) {       
            switch $0 {
            case let .success(response):
                dispatch(MoviesListFetchSuccess(page: self.page,
                                                response: response))
            case let .failure(error):
                dispatch(MovieListFetchError(error: error))
            }
        }
    }
}
    
struct FeedbackFactory {
    let apiService: APIService
    
    func fetchMoviesList(page: Int) -> Feedback {
        FetchMoviesList(apiService: apiService, page: page)
    }
}

struct Reducer {
    let feedbacks: FeedbackFactory
    
    func reduce(_ state: State, 
                action: Action) -> (State, Feedback?) {
        switch action {
        case let action as FetchMovies:
            let newState = .fetchingMovies
            let feedback = feedbacks.fetchMoviesList(page: action.page)
            return (newState, feedback)
        default:
           return (state, nil)
        }
    }
}
    
    

Side Effects Recap

As you can see, all side effects approaches are quite similar. All what we do is moving the same responsibility around and deciding who will be more dirty: either Action or Middleware or Reducer or somebody else.

Action Creators, Thunk, Saga in Redux

There are 3 most popular ways of working with SideEffects in the original Redux

  • Action Creators
  • React-Thunk
  • React-Saga

As I've already mentioned, action creators provides encapsulation of action init process.

React-Thunk is a kind of ActionCreator that encapsulates all async stuff under the hood and allows to create async actions in the same way as plain sync ones without exposure of any details.

React-Saga is a kind of intelligent middleware that intercepts plain actions and allows to perform complex sequences of work. It also allows to keep track of the workflow process, retry failed steps and so on.

Rather interesting that saga is actually a design pattern that came from distributed transactions world. More detailed info can be found here.

First impressions

Art at the Piazza Castello
Photo by Alexander Schimmeck / Unsplash

For some reason when somebody talks about using Redux-like architecture  in iOS it's often considered like weird experiment.  And I'm not an exception in that sense. The first time I saw Yasuhiro Inami's talk I thought something like:

"well... interesting thing but seems like you are the only guy in the world who use it".

The first time I heard about Redux single state I say Vietnam war veteran's flashbacks of singleton special forces. Props seemed just yet another name for view models. Single source of truth and state changes observations made me smile because I often used CoreData and was happy to have all this out of the box.

Advantages

Before we move to the main advantages list, let's introduce a short list of improvements that we can easily apply to our system, coded in Swift with Redux in mind.

Move everything to a single State

We will follow one of the opinions in the internet and will start to put in state, making presentational and the UI layer of the app stateless.

Actions as Plain DTOs

We will pick one of those approaches to produce side effects where sombody is intelligent and Actions are plain DTOs

Codable State

It will cost almost nothing to add auto-conformance to Codable protocol for the state.

Codable Actions

As long as our Actions are plain DTOs we can also make them conform to Codable. It might require some additional work and codegen with Sourcery, but it's absolutely worth it.  

Multidirectional vs unidirectional data flow

I won't tell you how much more predictable and comprehensible the program is, when the flow is unidirectional comparing to a spaghetti with multiple states scattered all over the app.

Sounds ridiculously obvious but we really have no power to restrict the data flow direction in our app other than using UDF architectures.

We simply have no tool to control that. Even if we enforce ourselves to do it manually by just calling the right functions we are likely to fail as soon as the programmed system becomes complicated enough.

Fed up with spaghetti call stack? Tired of helpless attempts to make the data flow in one direction?

Immutable State

I won't tell you how much more predictable and comprehensible the program is, when all mutations happen and restricted to happen only in predefined places.

Suffer from unpredictable state changes and corrupted data?

Single Source of Truth

I won't tell you how much more predictable and comprehensible the program is, when when the rest of app is at most stateless, while the whole app state is consistent, defines everything what's going on in the app and explicitly contained in one place. Handling sudden changes in requirements for the app has never been so easy!

Sudden feature update requires a workaround to deliver synced state in different parts of the app?
Sudden rework of a feature now requires to pass the state from one end of the app to another? 

Independent layers development

Redux-like architecture implies that that UI depends only on Props. So we can implement it independently from state or side effects. Not just implement but also preview and test it with different props as an input.

State and reducers can be implemented and tested completely independent from UI and Side effects.

Side effects. It depends on the exact way we choose to handle Side effects but basically we only need result Actions and some Core Services/Network Service stuff for them.

That all means that we can implement those layers almost independently from each other without need to spend additional time coding stubs.

Depressed because cannot work on different layers independently?

Testability

Tests without Redux

Usually We make a lot of effort firstly to make our code testable, then to test it and maintain tests later on.

If we need to test some decoupled Service, or tiny class, that's fine. Problems arise when we want to test something bigger, like cursed overcomplicated screen module.

Probably you've also found yourself looking at all those VIPER modules... and thinking that it's absolutely not a thing you want to write tests for...

Indeed, even properly decoupled screen module requires a lot of extra work to be done for testing.

The case

Let's assume that we want to test some complicated creation wizard or registration flow that involves several screen modules. That also implies data obtaining, some shared data between modules with variable navigation options.

It will either be untestable at all or it will require extensively decomposed app architecture.

Ok, we break it into tiny components and test them separately, but even if we have state-of-art testable Services + VIPER + Navigation Coordinator we will have to write mocks, spies and stubs for really a huge number of things.

The amount of work required to perform proper testing may be really overwhelming.

Conversely, that creates one more problem: such tests are not likely to be simple, compact and comprehensible. Maintenance of such tests suit would be even more painful than its initial implementation.

Tests with Redux

the pride of san francisco, golden gate bridge
Photo by Joseph Barrientos / Unsplash

In contrast Redux-like things is a great pleasure to test.

  • Any user behaviour, including navigation and inputs along with side effects results may be expressed by a sequence of actions.
  • No need to write mocks because state has no dependencies on services or side effects implementation.
  • Reducer is pure function without dependences, side effects or async work. The most testable guy in the world and the only guy we really need to test.
  • Tests are super simple: we just create state, mutate it with action through reducer and check the result
  • State can be decomposed down to small substates with tiny reducers. We can test them separately as cozy unit tests.
  • Side effects testability mainly depends on exact implementation but in general they are also rather test-friendly.
  • If we want to have some "integrational" test stuff we can test substates together in a larger state object. We can do it at any scale actually.
  • If we want to test presentational layer - we just create any State and test the generated Props
  • If we want to test UI - we generate any Props and then go on with XC tests UI capabilities
  • If we want to test UI with Snapshots - we generate any Props, make snapshots, compare, etc.

Redux-like architecture allows us to cover with tests all layers of our app, with exceptionally low effort. Probably the lowest effort I've ever seen.

Data Driven Tests with artifacts in Redux

As you remember, we've arranged to make the State and Actions Codable. That allows us to serialize them and turn into JSON in just a single line of code.

We can record any user flow by recording the sequence of actions and states. And that's mindblowing approach to testing the app.

Just imagine:

  • You launch the app in record mode.
  • Write down initial state
  • Perform some flow you want to test
  • Write down the sequence of actions
  • Write down the result state

Now you've got a full set of artifacts to test your flow.

Might I remind you that we are not talking about Xcode UI tests feature. That's absolutely viable data-driven testing wrap around unit tests for our business logic.

Advanced Debug with Redux

UDF, immutable state and single source of truth are the things that obviously help a lot in debugging.

Apart from this, Redux-like architecture brings debugging to some unreachable level.

Time travel debug

Time travel debug is about writing down the sequence of states and then switching between them back and forth as if you are Marty McFly.

It's a popular way to impress the audience and show the dominance of Redux-like architectures. However, I find it barely useful on practice.

Remote debug

Instead, I consider remote debug to be a much more helpful thing. Again, as we've got serializable State and Actions we can send them anywhere and use it for later investigations of bugs.

How can we use it on practice? We can trigger debug mode in the app and send states with actions to a remote debug server in real time. With the help of them we can reproduce bugs and use those sequences as artifacts for writing data-driven tests later.

Hot reload

During remote debug we can allow the app not just send its States/Actions but also receive them from server and apply/dispatch.

Great collaborative tools to work hand-in-hand with QA department, isn't it?

Redux-like +  SwiftUI and UIKit

SwiftUI views are state-driven and it might be not very convenient to integrate them in an existing app with too much "imperative" and spaghetti directional data flow architecture.

SwiftUI connects to Redux perfectly. We can also easily build state-driven ViewControllers and Views and connect them to Redux or interop them with SwiftUI.

If we are developing a new app in December 2020 and SwiftUI may not seem to be very suitable for us yet. But we may expect to start using it in the upcoming year.

Redux might be a very good candidate to assist such transition.

The other side of Redux

Moon
Photo by Nicolas Thomas / Unsplash

At the very first site Redux could seem to be an ultimately flawless architecture design pattern. Could. But it doesn't. In contrast it arises a lot of questions right away.

At first sight it seems to be a silly joke on the whole objective oriented programming.

Indeed. The whole app architecture happens to be turned inside out with totally broken encapsulation which seems very unusual if not to say weird.  

Huge, globally accessible, single state with a size of the app. God damn, what have you done to the the app?!

Scope

One of the questions arising when getting acquainted with redux-like stuff is: "What's the scope of the pattern?" Does it exist in UI layer and we should put it somewhere inside ViewController? Is it a replacement for VIPER?

It gets more confusing when many of UDF architecture libs for iOS suggest putting it at any scope you want.

You immediately think that you don't need yet another replacement for your sweetheart VIPER while building the whole app around UDF lib looks too much scary.

My opinion is: you can have whatever you want in the UI layer: MVC/MVP/VIPER built on UIKit or SwiftUI. UDF architectures work best for the whole app's scope state management.

G-word

Honestly, I'm so prejudiced about global/single/god objects that I still can't agree with myself about redux's single state. Just can't throw away the idea that it's definitely an antipattern that should be avoided by all means.

In its defense, I should note a few things.

First of all, global is not quite right word for the state. Actually it's not globally accessible from anywhere and it's only available for store's subscribers when it's delivered on change.

Secondly the state is significantly decriminalized by its immutability for all consumers. So yes, it's a kind of god object, but it's just a state without behaviour and without any ability for anyone to uncontrollably change it.

These two along with the fact that all changes happen only in predefined place are almost capable to convince me that it's possible to live with Redux's single state.

Single state pitfalls

Ok, even if we reach an agreement with ourselves about single state design, we may have problems that may arise when app grows large.

Fortunately, we have defusing kits for those bombs.

State Complexity

As soon as the app gets large enough figuring out anything from the single state will inevitably become a huge pain.

Flux <-> Redux

The idea of breaking the single state into multiple states with independent stores and going Flux way doesn't seem to be very attractive solution for me.

We may loose the many of very attractive Redux features and deal with possible headache from syncing of the state from multiple stores.

Decomposition

The first thing I'd start with is decomposition of the state into smaller substates while staying in the same single store.

Breaking the state by app features or by its functional units may work pretty well.

Facade & Interface Segregation

Decomposition will help for some time, but as soon as the complexity grows further it will be hard to get the needed data from the state. Hard because the whole state structure is huge and to get anything from it you'll have to know exactly what and where to look for.

The next step might be to put the whole state behind a facade with flat structure and hide all the complexity of getting data behind it.

The facade can be represented by just a protocol, which in turn may be a segregation of more simple protocols.

This step will allow to build the presentation layer of the app dependent not on the whole state, but on some specific parts of it, covered effectively with corresponding protocols.

In my opinion that would make the state more descriptive and even a huge app state can be turned into something more or less comprehensible.

Funny fact. We sometimes have to build facades in front of unmaintainable legacy code or some huge god objects but now I suggest building those god objects with facade in front by our own hands.

State Inconsistency

Another threat that always looms over the single state is the possibility to make it inconsistent. If we don't apply normalization to the entities, we can easily face data duplication that will turn the single state of truth into multiple sources of lie.

In order to fight it, we should store entity instances in the only part of the state. A kind of entity state repository which may be simply a key-value data struct. In all the other parts of the state, entity identifiers should be used.

When we need to obtain some entity on presentation layer or somewhere, we simply get it by id from the state repository.

Performance

How will the app perform in case of huge state? The whole app may mutate the state too often and trigger too many updates, delivered to the store observers. Which in turn may hit the performance.

Luckily, we have a lot of room for performance tuning.

  • If we face too frequent state updates that slow down UI, we can make props equatable and diff against them
  • SwiftUI performs diffs on its own, comparing view tree hierarchy by their bodies. Additionally we can mark Views as equatable and diff manually, based on props.
  • In UIKit it's also possible: we can diff against equatable props and reload UI only when necessary.
  • We can debounce and throttle at state observer level.
  • If props creation is slow for some reason, we can move those slow parts down to State and Reducer layer. Do heavy things in reducer and keep result in State.
  • We can perform reducer function on non-main thread queue (we actually should better do it by default) and don't affect main UI thread
  • If we hit responsiveness issues again we can profile reducer, spot the bottleneck of it and perform the computations as side effects.

State Size

Can huge state bring memory issues?

What we keep in state is plain data structs without any images or videos. Furthermore, the state avoids keeping data duplicates by normalization.

Frankly, I can hardly imagine the size of an app, whose plain data struct state will bring memory-related troubles on modern iOS devices.

However, if the state gets very large in size it may also have an overall negative performance impact so we should be ready to cut it.

Luckily our hands are not tied and we can invent optimisations for it. For example:

  • We can clean up state from fatty parts and those things that we often fetch from server. So we can treat a part of the state as a cache sort of things.
  • We can implement database storage for entities and perform fetch as side effect.

In my opinion, one of the most strong points of architecture built around the state is that it allows to implement and adopt significant upgrades on demand and avoid premature optimizations.

Caveats of Loose Coupling

Loosely coupled components bring not just a lot of freedom, but also great responsibility.

No matter how exactly do you handle side effects (middlewares, async actions or state-driven) upper layers of the app don't have dependency on side effects implementation.

On the one hand it allows to code UI and presentation layer, state and reducer completely independently from side effects.

On another hand, that means that you can simply miss or remove some side effect implementation and complier won't even remind you about it. You may forget to intercept and execute the right AsyncAction in your middleware easily.

This kind errors is easy to miss as the app doesn't crash or alert an error, it just doesn't perform network request when expected and keep quiet about it. It may be a big problem especially for big apps.

How to solve? Well, It's probably only Feedback-Loops-driven side effects that don't have such problem. For all the rest it can be only covered by tests I think.

State-Driven App as FSM

Using UDF redux-like architecture implies coding the app logiс as a finite state machine.

At first it seems very unusual. Then it turns into fun. Anyway that's rather different from common imperative programming and requires some effort and time to build those new neural pathways in our brain.

FSM is a trap

The main disappointment is when you got used to coding iOS app as FSM, you don't want to come back to imperative programming any more.

Redux in iOS

iOS developers fall in love only with abbreviation architecture design patterns.

So Redux won't become popular among us until we don't have a worthy decoding for its letters.

In my opinion Redux has a lot of undeniable advantages and cool features that easily outshine its cons, on small- and mid-sized projects. As I've already mentioned, its disadvantages on large scale apps are not dead-ends and usually have possible solutions.

Perhaps SwiftUI will make Redux more popular if the fashion doesn't drift to MVVM.

Conclusion

Instead of conclusion, everyone can now run through the famous iOS architecture design checklist and decide if Redux-like architecture fits his beliefs:

Is it scalable?

  • Does it handle changing business requirements efficiently?
  • Does it support gradual adoption in existing projects?
  • Does it allow replacing third-party components easily?
  • Does is encourage composition of small independent components?
  • Does it allow adding UI to features later?
  • Does it support breaking of retain cycles on integration rather than component level?
  • Does it support people working simultaneously on different parts of a feature?
  • Does it limit the use of event/notification systems to specific layers?
  • Does it provide a strategy for cutting corners and tech debt?

Is it maintainable?

  • Does it provide a tactic for fighting massive objects?
  • Does it limit the number of dependencies for an object?
  • Does it try to reduce the amount of boilerplate code?
  • Does it minimize the number of different roles of components?
  • Does it suggest project structure tolerant to changes?
  • Does it feel right?

Is it prepared for navigation?

  • Does it explain how to pass data between components?
  • Does it allow describing and discovering app flow naturally?
  • Does it avoid transient dependencies?
  • Does it define how to store state?
  • Does it minimize the amount of global state?
  • Does it allow opening a stack of screens?
  • Does it handle opening of a screen from a push notification?

Is it promoting quality?

  • Does it maximize the amount of testable code?
  • Does it encourages compile-time decisions over runtime decisions?
  • Does it avoid force casting and unwrapping?
  • Does it allow stubbing asynchronous code to run tests synchronously?
  • Does it support testing of UI?
  • Does it forgive mistakes?

Where to go from here?

I've made my own Redux Architecture library for iOS which is called Puredux

Here is a list of most famous iOS UDF libraries for inspiration: