Unidirectional Architectures over ReactiveSwift - Part I: Redux

Petro Korienev

Shared mutable state is the root of all evil. © Henrik Eichenhardt

Since Facebook presented Flux in 2014, and its successor / alternative Redux was released by Dan Abramov and Andrew Clark, there has been a lot of hype around unidirectional architectures in software development world. I’ve recently released an initial version of my implementation of Redux over ReactiveSwift. This article makes a short (not really) summary of reasons why it’s done and why I think it’s a bit over other Redux stuff already written for Swift.

What problem do I (as well as other libs I’ll mention below) try to solve? The answer is pretty simple and obvious — state management. Nowadays, apps have grown so large that demand in predictable state management led people to creation of such kind of predictable state containers as Redux.

 

Redux in general

Unlike server-side solutions, client-side apps need other approaches to reducing overall solution complexity. It doesn’t matter, which specific platform the client application targets — browser or mobile — it should perform a really complex task of processing a huge amount of events: user input, platform state changes, network events and updates. Usually, an app has plenty of unobvious data flows within it: fire network request upon user input, meanwhile store some transient state in app’s storage, parse response and make another write transaction to app’s storage, enqueue another network request, while the first one is in progress if the user changes his mind with the input… With going mobile, certain things become even more crazy, cause multithreading jumps into this game. Mobile developers need to worry about making UI updates only on the main thread, avoid concurrency when touching the database, avoid deadlocks, etc. Long story short, one does not simply shate mutable state.

Original Redux docs end up with three principles for predictable state management. They are:

  1. Single source of truth
  2. State is read-only
  3. Changes are made with pure functions

I will briefly go through Swift ports, which implement them:

ReSwift has been around for a while. It has something around 5k github stars and is up-to-date with all major Swift releases. Being ported closely from original JS implementation, this library adds few nasty Swift features, like generic state type, strong typing of subscribers, etc.

ReduxKit was also used widely, but has been deprecated in favor of ReSwift. There are also some less known implementations like thisthis and this, but they’re rather excerpts from people’s projects and are not well-maintained to be used as community-proven solutions.

Overall, they suggest the following app data flow (called “unidirectional” for obvious reasons):

ReSwift app scheme

ReSwift’s app scheme, source:

https://github.com/ReSwift/ReSwift/blob/master/Docs/img/reswift_concept.png

Here we see just a few details on implementation of the above principles:

  1. Store is a single source of truth for the app’s State
  2. State is readonly and observable by view when it’s subscribed to Store updates
  3. State is modified by pure functions called Reducer’s. Having previous state and an action emitted by view, they calculate a new app’s state. Encapsulation hides reducers from view, so state changes are isolated.

Having a few implementations around, why did I still come up with my own https://github.com/soxjke/Redux-ReactiveSwift? Here are several points why I think it’s still better:

  1. Simplicity. While ReSwift covers aspects of observing, subscription / unsubscription, events dispatch, thread safety, interfaces, and protocols, my solution utilizes power of ReactiveSwift and is represented more or less by a single Store class. All the above mentioned stuff is out of the box for me, because I use MutableProperty wrapped inside Store.
  2. Flexibility. My Redux implementation provides Store parametrized by generic State and Event types, with a little type extension for Defaultable type, allowing Store initialization without a default state if the state type provides a default value. ReactiveSwift’s PropertyProtocol and BindingTargetProvider protocols conformance enables to bind state / event streams with a simple <~ operator.
  3. Power and ease-of-use. With ReactiveSwift’s Signal and SignalProducer for State, it’s ready for simple subscribing as well as for complex map, filter, reduce, collect, and other operations to write a truly declarative code with the smell of FRP.
  4. Stricter requirements to State, no requirements to outer world. What do I mean by “stricter” — no optionals in either State or Event, no wrapping/unwrapping. What do I mean by outer world requirements— no protocol conformance needed to be subscripted to State changes. And yes, no protocols with associatedtype, so feel free to build any stores you like and create loosely-coupled solutions by using Dependency Injection (As a good example of DI framework I’ve used for Swift I can definitely name Swinject).

That’s pretty much it. Due to “no requirements to outer world”, this solution is pattern-agnostic, so it can be easily used as a model layer being a part of MVC, as a model and view-model layer as a part of MVVM, MVP, MV-Next-Big-Arch-Buzzword. The idea of a simple MVVM app (exactly like ReSwift example) can be found in Readme, I’ll get straight away to a more complex example.

 

Weather app using Redux

