It turned out, that in the 'main' method I did not check if the operation was already cancelled. I also did not take special considerations about 'cancel' method being called from the main thread
I also did not check, whether the 'runLoop' ever stopped. It usually stops if no input sources attached, so I was removing the port from the loop. I now changed the way I run the NSRunLoop using 'runMode:beforeDate:'.
Here is the updated working code
#define STORE_THUMB_LOAD_OPERATION_RETURN_IF_CANCELLED() \
if (self.cancelled) {\
[self internalComplete]; \
return; \
}
@interface StoreThumbLoadOperation () <NSURLConnectionDelegate>
@property (strong, nonatomic) StoreThumbRequest *request;
@property (strong, nonatomic) NSMutableData *downloadedData;
@property (strong, nonatomic) NSURLConnection *connection;
@property (strong, nonatomic) NSPort *port;
@property (strong, nonatomic) NSRunLoop *runLoop;
@property (strong, nonatomic) NSThread *thread;
@property (assign, nonatomic) unsigned long long existingOnDiskThumbWeightInBytes;
@property (assign, nonatomic) BOOL isCompleted;
@property (assign, nonatomic) BOOL needsStop;
@end
@implementation StoreThumbLoadOperation
-(id)initWithRequest: (StoreThumbRequest *)request existingCachedImageSize:(unsigned long long)bytes
{
NSParameterAssert(request);
self = [super init];
if (self) {
self.request = request;
self.existingOnDiskThumbWeightInBytes = bytes;
}
return self;
}
-(void)main
{
// do not call super for optimizations
//[super main];
STORE_THUMB_LOAD_OPERATION_RETURN_IF_CANCELLED();
@autoreleasepool {
NSURL *url = ...;
NSURLRequest *request = [NSURLRequest requestWithCredentialsFromUrl:url];
self.connection = [[NSURLConnection alloc] initWithRequest: request delegate: self startImmediately: NO];
self.runLoop = [NSRunLoop currentRunLoop];
self.port = [NSMachPort port];
self.thread = [NSThread currentThread]; // <- retain the thread
[self.runLoop addPort: self.port forMode: NSDefaultRunLoopMode];
[self.connection scheduleInRunLoop: self.runLoop forMode: NSDefaultRunLoopMode];
[self.connection start];
// will run the loop until the operation is not cancelled or the image is not downloaded
NSTimeInterval stepLength = 0.1;
NSDate *future = [NSDate dateWithTimeIntervalSinceNow:stepLength];
while (! self.needsStop && [self.runLoop runMode:NSDefaultRunLoopMode beforeDate:future]) {
future = [future dateByAddingTimeInterval:stepLength];
}
// operation is cancelled, or the image is downloaded
self.isCompleted = YES;
[self internalComplete];
}
}
-(void)cancel {
[super cancel];
STORE_THUMB_LOAD_OPERATION_LOG_STATUS(@"cancelled");
[self setNeedsStopOnPrivateThread];
}
- (BOOL)isFinished {
// the operation must become completed to be removed from the queue
return self.isCompleted || self.isCancelled;
}
#pragma mark - privates
- (void)setNeedsStopOnPrivateThread {
// if self.thread is not nil, that the 'main' method was already called. if not, than the main thread cancelled the operation before it started
if (! self.thread || [NSThread currentThread] == self.thread) {
[self internalComplete];
} else {
[self performSelector:@selector(internalComplete) onThread:self.thread withObject:nil waitUntilDone:NO];
}
}
- (void)internalComplete {
[self cleanUp];
self.needsStop = YES; // <-- will break the 'while' loop
}
- (void)cleanUp {
[self.connection unscheduleFromRunLoop: self.runLoop forMode: NSDefaultRunLoopMode];
[self.connection cancel];
self.connection = nil;
//break retain loop
self.request.thumbView.operation = nil;
[self.runLoop removePort: self.port forMode: NSDefaultRunLoopMode];
self.port = nil;
self.request = nil;
self.downloadedData = nil;
}
#pragma mark - NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
self.downloadedData = [NSMutableData new];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.downloadedData appendData: data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if (self.connection != connection)
return;
STORE_THUMB_LOAD_OPERATION_LOG_STATUS(@"entered connection finished");
//create a directory if required
STORE_THUMB_LOAD_OPERATION_RETURN_IF_CANCELLED();
// wright the image to the file
STORE_THUMB_LOAD_OPERATION_RETURN_IF_CANCELLED();
// create the image from the file
UIImage *image = [UIImage imageWithContentsOfFile:...];
STORE_THUMB_LOAD_OPERATION_RETURN_IF_CANCELLED();
if (image) {
// cache the image
STORE_THUMB_LOAD_OPERATION_RETURN_IF_CANCELLED();
// update UI on the main thread
if (self.request.targetTag == self.request.thumbView.tag) {
StoreThumbView *targetView = self.request.thumbView;
dispatch_async(dispatch_get_main_queue(), ^{
targetView.image = image;
});
}
}
[self setNeedsStopOnPrivateThread];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
if (error.code != NSURLErrorCancelled) {
[self cancel];
}
}
@end