Petro Korienev

Unidirectional architectures over ReactiveSwift  —  Part I: Redux continued

In the article Unidirectional Architectures over ReactiveSwift - Part I: Redux, we have discussed state managements with predictable state containers like Redux and began building a simple weather app using Redux and a couple of other tools.

Now, we continue with adding what we’d like to have at the end, the predictable app state. We create AppState.swift, AppEvent.swift, and AppStore.swift. These 3 files encapsulate pretty much all of what our application state and events might be — with the rules of how to use it. Despite it should be unit-tested, I’ll use “test last” approach here just to show how easily services can get around here and how truly declarative app’s underlying functioning can be.

Let’s take a look at LocationService we can build upon this store in LocationService.swift.

This class has no single method or instruction what to do. It has no modifiable state, moreover, it has no public methods to modify anything. It works only as a pluggable middleware to AppStore, but nevertheless functions as a usual location service you might have implemented plenty of times before. The last service thing to go is WeatherService. See how to do it in WeatherService.swift.

I skipped the network requests code from the above gist for simplicity. You can take a look at full implementation on github. Last thing to do is to initialize these singletons at some point. Let’s do this on application launch.

Cmd+R… And we can see a very verbose log of each state change happened to the app — location permission requests, location fetches, network operations, parsing — finally coming to the displayable app state we might want to show to the user — fetched geoposition, fetched current conditions weather as well as 5-days forecast. We’ve just done a solid model layer without any single line of UI code. This step’s result can be found here.

 

Tests again

We shamefully forgot about tests in the above part, however, we should fix it now. We’ve got one problem with our current testing approach, there’s an AppDelegate initializing all the services upon launch. This shouldn’t be the case for unit tests, since we don’t want the app to ask for geolocation, perform network requests, etc. when running unit tests. We have to trick our setup a bit more, adding a different TestAppDelegate class for unit tests target and removing swift’s implicit UIApplicationMain.

import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        let _ = AppStore.shared
        let _ = LocationService.shared
        let _ = WeatherService.shared
        
        AppStore.shared.producer.logEvents().start()
        return true
    }
}

AppDelegate.swift

 

import UIKit

var appDelegateClassName =
    (nil != Bundle.allBundles.first { $0.bundlePath.contains(".xctest") }) ?
        NSStringFromClass(TestAppDelegate.self) :
        NSStringFromClass(AppDelegate.self)
let argv = UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to: UnsafeMutablePointer<Int8>.self,
                                                                      capacity: Int(CommandLine.argc))
UIApplicationMain(CommandLine.argc, argv, nil, appDelegateClassName)

main.swift

 

import UIKit

class TestAppDelegate: UIResponder, UIApplicationDelegate {
    
    var window: UIWindow?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        return true
    }
}

Test-aware app launch

 

Cmd+U… No store logs, only tests ones. You can browse intermediate results here.

 

Further tests

