USA
Unidirectional Architectures over ReactiveSwift – Part I: Redux
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:
- Single source of truth
- State is read-only
- 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 this, this 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’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:
- Store is a single source of truth for the app’s State
- State is readonly and observable by view when it’s subscribed to Store updates
- 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:
- 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.
- 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.
- 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.
- 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 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:
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:
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
Main.storyboard
Hit “Run” and you’ll be able to see the following UI stub on Simulator.
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 II: Redux Continued.
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.
ADHD is a neurodevelopmental disorder that affects many people around the world and this includes symptoms such as inattention combined with hyperactivity and i...
At the Data + AI Summit 2024, Databricks revealed several key developments that are set to influence the future of data management and artificial intelligence. ...
In April 2024, Sigma Software’s CEO Valery Krasovsky and IdeaSoft’s CEO Andrii Lazorenko attended Dominion, a leading cryptocurrency conference organized by D3 ...