
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. Allsubjects
,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 toself
inside a callback. Andself.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 tosubscribe
,drive
orbind
, which includes any reference ofself
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:
@IBOutletvarstatusLabel:UILabel!myModel.statusLabel.drive(statusLabel.rx.text).disposed(by:disposeBag)
No need to do anything if callback doesn’t includeself
or it’s weak reference already:
myModel.statusLabel.subscribe(onNext:{newLabelinprint(newLabel)}).disposed(by:disposeBag)
@IBOutletweakvarstatusLabel:UILabel!myModel.statusLabel.subscribe(onNext:{newLabelinstatusLabel.text=newLabel}).disposed(by:disposeBag)
Useweak
orunowned self
when callback includes self:
myModel.statusLabel.subscribe(onNext:{[weakself]newLabelinguardletself=selfelse{return}self.saveNewLabelSomewhere(value:newLabel)}).disposed(by:disposeBag)funcsaveNewLabelSomewhere(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 inViewDidLoad
. 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 inViewWillAppear
and to clean thedisposeBag
inViewWillDisappear
. 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 inviewDidLayoutSubviews
. In such a case watch out to not to leave zombie subscriptions behind! Example:
overridefuncviewDidLayoutSubviews(){model.uiRelatedStuff.subscribe(onNext:{...}).disposed(by:disposeBag)}
What happened isviewDidLayoutSubviews
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, ortraits
. 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 useBehaviorSubject
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:
importXCTestimportRxSwiftimportRxTest@testableimportMyFancyAppclassPLPCoordinatorContentModelTests:XCTestCase{varscheduler:TestScheduler!vardisposeBag:DisposeBag!overridefuncsetUp(){scheduler=TestScheduler(initialClock:0)disposeBag=DisposeBag()super.tearDown()}overridefunctearDown(){// simply reassigning new DisposeBag does the job// as the old one is cleaned updisposeBag=DisposeBag()super.tearDown()}functestStatusLabelObservable(){letmodel=MyViewModel()// create observer of the expected events value typelettestableLabelObserver=scheduler.createObserver(String.self)model.titleObservable.subscribe(onNext:{(newStatus)in// just pass the value to the fake observertestableTitleObserver.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, thenscheduler.start()// define what you expect to happenletexpectedEvents:[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()letexpectedEvents:[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 aTableView
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 :)
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse