I was playing with RAC and, in particular, Colin Eberhardt's Twitter search example, and came across a crash that I could not explain to myself.
Here is a sample project I have created to illustrate the issue and base the question on.
The app uses a UITableView
with reusable cells; each cell has a UIImageView
on it whose image is downloaded by some URL.
There is also defined a signal for downloading an image on a background queue:
- (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 loading signal to the image view's image
property with RAC
macro:
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 app and start scrolling the table view up and down, the app crashes with the 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 wrap the image loading signal into deliverOn:
first, and then into takeUntil:
, the cell reuse will work just fine:
RAC(cell.kittenImageView, image) =
[[[self signalForLoadingImage:self.imageURLs[indexPath.row]]
deliverOn:[RACScheduler mainThreadScheduler]]
takeUntil:cell.rac_prepareForReuseSignal]; // No issue
So my questions are:
image
property before the existing one completes, but I'm totally not sure how exactly it occurs.Thanks for reading up to here :-)
I haven't confirmed this, but here's a possible explanation:
prepareForReuse
is called.rac_prepareForReuseSignal
sends a value.deliverTo:
, the value is dispatched to the main queue, introducing a runloop delay. Of note, this prevents synchronous/immediate unbinding of the image property.cellForRowAtIndexPath:
… next runloop …
So basically the signal should be unbound between 4 and 6, but the -deliverTo:
reorders the unbinding to come later.