RAC and cell reuse: placing deliverOn: in the right place?
I played around with RAC and Colin Eberhardt Twitter in particular to find an example and ran into a crash that I couldn't explain to myself.
Here's an example I created to illustrate the problem and ask a question.
Application uses UITableView
with reusable cells; each cell has UIImageView
on it, whose image is loaded by some url.
Also, a signal for loading an image in the background is defined:
- (RACSignal *)signalForLoadingImage:(NSString *)imageURLString { RACScheduler *scheduler = [RACScheduler schedulerWithPriority:RACSchedulerPriorityBackground]; return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageURLString]]; UIImage *image = [UIImage imageWithData:data]; [subscriber sendNext:image]; [subscriber sendCompleted]; return nil; }] subscribeOn:scheduler]; }
In cellForRowAtIndexPath:
I bind the load signal to the image view image
using a macro RAC
:
RAC(cell.kittenImageView, image) = [[[self signalForLoadingImage:self.imageURLs[indexPath.row]] takeUntil:cell.rac_prepareForReuseSignal] // Crashes on multiple binding assertion! deliverOn:[RACScheduler mainThreadScheduler]]; // Swap these two lines to 'fix'
Now when I run the application and start scrolling the table view up and down, the application crashes with an assertion message:
Signal <RACDynamicSignal: 0x7f9110485470> name: is already bound to key path "image" on object <UIImageView: <...>>, adding signal <RACDynamicSignal: 0x7f9110454510> name: is undefined behavior
However , if I first pack the image load signal into deliverOn:
and then intotakeUntil:
, cell reuse works fine:
RAC(cell.kittenImageView, image) = [[[self signalForLoadingImage:self.imageURLs[indexPath.row]] deliverOn:[RACScheduler mainThreadScheduler]] takeUntil:cell.rac_prepareForReuseSignal]; // No issue
So my questions are:
- How can you explain why the latter works and the former does not? Obviously there is some kind of race condition causing the new signal to bind to a property
image
before it completes, but I'm completely unsure how exactly this happens. - What should I remember to avoid this subtlety in my RAC code? Am I missing some basic principle in the code above, or is there any rule to apply (unless of course there is a bug in RAC)?
Thanks for reading here :-)
source to share
I have not confirmed this, but here is a possible explanation:
- Cell X starts at startup, starts loading the image.
- Cell X scrolls the screen before loading the image.
- Core X is used again,
prepareForReuse
invoked. - Cell X
rac_prepareForReuseSignal
sends a value. - Because of, the
deliverTo:
value is sent to the main queue, introducing the runloop delay. It should be noted that this prevents synchronous / immediate unpinning of the image property. - Cell X is used
cellForRowAtIndexPath:
- New image binding is called and raises a warning
-
… next runloop …
- The original binding is finally now broken, but it's too late.
So basically the signal needs to be decoupled between 4 and 6, but -deliverTo:
reorders the unlock to come later.
source to share