Pragmatic Reactive Programming

After working extensively with ReactiveSwift (formerly known as ReactiveCocoa) and RxSwift for the last year and a half, hopefully I can shed some light on what works when it comes to getting down and dirty with real projects.

Some jargon to help get started, feel free to skip this:

  • Observable (RxSwift): this is a stream of values. It may or may not give you a value immediately on subscription, and it might or might not be hot or cold. It’s a bit vague.
  • Signal (ReactiveSwift): This is a ‘hot’ stream of values. This means that subscription doesn’t affect the stream. Imagine a car with the engine already running (hot) before you get in (subscribe).
  • SignalProducer (ReactiveSwift): This is a ‘cold’ stream. This means subscribing affects the stream. Typically this means that when you subscribe, it starts the thing that you’re observing, e.g. a network request. Imagine a car with a cold engine that starts up when you get in.
  • Variable (both Rx and Reactive): A handy bridge between the reactive and imperative worlds. Basically it's a wrapper around a plain old variable, and lets all subscribers know whenever the variable changes. It also lets subscribers know of the current value immediately upon subscription. If you're overusing this, it may be considered a code smell. Having said that, pragmatically, you should use it judiciously to get things done - it's unrealistic to make your codebase 100% reactive when you're dealing with UIKit.
  • BehaviorSubject (Rx): RxSwift 4 will be deprecating Variable because they think it is a sop to imperative programming - you must use BehaviorSubject instead at some point going forwards.

Alright, let’s jump right in!

Observables/Signals vs Variables

Oftentimes, when facing a problem to solve, the question will be: to create an Observable or Variable? The observable will be more pure-functional, which is laudable. And a variable introduces extra state, which is something we wish to minimise. Rather than having too much state, it is better to have a minimal ‘source of truth’ state, and pure-functional observables derived from said source of truth. This way, you cannot fall into the trap of updating, say, your user account in one class but forgetting to update it in another class.

State

To this end, I recommend taking a leaf out of Redux’s book, and follow the philosophy of having a single source of truth. For instance, I recommend one app singleton named State, which in turn contains an instance of each category of state required, such as User for login/logout state. Since, by nature, state is required to be stored here, I recommend using a Variable. Accessing would then look like so: State.shared.user.state.

Example of state:

class State {
	static let shared = State()
	let user = UserState()
}

class UserState {
	let isLoggedIn = Variable<Bool>(false)
	let name = Variable<String?>(nil)
}

Alternatives proposed include having your individual state objects (such as UserState above) be individual singletons in their own right. However the approach in the example above has one advantage: it exposes a single convenient chokepoint at which you can substitute your mocks for testing: State.shared.

View Models

Many ViewControllers will do well to be backed by a view model. The view model is responsible for deriving a subset of the application’s state in a way that is simpler to apply to the appropriate screen. For instance, say you have a screen where the current user state is to be displayed in the navigation bar title: the view model is responsible for combining the State.shared.user.isLoggedIn and the user.name variables into one title observable. I recommend that these view models be structs, and contain only observables that are derived from the state singletons’ variables.

Example of a view model:

struct UserDetailsViewModel {
	let title: Observable<String> = Observable
		.combineLatest(
			State.shared.user.isLoggedIn,
			State.shared.user.name)
		.map({ (isLoggedIn, name) -> String in
			if isLoggedIn {
				return name
			} else {
				return "Logged out"
			}
		})
}

Wherever reasonable: Use Variable for a source of truth, and Observable/Signal for a derived truth. Where ‘source of truth’ means your state singleton, and ‘derived truth’ means your view models.

On the other hand: Sometimes it is simply unworkable to have only observables in your view models. Sometimes you just need the value ‘now’ rather than binding it and trusting that it will be there later. Sometimes it just isn’t practical to refactor your app to be Rx the whole way through. In these cases, my team has agreed on a pragmatic compromise: your view model will create pure-functional observables for all its fields, and then bind that observable to a Variable, and expose the Variable. As part of this, you’ll want to change to using a class, so variable state and disposal is easier to reason about. For example:

class UserDetailsViewModel {
	let title = Variable<String?>()
	let disposeBag = DisposeBag()
	
	init() {
	 Observable
		.combineLatest(
			State.shared.user.isLoggedIn,
			State.shared.user.name)
		.map({...})
		.bind(to: title)
		.disposed(by: disposeBag)
	}
}

View Controllers

In your view controller, I recommend using bind to connect your view models to your fields. For instance:

class EditUserViewController: UIViewController {
	@IBOutlet var name: UITextField
	
	let disposeBag = DisposeBag()
	
	let viewModel: EditUserViewModel!
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
		viewModel.name
			.bind(to: name.rx.text)
			.disposed(by: disposeBag)
	}
}

This will accomplish one direction of data flow: from the state model to the UI. For the reverse direction, e.g. when the user enters a new name and taps ‘save’, I recommend building a series of ‘State Services’ for persisting those back to the state singleton. Feel free to think of a better name than ‘state service’, by the way. For instance:

struct UserStateService {
	static func save(name: String) {
		State.shared.user.name.value = name
	}
}

This way, your state services are the only layer responsible for applying business logic and directly manipulating your state, keeping such code away from view controllers, which we’d like to keep as simple as possible:

extension EditUserViewController {
	func tapSave() {
		UserStateService.save(name.text)
	}
}

Closing thoughts

A grab-bag of ideas that I couldn’t figure where to fit above:

  • Bind things wherever possible, don’t use imperative code. For instance, subscribing to a view model and using onNext to update a field feels very imperative. Rather, bind the view model to your fields. You want the code to read like ‘this is what I want it to do’ rather than ‘this is how I’d like it done’.
  • Use combineLatest upfront to grab everything you need for a given field in a view model, rather than referring to State.shared.foo.x in a map statement. Otherwise you run the risk that a change to X will not update your fields correctly.

Best of luck!

Thanks for reading! And if you want to get in touch, I'd love to hear from you: chris.hulbert at gmail.

Chris Hulbert

(Comp Sci, Hons - UTS)

Software Developer (Freelancer / Contractor) in Australia.

I have worked at places such as Google, Cochlear, Assembly Payments, News Corp, Fox Sports, NineMSN, FetchTV, Coles, Woolworths, Trust Bank, and Westpac, among others. If you're looking for help developing an iOS app, drop me a line!

Get in touch:
[email protected]
github.com/chrishulbert
linkedin



 Subscribe via RSS