Integrating RxSwift Into Your Brain and Code Base

So you have recently learned about RxSwift and RxCocoa. You have read some tutorials and maybe even a book or two. You have a program already in existence or maybe you are starting a new app but you can’t afford to do too much experimentation. Where do you go from here?

I have tutored lots of people in RxSwift over the years and it has been my experience that if they try to go “full Rx” right away they will end up with a mess of code that is harder to understand than anything they have ever written. Because of this, I recommend integrating Rx into your code slowly and with intention.

In order to effectively integrate Rx into your app you first have to identify two kinds of functions: pull type and push type. The first type of function returns values when called and is used to “pull” or get information from objects. The second type takes values as parameters and doesn’t return anything and is used to “push” or set information into objects. (Watch out, there are also functions that do both.) Rx strongly favors push type functions, so if your style also favors push functions, then the integration will be more effective.

If the code base you are starting with favors a push style, then you will find it much easier to integrate Rx and will be able to do it more fully. Fortunately, Apple’s frameworks also favor push style code, so that should help reduce the friction.

For this post, I assume you have already read some tutorials and understand what an Observable, Observer and Subject are. I also assume that you know at least how merge, map, flatMap and filter work and are comfortable looking up other operators as you go along.

There are seven different systems used in iOS code to push data from one object to another:

  • closures
  • target/action (UIControl aka IBAction)
  • delegates
  • notifications (NotificationCenter)
  • KVO
  • setting variables
  • sub-classing abstract base classes

A lot of the complexity of writing an application comes from trying to integrate these various systems into a unified whole. Each of the systems individually is simple to use, but weaving them together makes our code complex. One of the benefits of using RxSwift is that it wraps all of the above systems into a single powerful mechanism, thus making our code less complex overall.

Knowing the above gives a clue on how to go about integrating RxSwift into an existing project. If RxSwift can replace all those other technologies, then to integrate means to replace them with it. Once this is done, we will be able to make the code more declarative and less complex.

For this article, I’ve decided to use an old sample from Apple, MultipeerGroupChat. This code was originally written for iOS 7 and Objective-C but that won’t do for our purposes so I updated the code to Swift.

If you look at the first commit of the sample code in my GitHub repo, you will find a rather straightforward reimplementation of Apple’s sample code. There are some quirks about the code (not the least of which stems from the fact that all phones were the same width back then,) but our job isn’t to fix those. Rather our goal will be to integrate RxSwift into the program and along the way see how it gets transformed.

Each of this article’s section titles corresponds to a commit in the repository so it’s easy to follow along in the code. Be sure to use the code analysis tools in GitHub or Xcode to compare before-and-after changes. If you aren’t doing this, you won’t be able to understand what’s going on.

I picked this particular sample because it has a nice mix of target/action, delegates, notifications and even a sprinkling of KVO so there’s a lot to work with. Let’s get started, shall we?

Replacing IBActions

  1. Make sure that the input that triggers the action is an IBOutlet in the view.
  2. Make sure that RxSwift and RxCocoa have been imported into the view’s file.
  3. Make sure the view has a DisposeBag
  4. In the view’s initializer (for a UIView this would be the awakeFromNib method, for a UIViewController it would be the viewDidLoad method) subscribe to an Observable on the input that just calls the action.

As an example of this, in the MainViewController of our sample, there is an @IBAction called browseForPeers(_:). We see that the button that triggers this action isn’t an IBOutlet so first we do that, then we put this in the viewDidLoad:

browseForPeersButton.rx.tap
.bind(onNext: { [weak self] in self?.browseForPeers() })
.disposed(by: disposeBag)

In this case, the browseForPeers method takes a sender which we don’t need so we update the function signature from @IBAction func browseForPeers(_ sender: Any) to simply func browseForPeers().

Replacing Delegates: Removing App Specific Protocols

We will start with removing the special purpose delegates. The basic process is again pretty mechanical.

  1. Create a private PublishSubject for each function in the delegate protocol.
  2. Create a computed property that returns each of the publish subjects as an observable.
  3. Remove the delegate property and replace calls to it to onNext calls to the correct publish subject.
  4. In the object that conforms to the delegate, remove the protocol conformance and replace with binders to each observable created in step 2.

A simple example is the SettingViewControllerDelegate which only has one method. The purpose of the protocol is to push out the new displayName and serviceType strings so our observable will want to do likewise.

We create the new didCreateChatRoom Observable:

var didCreateChatRoom: Observable<(displayName: String, serviceType: String)> {
return _didCreateChatRoom.asObservable()
}
private let _didCreateChatRoom = PublishSubject<(displayName: String, serviceType: String)>()

And then call its onNext function, instead of the delegate’s method, which will allow us to do away with the protocol. Meanwhile in the MainViewController, we bind to the new observable and remove the protocol conformance. You will notice that when I removed the conformance, I didn’t actually remove the function. Instead I just changed its signature so it no longer accepts a SettingsViewController and called that function from within the bind closure.

I need to make a special mention of the ProgressObserver's delegate. It contains three functions (changed, canceled, completed) and the semantics of them went well with those of an observable event (next, error, completed) so instead of making three observables, I just went with one.

Replacing Delegates: Wrapping iOS provided protocols

  1. Remove the protocol conformance tag from the class/extension.
  2. At the point where the delegate was getting set, replace that setter with the needed subscriptions to the various observables.

For example, getting data from a UIImagePickerController requires that we add the rx.didCancel and rx.didFinishPickingMediaWithInfo binders.

imagePicker.rx.didCancel
.bind(onNext: { [weak self] in
self?.imagePickerControllerDidCancel()
})
.disposed(by: disposeBag)
imagePicker.rx.didFinishPickingMediaWithInfo
.bind(onNext: { [weak self] info in
self?.didFinishPickingMediaWithInfo(info)
})
.disposed(by: disposeBag)

Another special mention needs to be made here, this time about UITextFieldDelegate. This particular delegate is often used incorrectly (even in Apple sample code) and can almost always be replaced by IBActions. That is the case here and since we already covered IBActions, I have simply made the replacement directly to observables.

Replacing Notification Observers

Fixing a photo capture bug

extension Reactive where Base: PHPhotoLibrary {
static var requestAuthorization: Observable<PHAuthorizationStatus> {
return Observable.create { observer in
Base.requestAuthorization { status in
observer.onNext(status)
observer.onCompleted()
}
return Disposables.create()
}
}
}

Replacing KVO

Interlude

Now that we have most of the code converted to using RxSwift, we can start removing a lot of it and tease out the logic for testing. When you are ready, check out Part 2.

I started programming as a hobby in the late ’70s and professionally in the late ’90s. I am the director of development at Haneke Design.