Such kind of an app is usually provided as a test task for candidates applying for junior iOS engineer position. They are asked to do a fetch of weather data for the current location using Gismeteo (or some other weather service) API. The weather forecast should be stored in a local storage. Usually, SQLite or CoreData are suggested as local storages, however it’s 2017, and apps don’t have memory limitations like those that were around for iPhone 3GS. There are strong alternatives, e.g. Realm, so we won’t strictly require any framework/implementation.

The next part of this article is mostly a tutorial of building this app using Redux and a couple of other tools, so if you want to skip it (danger zone: you might miss a few valuable insights) — here’s the link to the part of the article after the tutorial and here is the link to the full source code of the described example.

If you’re still here… let’s dive inside the tutorial!

Creating the project

This should look pretty similar to XCode templates. Let’s create a project called “Simple-Weather-App” and, for sure, check on Unit Tests checkmark.

Creating a project with Redux

Creating the project

For sure, we’ll use CocoaPods as a dependency manager (because I think, it’s the best dependency manager, Carthage and SPM aren’t that strong and widespread like pods).

Let’s go ahead and create Podfile in the root project directory. I usually use this template for multi-target projects and have nothing against you borrowing it for your own use :)

source 'https://github.com/CocoaPods/Specs.git'
use_frameworks!
inhibit_all_warnings!
    
def common_pods
    pod 'Redux-ReactiveSwift', '~> 0.1.1' # Our Redux library
    pod 'ReactiveCocoa', '~> 7.0.0-alpha1' # ReactiveCocoa has necessary UI bindings
    pod 'ObjectMapper', '3.0.0' # ObjectMapper is for fast from/to JSON transformations
end

def test_pods
    pod 'Quick', '1.2.0' # Quick is BDD-like Specs framework for XCode unit testing
    pod 'Nimble', '7.0.2' # Nimble is a set of matchers for XCode unit testing
end

def app_pods
    pod 'Alamofire', '4.5.1' # Alamofire is library we will use for network requests
    pod 'SnapKit', '4.0.0' # A Swift Autolayout DSL for iOS & OS X
end

test_targets = ['Simple-Weather-AppTests']
app_targets = ['Simple-Weather-App']

test_targets.each { |targetName|
    target targetName do
        common_pods
        test_pods
    end
}

app_targets.each { |targetName|
    target targetName do
        common_pods
        app_pods
    end
}

# Since we're integrating alpha version of ReactiveCocoa we tweak SWIFT_VERSION
# By the final 7.0 release in November there won't be any need in tweaking SWIFT_VERSION
post_install do |installer|
    installer.pods_project.targets.each do |target|
        if target.name == 'ReactiveCocoa'
            target.build_configurations.each do |config|
                config.build_settings['SWIFT_VERSION'] = '3.2'
            end
        end
    end
end

 

pod install --verbose in Terminal, close XCode project and open the newly created XCode workspace. We’re done with the most tricky part.

Designing our app’s UI

Let’s focus on which data we will show to the user: this should be — the date of the last weather update, the closest location, air temperature, real feel (if available), wind, rain, etc. This data can be fetched from AccuWeather API, which we select as a data source. Let’s show current conditions and forecasts for 5 day, switchable by segmented control. Data will be shown via a static table and has a horizontal pageable scroll. Let’s create a new file WeatherView.swift and WeatherView.xib (I don’t normally use IB and do layout in code, but for simplicity let’s use IB). Its layout is fairly simple:

Desiging weather app UI

WeatherView.xib

 

The screenshot shows the connected outlet, dataSource, and delegate for the table. I’ve also disabled safe area guides and trait variations, since this view is intended to be content-only and invariant relative to a size class. The table view needs cells to display content, let’s create some. We want a cell that will display our weather values, which can be in the following formats (information obtained by exploring AccuWeather APIs):

  • {Value} {Unit} // Single line of weather data, e.g. Air temperature: 68 F
  • {Value}-{Value} {Unit} // Range of data, e.g. Forecasted wind 10–12m/s
  • {Value} {Unit} / {Value} {Unit} // Day/night. Precipitation 25%/75%

Let’s go straight ahead and create a cell class with XIB and two labels: name and value. WeatherFeatureLabel.swift & WeatherFeatureLabel.xib, here we go:

Creating a cell class with XIB

WeatherFeatureCell.xib

 

Next, we’re going to add stub implementations to WeatherView in order to add table view cells and WeatherView on screen.

import UIKit

class WeatherView: UIView {
    private struct Const {
        static let cellIdentifier = "WeatherFeatureCell"
    }
    @IBOutlet private weak var tableView: UITableView!
    
