DEV Community

loading...
Cover image for RxSwift - easy, yet not obvious, tips

RxSwift - easy, yet not obvious, tips

noemi_rozpara profile image Noemi Rozpara ・6 min read

Last few months were for me my personal RxSwift bootcamp. I decided to refactor my app and rewrite a few big protocols to observables. This helped me to view Rx with a fresh eye and catch a few problems. Those might not be obvious for people who do presentations or boot camps or are familiar with this technology for a long time. Some - or all - of the points might be silly for you. But if I will save just one person from being confused, it was worth it! So without a further ado, let’s jump to the list.

Rx is just an array + time

I think this is the most important thing to understand with all your heart. All subjects, relays and operators might seem scary, especially with Rx-hermetic jargon. But simplifying it in your head to a simple collection will help you to understand all concepts easily. For me it was a sudden click. If you want to learn more, official docs are great source and emphasize this concept: https://github.com/ReactiveX/RxSwift/blob/main/Documentation/GettingStarted.md#observables-aka-sequences

Oh, those memories

RxSwift syntax is elegant. But sometimes it makes it harder to spot the moment, when you create strong reference cycles. Also, this issue is ignored by many learning resources. Authors are trying to maintain snippets as clean as possible. It took me a while to get to that by myself.

Problem: we have a strong reference to self inside a callback. And self.disposeBag holds the strong reference to the callback. Finally it’s not getting cleaned up forever.

How to solve it? Always include [weak self] or [unowned self] when you are passing a lambda to subscribe, drive or bind, which includes any reference of self inside. Be cautious for implicit self calls! You don’t need to do that, when you are binding rx property.

Few examples:

No need to do anything for driving or binding to rx attributes:

@IBOutlet var statusLabel: UILabel!

myModel.statusLabel
  .drive(statusLabel.rx.text)
  .disposed(by: disposeBag)
Enter fullscreen mode Exit fullscreen mode

No need to do anything if callback doesn’t include self or it’s weak reference already:

myModel.statusLabel
  .subscribe(onNext: { newLabel in
    print(newLabel)
  })
  .disposed(by: disposeBag)
Enter fullscreen mode Exit fullscreen mode
@IBOutlet weak var statusLabel: UILabel!

myModel.statusLabel
  .subscribe(onNext: { newLabel in
    statusLabel.text = newLabel
  })
  .disposed(by: disposeBag)
Enter fullscreen mode Exit fullscreen mode

Use weak or unowned self when callback includes self:

myModel.statusLabel
  .subscribe(onNext: { [weak self] newLabel in
    guard let self = self else { return }
    self.saveNewLabelSomewhere(value: newLabel)
  })
  .disposed(by: disposeBag)

func saveNewLabelSomewhere(value: String) {  }
Enter fullscreen mode Exit fullscreen mode

Note: you might want to use simplified, pretty syntax:

myModel.statusLabel
  .subscribe(onNext: saveNewLabelSomewhere })
  .disposed(by: disposeBag)
Enter fullscreen mode Exit fullscreen mode

… but that would create the same problem.

Be careful with ViewController lifecycle

What time for binding is the best?

In most tutorials I have seen an example of doing all ViewController setup in ViewDidLoad. It’s comfortable, you do it once, when ViewController is removed from the memory, it will clean up all resources, that’s it. But in reality I noticed 2 things:

  1. Users tend to leave your app running in the background,
  2. Usually you don’t need to compute anything on screens, which are not visible at the moment.

Those observations led me to the conclusion that a much better option is to do all bindings in ViewWillAppear and to clean the disposeBag in ViewWillDisappear. That way you only use resources for what’s needed on the screen, and you explicitly know when the subscription is finished. It helped me to follow the subscription flow in the controllers.

Subscriptions can be accidentally duplicated

Sometimes you will need to subscribe more often than once for the screen - like in viewDidLayoutSubviews. In such a case watch out to not to leave zombie subscriptions behind! Example:

override func viewDidLayoutSubviews() {
  model.uiRelatedStuff
  .subscribe(onNext: {...})
  .disposed(by: disposeBag)
}
Enter fullscreen mode Exit fullscreen mode

What happened is viewDidLayoutSubviews probably was called a few times on screen load. Maybe another few times after the user rotated the screen. But the disposeBag is still alive! The effect is we have few copies of the same subscription. You can expect some bizarre side effects, like duplicated API calls.

Long story short: make sure you subscribe once for every time you need to, it’s not obvious.

Don’t reinvent the wheel