Let’s get to testing our app’s “real” business logic. It resides currently inside AppStore.swift and… is private. Doing testable import Simple_Weather_App doesn’t really help in this case, because this turns our to help only against internal methods. We can try to test the state changes themselves, however, it’s not such a good idea, because we can’t set an initial state for each test case (Redux principle #2 — state is read-only).

But what will happen if we end up changing reducers to be internal, not private? Does it break any encapsulation? The short answer is “no”. There’s always some trade-off between encapsulation and testing simplicity, however, this is not the case. Exposure of AppStore’s reducers into global scope (probably) increases the compile time for the module and it might become a problem upon horizontal scaling. But from the design prospective, making reducers not private or even moving them out of the AppStore's type scope will not make any difference. They’re pure functions.

Keeping this in mind, I’ll move functions away from the AppStore and focus on reducer testing. Posting full testing gist here would be overkill (because it’s 575 SLOC ), i’ll leave a link. Ease of coverage for pure functions is a gift — powerful but often overlooked.

When it comes to testing WeatherService and LocationService we face real troubles. In non-Objective-C world, mocking is a nightmare. Subjects-under-test should be written that way, so their initialization involves dependency injection in some way — constructor injection, property injection, etc. Let’s take a look at our current LocationService. It depends on AppStore(which is affordable, since it’s a “by-design foundation” for the app’s state). It also depends on a CLLocationManager(which is really unwanted, because now there’s no way to test LocationService, except providing a CLLocationManager instance from without class scope).

Needed effort doesn’t worth it. We end up full of sorrow, leaving our two services without unit tests.

 

Tie to UI

The last but not the least part of our weather app will be to display a thoroughly designed state in UI. I’ll try to show you how neat and expressive ReactiveCocoa might be when it comes to reducing UIViewController boilerplate. Let’s take a look at the desired design for our app. We want UI controls to show: content, loading state, switching current / forecast view for weather, update location, switch forecast days. Our storyboard now looks like this:

Screen in IB

Screen in IB

Connecting outlets is not interesting, let’s focus on ViewController.setupObserving. This method makes all UI controls “alive” by assigning properties / actions to them. See ViewController.swift.

Let’s go for them one-by-one:

  1. We’re adding reactive extension weatherFeatures on WeatherView to bind an appropriate property from ViewModel. This gonna be implemented in ViewModel
  2. viewModel.isLoading should be implemented as SignalProducer<Bool, NoError>to bind to activity indicator
  3. isEnabled(for:) is a function returning SignalProducer. This is bound to isEnabled property of UISegmentedControl
  4. The same for title, but String, not Bool
  5. We’re creating a binding target for rightBarButtonItem and binding either the activity indicator or the refresh button, depending on the state
  6. We’re wrapping actions for left, right, reload, locate buttons into CocoaAction and assigning them to reactive.pressed property. This automatically handles click events and isEnabled state.
  7. We’re adding a custom handler for UISegmentedControl.controlEvents signal, because it has no built-in action expression

Implementations in ViewModel look like this.

I skipped helper methods exporting data from model objects, because they’re pretty straightforward. The full example code can be found at: https://github.com/soxjke/Redux-ReactiveSwift/tree/0bd73cafd4263d93e8210b4847372220157eae70/Example/Simple-Weather-App.

 

‍Run

Upon Run on Simulator we would need to simulate location a few times (and hit the location button in the navigation bar) and get the following screen results:

Run on Simulator

Run

We can see that states are processing precisely — current, forecast, ability to hit left/right in the toolbar depending on the current page. The app built entirely on two stores (AppStore and UIStore) and their state combinations. So the rough structure can be depicted by the following chart:

App structure

App structure

 

As we can see, the app is built based on several simple responsibilities:

  1. The store is responsible for managing states
  2. Services are helper classes to deliver content. They communicate directly only to the store
  3. ViewModel holds UIStore responsible for managing UI state. It shouldn’t be mixed with the app’s state.
  4. ViewModel itself applies transformation to the app state to make it ready for use in UI. It provides actions responsible for events delivery back to stores.
  5. ViewController binds ViewModel’s actions and state signals to views.

On this chart, we see the following data flows:

  • State (and its transformations) is propagated from bottom to top
  • Events are propagated from top to bottom

They build an infinite loop with a single data flow, that’s why this architecture can be called unidirectional.

 

Responsiveness to changes

When we talk about architectures, we often judge them by one simple criteria — how easy it is to make changes to a ready solution. Let’s take a quick look on a few possible changes:

1. Make an expiration timeout for geolocation not 5 minutes, but 10 seconds — easy (1 line in reducer).

Expiration timeout for geolocation

2. Make navigation between forecast days by swipe / scroll and not (okay, not only) by toolbar buttons — simple, but not easy. We need to reconsider UI layer for display because one tableview won’t be enough to provide a smooth scrolling experience. The key complexity is UI layer — we have to reconsider ViewModel code to append additional UI actions, change content state delivery to ViewController and implement reactive UIScrollView behavior.

The solution is here: https://github.com/soxjke/Redux-ReactiveSwift/tree/ade4fd5839a3c9d9affab522838f043484036544/Example/Simple-Weather-App. This change has a bit of complexity, even by looking at the amount of changes:

Changes

3. Integrate hourly forecasts API: https://developer.accuweather.com/accuweather-forecast-api/apis

When it comes to changing a state, Redux becomes solid pain, because there is too much to change — state, models, reducers, view-models (in case of MVVM), ViewController.

 

Bonus tests

What else needs tests in a project? Let’s go for ViewModelSpec. You can find full-script here, I’ll describe the most vital parts in general:

  • We need stubs for success states (L16-L49). They will be used to setup stores for testing success logic.
  • We need a few Equatable extensions (L221–227). They will be needed to make convenient equal matchers.
  • We’re covering a few main parts of ViewModel's responsibility — testing controls’ enabled state producer (L53-L99), testing UIStore, which is the easiest part due to Redux Store nature (L100–L127), testing actions (L128-L164), testing the rest of SignalProducers (L165-L197).

A comprehensive description of reactive tests is out of scope of this article, however, you can pick up a few ideas and build your own testing strategy.

 

Summary

Thanks and where to go next

Thanks for reading down here, I hope you enjoyed and feel how to empower you project with a unidirectional data flow.

We’ve come up with a very basic Redux app, however, it covers aspects of network, loading state, state persistence, and restoration — things that are often overlooked in iOS development. Despite Redux is overall well, please remember that there’s no “sorcerer’s stone” or “silver bullet” in the software development world. It even doesn’t pretend to be — but if you’re struggling with the state management and related bugs, probably that’s high time to get some inspiration from here. I’ll put a link to a good disclaimer post from original Redux author Dan Abramov: You might not need Redux.

Also please don’t consider this example project as a “Bible” of how to do it right. After all, that’s only my vision. I’ve intentionally made simplifications with the project to focus on Redux itself, especially:

  • For a real-world project, I’d use the concept of phantom types for cell identifiers, move some work to UIView / UITableViewCell extensions to stay more clear & SOLID
  • I’d have constants for parsing key-paths instead of literals
  • I’d create a view layout and setup in code, because it gives a flexibility unlike when using IB. SnapKit is a brilliant DSL that maks this possible
  • I’d use a concept called Dependency Injection to avoid dependencies on network and stores. Swinject is what I’d use for it
  • I would use unit tests with no host application target for them. This requires a bit more sophisticated isolation and modularization, but gets slightly better results in terms of speed & upon scaling. The idea is described here
  • I would provide a slightly better test coverage — especially, specific error checks (Swift typed errors are very handy here)

Links

The example project is here: https://github.com/soxjke/Redux-ReactiveSwift/tree/master/Example/Simple-Weather-App

The lib itself is here: https://github.com/soxjke/Redux-ReactiveSwift

I recommend reading the whole http://redux.js.org and get familiarized with the concepts of reactive programming here.

Also, all the used libs in Podfile are battle-tested and if you’re not using something, it’s a good time to start.

Key insights

  • In your apps, try to benefit from state being immutable and readonly;
  • Pure functions are easy to test. Stateful instances are hard to test;
  • Pure functions are safe to be public/internal;
  • Redux is neither the shortest, nor thesimplest approach. But it’s testable and predictable.
Petro
Korienev
Subscribe for regular updates
By clicking Submit you agree to Sigma Software's Privacy Policy