Managing reactive cocoa observations on UITableViewCells
I was playing with Reactive Cocoa and Swift 2 lately and came upon common UITableViewCell
reuse problem that I could solve in very elegant way. I’ll describe the problem and my solution for it in this blog post.
The problem
In this particular example, I was using UITableView
on a modal UIViewController
. One of table view cells had a UITextField
on it in which user can enter some information. I wanted to use Reactive Cocoa to “listen” to text changed events so I could update controller properties with new values and use that for validating the input as well as return information for parent view controller.
My initial attempt was:
cell.textField.rac_textSignal().subscribeNext { _ in
self.enteredValue = cell.textField.text
}
All was well and working as expected. However I noticed couple smaller and one big issue with it:
rac_textSignal()
was fired once during cell was being populated during dequeue.- The signal was also fired when editing ended.
- There was a crash if controller was dismissed while text field was focused.
First two didn’t bother me much in this particular case - assigning values didn’t trigger any side effect, but last one was of course deal breaker.
The solution
After investigating for a while, I realized the source of crash was because text field stop editing event was fired after parent view controller was dismissed and disposed. Finally I came up with this solution that takes care of all above issues:
cell.textField.rac_textSignal()
.takeUntil(cell.rac_prepareForReuseSignal) // Ignore after cell gets reused
.distinctUntilChanged() // Only take value if different from previous one
.skip(1) // Skip cell dequeue event
.subscribeNext { _ in
self.enteredValue = cell.textField.text
}
I’m still taking rac_textSignal()
, but then pass it through couple “filters”:
- First, and the most important one as far as the crash goes, is
takeUntil()
. It effectively prevents signals from firing again after table view cell is reused. Turns out this filters out the end editing event occurring during view controller dismissal which was causing the crash. - Secondly, I wanted to filter out dequeue and editing start and end events; I started with
distinctUntilChanged()
which prevented editing start and end events and only passed actual text changes through. - But there was still 1 event fired while cell was being populated (even though signals were setup after initial values were set).
skip(1)
took care of that.
Conclusion
Reactive Cocoa is very simple to grasp, but takes the time to master. It’s one of those tools that require us to change the way you think about code, similarly to how auto layout changed our way of thinking of laying out user interface.
skip(1)
feels a bit on the “hacky” side, but it works… Besides, the main point is, I got exact behavior I wanted in a very expressive and understandable way and it was all declared inline, where events were being consumed!
Note for readers who already tried Reactive Cocoa in the past and discarded it for one or the other reason: this is not the first time I’m playing with Reactive Cocoa too - I tried and used it in the past, with Objective C. While v3 feels the same, it’s also different - it feels much more solid, and I’d encourage you to give it another go. 3.0 is still in making and is lacking good documentation and examples, something I hope will be fixed once it’s released. But there are couple nice blog posts I found from Colin Eberhardt that might give you a kickstart: