RxSwift - easy, yet not obvious, tips
Noemi Rozpara
Posted on January 21, 2021
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)
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)
@IBOutlet weak var statusLabel: UILabel!
myModel.statusLabel
.subscribe(onNext: { newLabel in
statusLabel.text = newLabel
})
.disposed(by: disposeBag)
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) { … }
Note: you might want to use simplified, pretty syntax:
myModel.statusLabel
.subscribe(onNext: saveNewLabelSomewhere })
.disposed(by: disposeBag)
… 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:
- Users tend to leave your app running in the background,
- 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)
}
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)
}
}
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!")
]
That’s it! You could describe flow above as:
- Create test scheduler which will handle events
- Create fake observable
- Subscribe fake observable to actual value you want to test
- Schedule some events
- Run scheduler
- 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 :)
Posted on January 21, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024
November 28, 2024