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.

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.

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

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:

We’re adding reactive extension weatherFeatures on WeatherView to bind an appropriate property from ViewModel. This gonna be implemented in ViewModel

viewModel.isLoading should be implemented as SignalProducer<Bool, NoError>to bind to activity indicator

isEnabled(for:) is a function returning SignalProducer. This is bound to isEnabled property of UISegmentedControl

The same for title, but String, not Bool

We’re creating a binding target for rightBarButtonItem and binding either the activity indicator or the refresh button, depending on the state

‍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

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

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

The store is responsible for managing states

Services are helper classes to deliver content. They communicate directly only to the store

ViewModel holds UIStore responsible for managing UI state. It shouldn’t be mixed with the app’s state.

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.

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).

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.

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 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

Thank you for reaching out to Sigma Software! Please fill the form below. Our team will contact you shortly.

Full Name *

E-mail *

Phone

Company

Message

Page url

I hereby confirm that I am familiar with
Sigma Privacy Policy and agree to the personal data provided by me being stored and processed in accordance with the Policy
*

Petro

Korienev

Senior Software Engineer / Tech Lead

Petro is Senior Software Engineer / Tech Lead with focus on iOS software development. He's working with iOS solutions for more than 6 years and he's a big fan of applying functional reactive programming principles to iOS applications. Petro is the leader of CocoaHeads Ukraine - the biggest Ukrainian community of iOS/macOS/Swift developers.