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

For our first step, we will start with something easy — replacing IBActions with observables. This is as simple as:

  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

This is a much larger topic and there are a lot of delegates in the code we are working with, so I will break it up into stages. We see two basic sorts of delegates in this code — those provided by libraries (for e.g., MCBrowserViewControllerDelegate) and those created custom for this app (e.g., SessionContainerDelegate.) For the first sort we are forced to use the DelegateProxy class provided by RxCocoa; for the latter sort I recommend doing away with them completely.

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

Sadly, creating wrappers using the DelegateProxy class provided by RxCocoa is outside of the scope of this article. I will have to give it its own treatment later. However, once you have them, replacing the protocol conformance with observables is something that we will cover. The steps are:

  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

There are a number of problems with the sample code surrounding keyboard display and dismissal. I assume that the bugs exist because of changes in how the keyboard notifications work. Rather than just a mechanical replacement, we are forced to do a rewrite of the moveToolBar function.

Fixing a photo capture bug

During routine testing, I noticed that the app was unable to capture and send photos. I realized the problem was two fold. First, the operating system now requires the app to explicitly ask for permission to access photos, and second, the UIImagePicker delegate proxy I cribbed from the RxSwift sample code is out of date. To fix these, I wrote my own UIImagePicker delegate proxy, included the required permission strings, and added the request authorization method. This later is a good example of how to wrap an OS function that requires a callback and returns Void.

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

This app uses KVO inside the ProgressObserver class to observe the Progress object. It observes two properties of the progress object, cancelled and completedUnitCount. Whenever either of them is updated, the appropriate bound observer will be called. You will see that now each observed key-value gets its own closure rather than having to route all of them through a single function and since the dispose bag takes care of cleanup for us, we can do away with the deinit function.


Interlude

Wow, that was a lot of work and a pretty big word count for some fairly mechanical code refactoring. I did some runs of the app throughout the refactoring but by and large I knew that behavior wasn’t changing so I wasn’t concerned. It would be nice to test the logic, but the logic was so interwoven with the effects and so much of the code was about translating from one observation system to another that, up to this point, writing the tests would have been very difficult.

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.