RxSwift Safety Manual šŸ“š

RxSwift gives you a lot of useful tools to make your coding more pleasurable, but it also can bring you a lot of headaches andā€¦ bugs šŸ˜± After three months of using it actively I think I can give you some tips to avoid the problems I encountered.

Side Effects

Side Effect in computer science may be hard to understand, because itā€™s a very broad term. I think this Stackoverflow thread does a good job of explaining it.

So basically, a function/closure/ā€¦ is said to have a side effect if it changes the state of the app somewhere.

In the context of RxSwift:

var counter = 1

let observable = Observable<Int>.create { (observer) -> Disposable in
   // No side effects
   observer.onNext(1)
   return Disposables.create()
}

let observableWithSideEffect = Observable<Int>.create { (observer) -> Disposable in
   // There's the side effect: it changes something, somewhere (the counter)
   counter = counter + 1
   observer.onNext(counter)
   return Disposables.create()
}

Why is that important in RxSwift? Because a coldā„ļø signal (like the one we just created) starts new work every time itā€™s observed !

Letā€™s observe on observableWithSideEffect twice:

 observableWithSideEffect
     .subscribe(onNext: { (counter) in
         print(counter)
     })
 .addDisposableTo(disposeBag)
 
 observableWithSideEffect
     .subscribe(onNext: { (counter) in
         print(counter)
     })
 .addDisposableTo(disposeBag)

We would expect it to print 2 and 2, right? Wrong. It prints 2, 3, because each subscription creates a separate execution, so the code inside the closure is executed twice and the side effect(counter incrementation) happens twice.

It means that if you put a network request inside, it would execute two times!

How do we fix this? By turning the cold observable into a hot one šŸ’”! We do this using publish, connect/refCount operators. Hereā€™s a whole tutorial explaining this in detail.

let observableWithSideEffect = Observable<Int>.create { (observer) -> Disposable in
 counter = counter + 1
 observer.onNext(counter)
 return Disposables.create()
}.publish()
// publish returns an observable with a shared subscription(hot).
// It's not active yet

observableWithSideEffect
 .subscribe(onNext: { (counter) in
     print(counter)
 })
.addDisposableTo(disposeBag)

observableWithSideEffect
 .subscribe(onNext: { (counter) in
     print(counter)
 })
.addDisposableTo(disposeBag)

// connect starts the hot observable
observableWithSideEffect.connect()
.addDisposableTo(disposeBag)

Itā€™ll print 2, 2.

Most of time time itā€™s enough if you just use the more high level shareReplay operator. It uses the refCount operator and replay. refCount is like connect, but itā€™s managed automatically - it stars when thereā€™s at least one observer. replay is useful to emit some elements to observers ā€œlate to the partyā€.

let observableWithSideEffect = Observable<Int>.create { (observer) -> Disposable in
 counter = counter + 1
 observer.onNext(counter)
 return Disposables.create()
}.shareReplay(1)

observableWithSideEffect
 .subscribe(onNext: { (counter) in
     print(counter)
 })
.addDisposableTo(disposeBag)

observableWithSideEffect
 .subscribe(onNext: { (counter) in
     print(counter)
 })
.addDisposableTo(disposeBag)

Main Queue

When observing in the view controller and updating the UI you never know on which thread/queue does the subscription happen. You always have to make sure itā€™s happening on the main queue, if youā€™re updating UI.

observableWithSideEffect
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { (counter) in
        print(counter)
    })
.addDisposableTo(disposeBag)

Error Events

If you have a stream of observables bound together and thereā€™s an error event on the far end all the observables will stop observing! If on the first end thereā€™s UI, it will just stop responding. You have to carefully plan your API and think of whatā€™s going to happen when completed and error events are passed in your observables.

viewModel
.importantText
.bindTo(myImportantLabel.rx_text)
.addDisposableTo(disposeBag)

If importantText sends an error event for some reason, the binding/subscription will be disposed.

If you want to prevent that from happening you use catchErrorJustReturn

viewModel
.importantText
.catchErrorJustReturn("default text")
.bindTo(myImportantLabel.rx_text)
.addDisposableTo(disposeBag)

Driver

Driver is an Observable with observeOn, catchErrorJustReturn and shareReplay operators already applied. If you want to expose a secure API in your view model itā€™s a good idea to always use a Driver!

viewModel
.importantText
.asDriver(onErrorJustReturn: 1)
.drive(myImportantLabel.rx_text)

Reference Cycles

Preventing memory leaks caused by reference cycles takes a lot of patience. When using any variable in the observe closure it gets captured as a strong reference.

//In a view controller:

viewModel
	.priceString
    .subscribe(onNext: { text in
       self.priceLabel.text = text
    })
    .addDisposableTo(self.disposeBag)

The view controller has a strong reference to the view model. And now, the view model has a strong reference to the view controller because weā€™re using self in the closure. Pretty much basic Swift stuff.

Hereā€™s a small tip:

viewModel
	.priceString
    .subscribe(onNext: { [unowned self] text in
       self.priceLabel.text = text
    })
    .addDisposableTo(self.disposeBag)

Use [unowned self] without worrying it might be dangerous when adding disposable to self.disposeBag. If self is nil, then the dispose bag is nil, so the closure will never be called when self is nil - app will never crash and you donā€™t have to deal with optionals šŸ¤—

self is not the only case you have to watch out for! You have to think of every variable that gets captured in the closure.

//Outside the view controller

viewModel
	.priceString
    .subscribe(onNext: { [weak viewController] text in
       viewController?.priceLabel.text = text
    })
    .addDisposableTo(self.disposeBag)

It might get very complex, thatā€™s why itā€™s a very good idea to keep closures short! If a closure is longer than 3-4 lines consider moving the logic to a separate method - youā€™ll see all the ā€œdependenciesā€ clearly and youā€™ll be able to decide what you want to capture weakly/strongly.

Managing your subscriptions

Remember to always clear the subscriptions you donā€™t need anymore. I had an issue where I didnā€™t clear my subscriptions, the cells were reused, new subscriptions were created each time causing some very spectacular bugs.

let reuseBag = DisposeBag()

// Called each time a cell is reused
func configureCell() {
  viewModel
  .subscribe(onNext: { [unowned self] (element) in
                self.sendOpenNewDetailsScreen()
            })
}


// Creating a new bag for each cell
override func prepareForReuse() {
  reuseBag = DisposeBag()
}

RxSwift is very complex, but if you set your own rules in the project and adhere to them, you should be fine šŸ˜‡ Itā€™s very important to be consistent in the RxSwift API youā€™re exposing for each layer - it helps with catching bugs.


Michal Ciurus

A passionate iOS dev always trying to get to the bottom of stuff.