RxSwift library provides us with many special types, or traits. You need a warranty that call will happen from the main thread? You need to share effects? Before you write another few lines of code, check first if you could use a special type created for that purpose! I won’t be describing all of them here, check the docs:
https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Traits.md
https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Subjects.md

On the other hand, don’t use BehaviorSubject everywhere now - make sure a simpler type wouldn’t do. Sometimes less is more :)

Yes, you should test that, too

I must admit: I hate writing tests. I do it obviously, but I usually procrastinate it or hope someone will make it. But finally I found out testing Rx is simpler than I thought!

The easiest way to think of it for me is: you test the Rx element by observing like you would do normally. You just use fake the subscriber. And what you are actually testing is a collection of events emitted (value + time).

The example:

import XCTest
import RxSwift
import RxTest
@testable import MyFancyApp

class PLPCoordinatorContentModelTests: XCTestCase {

var scheduler: TestScheduler!
var disposeBag: DisposeBag!

override func setUp() {
scheduler = TestScheduler(initialClock: 0)
  disposeBag = DisposeBag()
  super.tearDown()
}

override func tearDown() {
  // simply reassigning new DisposeBag does the job 
  // as the old one is cleaned up
  disposeBag = DisposeBag()
  super.tearDown()
}

func testStatusLabelObservable() {
  let model = MyViewModel()

  // create observer of the expected events value type
  let testableLabelObserver = scheduler.createObserver(String.self) 

  model.titleObservable.subscribe(onNext: { (newStatus) in
  // just pass the value to the fake observer
    testableTitleObserver.onNext(title)
  }).disposed(by: disposeBag)

  // schedule action which would normally trigger the observable 
  // (see note below the snippet)
  scheduler.scheduleAt(10) {
    model.searchFor(query: Kittens)
  }
  // more events if you need, then
  scheduler.start()

  // define what you expect to happen
  let expectedEvents: [Recorded<Event<String>>] = [
  .next(0, "Inital Label Value"),
  .next(10, "Found Some Kittens!")
  ]

  // and check!
  XCTAssertEqual(subscriber.events, expectedEvents)
  }
}
Enter fullscreen mode Exit fullscreen mode

Note about scheduling events: you also can do this in bulk if you need more events than one:

scheduler.createColdObservable(
  [
    .next(10, Kittens),
    .next(20, nil),
    .next(30, Puppies)
  ]
)
.bind(to: model.query)
.disposed(by: disposeBag)

scheduler.start()

let expectedEvents: [Recorded<Event<String>>] = [
  .next(0, "Inital Label Value"),
  .next(10, "Found Some Kittens!")
  .next(20, "Search cleared")
  .next(30, "Found Some Puppies!")
]

Enter fullscreen mode Exit fullscreen mode

That’s it! You could describe flow above as:

  1. Create test scheduler which will handle events
  2. Create fake observable
  3. Subscribe fake observable to actual value you want to test
  4. Schedule some events
  5. Run scheduler
  6. Compare produced events to the expected ones

Worth noting - scheduler works on virtual time, which means all operations will be executed right away. The values provided (like 10, 20 in examples above) are used to check the order and match to clock “ticks”.

Brilliant resources about testing RxSwift are here:
https://www.raywenderlich.com/7408-testing-your-rxswift-code
https://www.raywenderlich.com/books/rxswift-reactive-programming-with-swift/v4.0/chapters/16-testing-with-rxtest#toc-chapter-020

Rx all the things!

… not. When you will feel comfortable with reactive concepts, you might feel an urge to refactor all the code to Rx. Of course, it’s nice and tidy. But it has few downsides: it’s hard to debug and you need to think about the time and memory. Also - equally important - not everyone is comfortable with that. So if your team is not ready, then it might be a good idea to hold on.

When and where to use it then? There are no silver bullets. If you have a simple component, like a TableView cell, then observables can be an overkill. But if you have a screen on which every time status changes you need to manually update 10 UI elements, then observing can be a charm. Also you can always apply RxSwift to one module of your app and decide upon further development.

After all, doing those small decisions and maneuvering between endless technical solutions is the most difficult and beautiful part of a developer's job.

Summary

I’m definitely not an RxSwift expert. Also I didn’t know or use reactive programming before RxSwift. It felt overwhelming at the beginning, but after catching a few basic concepts I liked it. I think points in this article are universal enough to be applied to Combine as well.

I hope the list above is helpful to some of you. Let me know if I made any mistake! Also can you think of some other non-obvious RxSwift gotchas? Leave that in comment below :)

Discussion (0)

pic
Editor guide