    static func fromNib() -> WeatherView {
        guard let view = Bundle.main.loadNibNamed("WeatherView", owner: nil)?.first as? WeatherView else {
            fatalError("No bunlde for: \(String(describing: self))")
        }
        return view
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        tableView.register(UINib.init(nibName: Const.cellIdentifier, bundle: nil), forCellReuseIdentifier: Const.cellIdentifier)
    }
}

extension WeatherView: UITableViewDelegate {}

extension WeatherView: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 3
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return tableView.dequeueReusableCell(withIdentifier: Const.cellIdentifier, for: indexPath)
    }
}

WeatherView.swift

 

import UIKit
import SnapKit

class ViewController: UIViewController {
    @IBOutlet private weak var containerView: UIView!
    @IBOutlet private weak var segmentedControl: UISegmentedControl!
    @IBOutlet private weak var leftBarButtonItem: UIBarButtonItem!
    @IBOutlet private weak var rightBarButtonItem: UIBarButtonItem!
    private lazy var currentWeatherView: WeatherView = WeatherView.fromNib()
    override func viewDidLoad() {
        super.viewDidLoad()
        containerView.addSubview(currentWeatherView)
        currentWeatherView.snp.makeConstraints { (make) in make.edges.equalToSuperview() }
    }
}

ViewController.swift

 

Adding stub implementations and table view cells

Main.storyboard

 

Hit “Run” and you’ll be able to see the following UI stub on Simulator.

Simulator with UI stub

Simulator with UI stub

We’ve confirmed we have a very basic UI to display what we’d like to display, let’s now add some “meat” to the project.

Models

Let’s set some goals we’d like to achieve with our model layer:

  • It should be JSON parseable / serializable
  • It should be immutable (of course!)
  • It should be easy to display on UI
  • It should represent our actual domain area (weather)

Having this in mind, let’s get straight ahead to modelling. We will create Weather.swift and write some neat and swift (I hope!) code:

enum  WeatherValue<Value> {
    case single(value: Value)
    case minmax(min: Value, max: Value)
}

struct  WeatherFeature<T> {
    let unit: String
    let value: WeatherValue<T>
}

struct  DayNightWeather {
    let windSpeed: WeatherFeature<Double>
    let windDirection: String
    let precipitationProbability: Int
    let phrase: String
    let icon: Int
}

// Since Swift 3.1 there's a neat feature called "Type nesting with generics" is
// around, however implementation is buggy and leads to runtime error
// https://bugs.swift.org/browse/SR-4383
// As a workaround, WeatherValue, WeatherFeature, DayNightWeather are standalone types
struct  Weather {
    let effectiveDate: Date
    let temperature: WeatherFeature<Double>
    let realFeel: WeatherFeature<Double>
    let day: DayNightWeather
    let night: DayNightWeather
}

Weather.swift

 

We get the weather model with fields we would like to display. Now it’s time to get some weather JSON and start parsing. I won’t put full JSON here, since it’s pretty long, you can take a look at it here. I usually do Playground / Unit tests for parsing, so let’s go straight ahead and start our Unit tests! We will use Quick / Nimble over XCTest, since they allow writing more structured & human-readable unit tests. In terms of Quick, Test Suite is called “Spec”, so let’s create WeatherSpec.swift and start writing tests.

import Foundation
import Quick
import Nimble
@testable import Simple_Weather_App

class WeatherSpec: QuickSpec {
    override func spec() {
        let weatherJSON: [String: Any] = try! JSONSerialization.jsonObject(with: try! Data.init(contentsOf: Bundle.test.url(forResource: "Weather", withExtension: "json")!)) as! [String: Any]
        describe("parsing") {
            it("should parse weather") {
                let weather = try? Weather(JSON: weatherJSON)
                expect(weather).notTo(beNil())
            }
        }
    }
}

 

We haven’t implemented any mapping yet, so this file simply won’t compile. To make the mapping magic work, let’s utilize power of ObjectMapper. Get back to Weather.swift and implement ImmutableMappable conformance:

extension Weather: ImmutableMappable {
    init(map: Map) throws {
        effectiveDate = try map.value("EpochDate", using: DateTransform())
        temperature = try map.value("Temperature")
        realFeel = try map.value("RealFeelTemperature")
        day = try map.value("Day")
        night = try map.value("Night")
    }
    func mapping(map: Map) {
    }
}

extension WeatherFeature: ImmutableMappable {
    init(map: Map) throws {
        if let minimum: T = try? map.value("Minimum.Value"),
            let maximum: T = try? map.value("Maximum.Value") { // Min/max
            unit = try map.value("Minimum.Unit")
            value = .minmax(min: minimum, max: maximum)
        }
        else { // Single value
            unit = try map.value("Unit")
            value = .single(value: try map.value("Value"))
        }
            
    }
    func mapping(map: Map) {
    }
}

extension DayNightWeather: ImmutableMappable {
    init(map: Map) throws {
        windSpeed = try map.value("Wind.Speed")
        windDirection = (try? map.value("Wind.Direction.Localized")) ?? ((try? map.value("Wind.Direction.English")) ?? "")
        precipitationProbability = try map.value("PrecipitationProbability")
        phrase = try map.value("LongPhrase")
        icon = try map.value("Icon")
    }
    func mapping(map: Map) {
    }
}

Weather.swift

Cmd+U… Executed 1 test, with 0 failures (0 unexpected) in 0.012 (0.016) seconds! (Honestly saying, I did Cmd+U with fails like 10 times, until I’ve fixed all typos in the implementation, that’s why tests are especially useful for cases like parsing. Now I’m: 1) sure that my model is parseable from sample JSON; 2) If something is changed, test will fail).

More tests

That’s probably the most boring stuff, but I’m adding at least positive cases to check all parsed fields to make sure that sample parsing is correct.

class WeatherSpec: QuickSpec {
    override func spec() {
        let weatherJSON: [String: Any] = try! JSONSerialization.jsonObject(with: try! Data.init(contentsOf: Bundle.test.url(forResource: "Weather", withExtension: "json")!)) as! [String: Any]
        describe("parsing") {
            let weather = try? Weather(JSON: weatherJSON)
            it("should parse weather") {
                expect(weather).notTo(beNil())
            }
            it("should parse precipitation probability") {
                expect(weather!.day.precipitationProbability).to(equal(12))
                expect(weather!.night.precipitationProbability).to(equal(1))
            }
            it("should parse time") {
                expect(weather!.effectiveDate.timeIntervalSince1970).to(beCloseTo(1508997600))
            }
            it("should parse temperature") {
                expect(weather!.temperature.unit).to(equal("F"))
                if case .minmax(let min, let max) = weather!.temperature.value {
                    expect(min).to(beCloseTo(71)) // note matcher beCloseTo used for Double
                    expect(max).to(beCloseTo(76)) // comparing Doubles by equality is incorrect
                } else {
                    fail("parsed temperature is not in minmax format")
                }
            }
            it("should parse RealFeel temperature") {
                expect(weather!.realFeel.unit).to(equal("F"))
                if case .minmax(let min, let max) = weather!.realFeel.value {
                    expect(min).to(beCloseTo(71))
                    expect(max).to(beCloseTo(78))
                } else {
                    fail("parsed RealFeel temperature is not in minmax format")
                }
            }
            it("should parse icon") {
                expect(weather!.day.icon).to(equal(4))
                expect(weather!.night.icon).to(equal(34))
            }
            it("should parse phrase") {
                expect(weather!.day.phrase).to(equal("Humid with sun through high clouds"))
                expect(weather!.night.phrase).to(equal("Mainly clear and humid"))
            }
            it("should parse wind direction") {
                expect(weather!.day.windDirection).to(equal("S"))
                expect(weather!.night.windDirection).to(equal("SSE"))
            }
            it("should parse wind speed") {
                expect(weather!.day.windSpeed.unit).to(equal("mi/h"))
                if case .single(let value) = weather!.day.windSpeed.value {
                    expect(value).to(beCloseTo(11.5))
                } else {
                    fail("parsed day wind speed is not in single format")
                }
                expect(weather!.night.windSpeed.unit).to(equal("mi/h"))
                if case .single(let value) = weather!.night.windSpeed.value {
                    expect(value).to(beCloseTo(9.2))
                } else {
                    fail("parsed night wind speed is not in single format")
                }
            }
        }
    }
}

WeatherSpec.swift

Please note my extensive use of ! operator in unit tests code. Despite you’ve learned previously that force-unwrapping is very bad, I do it intentionally inside unit tests, because I want them:

  • To be simple and straightforward.
  • To fail much. They shouldn’t be error-prone like an app’s code.

Now let’s go ahead and add one more model, response, and spec to our app and tests — result of Geoposition search. We need this, because AccuWeather cannot provide us with weather data for particular GPS coordinates. Instead it provides weather data for the closest point we can find. Sample JSON is here, model and spec below.

See Geoposition.swift file for adding geopision model here and GeopositionSpec.swift file for adding geoposition spec here.

Take a look at our tests for country flag transform. Do you see how easy they are for testing? That’s because we’re testing pure functions here — functions that produce predictable output for given input and have no side effect. We’ll recall this concept again when we’ll be testing our reducers.

You can browse intermediate result here.

You may ask where's Redux and predictable app state in all that. Let's discuss it in Unidirectional architectures over ReactiveSwift  —  Part I: Redux Continued.