diff --git a/SDWebImage/SDWebImageDownloader.h b/SDWebImage/SDWebImageDownloader.h index d440e043..84592efa 100644 --- a/SDWebImage/SDWebImageDownloader.h +++ b/SDWebImage/SDWebImageDownloader.h @@ -176,12 +176,19 @@ typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDi * before to be called a last time with the full image and finished argument * set to YES. In case of error, the finished argument is always YES. * - * @return A cancellable SDWebImageOperation + * @return A token that can be passed to -cancel: to cancel this operation */ -- (id )downloadImageWithURL:(NSURL *)url - options:(SDWebImageDownloaderOptions)options - progress:(SDWebImageDownloaderProgressBlock)progressBlock - completed:(SDWebImageDownloaderCompletedBlock)completedBlock; +- (id)downloadImageWithURL:(NSURL *)url + options:(SDWebImageDownloaderOptions)options + progress:(SDWebImageDownloaderProgressBlock)progressBlock + completed:(SDWebImageDownloaderCompletedBlock)completedBlock; + +/** + * Cancels a download that was previously queued using -downloadImageWithURL:options:progress:completed: + * + * @param token The token received from -downloadImageWithURL:options:progress:completed: that should be canceled. + */ +- (void)cancel:(id)token; /** * Sets the download queue suspension state diff --git a/SDWebImage/SDWebImageDownloader.m b/SDWebImage/SDWebImageDownloader.m index 96aefa77..a6811feb 100644 --- a/SDWebImage/SDWebImageDownloader.m +++ b/SDWebImage/SDWebImageDownloader.m @@ -10,15 +10,22 @@ #import "SDWebImageDownloaderOperation.h" #import -static NSString *const kProgressCallbackKey = @"progress"; -static NSString *const kCompletedCallbackKey = @"completed"; +@interface _SDWebImageDownloaderToken : NSObject + +@property (nonatomic, strong) NSURL *url; +@property (nonatomic, strong) id downloadOperationCancelToken; + +@end + +@implementation _SDWebImageDownloaderToken +@end @interface SDWebImageDownloader () @property (strong, nonatomic) NSOperationQueue *downloadQueue; @property (weak, nonatomic) NSOperation *lastAddedOperation; @property (assign, nonatomic) Class operationClass; -@property (strong, nonatomic) NSMutableDictionary *URLCallbacks; +@property (strong, nonatomic) NSMutableDictionary *URLOperations; @property (strong, nonatomic) NSMutableDictionary *HTTPHeaders; // This queue is used to serialize the handling of the network responses of all the download operation in a single queue @property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue; @@ -66,7 +73,7 @@ static NSString *const kCompletedCallbackKey = @"completed"; _executionOrder = SDWebImageDownloaderFIFOExecutionOrder; _downloadQueue = [NSOperationQueue new]; _downloadQueue.maxConcurrentOperationCount = 6; - _URLCallbacks = [NSMutableDictionary new]; + _URLOperations = [NSMutableDictionary new]; #ifdef SD_WEBP _HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy]; #else @@ -112,11 +119,10 @@ static NSString *const kCompletedCallbackKey = @"completed"; _operationClass = operationClass ?: [SDWebImageDownloaderOperation class]; } -- (id )downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock { - __block SDWebImageDownloaderOperation *operation; - __weak __typeof(self)wself = self; +- (id)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock { + __weak SDWebImageDownloader *wself = self; - [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{ + return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{ NSTimeInterval timeoutInterval = wself.downloadTimeout; if (timeoutInterval == 0.0) { timeoutInterval = 15.0; @@ -132,44 +138,7 @@ static NSString *const kCompletedCallbackKey = @"completed"; else { request.allHTTPHeaderFields = wself.HTTPHeaders; } - operation = [[wself.operationClass alloc] initWithRequest:request - options:options - progress:^(NSInteger receivedSize, NSInteger expectedSize) { - SDWebImageDownloader *sself = wself; - if (!sself) return; - __block NSArray *callbacksForURL; - dispatch_sync(sself.barrierQueue, ^{ - callbacksForURL = [sself.URLCallbacks[url] copy]; - }); - for (NSDictionary *callbacks in callbacksForURL) { - dispatch_async(dispatch_get_main_queue(), ^{ - SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; - if (callback) callback(receivedSize, expectedSize); - }); - } - } - completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { - SDWebImageDownloader *sself = wself; - if (!sself) return; - __block NSArray *callbacksForURL; - dispatch_barrier_sync(sself.barrierQueue, ^{ - callbacksForURL = [sself.URLCallbacks[url] copy]; - if (finished) { - [sself.URLCallbacks removeObjectForKey:url]; - } - }); - for (NSDictionary *callbacks in callbacksForURL) { - SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey]; - if (callback) callback(image, data, error, finished); - } - } - cancelled:^{ - SDWebImageDownloader *sself = wself; - if (!sself) return; - dispatch_barrier_async(sself.barrierQueue, ^{ - [sself.URLCallbacks removeObjectForKey:url]; - }); - }]; + SDWebImageDownloaderOperation *operation = [[wself.operationClass alloc] initWithRequest:request options:options]; operation.shouldDecompressImages = wself.shouldDecompressImages; if (wself.urlCredential) { @@ -190,39 +159,60 @@ static NSString *const kCompletedCallbackKey = @"completed"; [wself.lastAddedOperation addDependency:operation]; wself.lastAddedOperation = operation; } - }]; - return operation; + return operation; + }]; } -- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback { +- (void)cancel:(id)token { + if (![token isKindOfClass:[_SDWebImageDownloaderToken class]]) { + return; + } + + dispatch_barrier_async(self.barrierQueue, ^{ + _SDWebImageDownloaderToken *typedToken = (_SDWebImageDownloaderToken *)token; + SDWebImageDownloaderOperation *operation = self.URLOperations[typedToken.url]; + BOOL canceled = [operation cancel:typedToken.downloadOperationCancelToken]; + if (canceled) { + [self.URLOperations removeObjectForKey:typedToken.url]; + } + }); +} + +- (id)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageDownloaderOperation *(^)())createCallback { // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data. if (url == nil) { if (completedBlock != nil) { completedBlock(nil, nil, nil, NO); } - return; + return nil; } + __block _SDWebImageDownloaderToken *token = nil; + dispatch_barrier_sync(self.barrierQueue, ^{ - BOOL first = NO; - if (!self.URLCallbacks[url]) { - self.URLCallbacks[url] = [NSMutableArray new]; - first = YES; - } + SDWebImageDownloaderOperation *operation = self.URLOperations[url]; + if (!operation) { + operation = createCallback(); + self.URLOperations[url] = operation; - // Handle single download of simultaneous download request for the same URL - NSMutableArray *callbacksForURL = self.URLCallbacks[url]; - NSMutableDictionary *callbacks = [NSMutableDictionary new]; - if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; - if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; - [callbacksForURL addObject:callbacks]; - self.URLCallbacks[url] = callbacksForURL; - - if (first) { - createCallback(); + __weak SDWebImageDownloaderOperation *woperation = operation; + operation.completionBlock = ^{ + SDWebImageDownloaderOperation *soperation = woperation; + if (!soperation) return; + if (self.URLOperations[url] == soperation) { + [self.URLOperations removeObjectForKey:url]; + }; + }; } + id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock]; + + token = [_SDWebImageDownloaderToken new]; + token.url = url; + token.downloadOperationCancelToken = downloadOperationCancelToken; }); + + return token; } - (void)setSuspended:(BOOL)suspended { diff --git a/SDWebImage/SDWebImageDownloaderOperation.h b/SDWebImage/SDWebImageDownloaderOperation.h index dd48b228..68abec23 100644 --- a/SDWebImage/SDWebImageDownloaderOperation.h +++ b/SDWebImage/SDWebImageDownloaderOperation.h @@ -61,18 +61,33 @@ extern NSString *const SDWebImageDownloadFinishNotification; * * @param request the URL request * @param options downloader options - * @param progressBlock the block executed when a new chunk of data arrives. - * @note the progress block is executed on a background queue - * @param completedBlock the block executed when the download is done. - * @note the completed block is executed on the main queue for success. If errors are found, there is a chance the block will be executed on a background queue - * @param cancelBlock the block executed if the download (operation) is cancelled * * @return the initialized instance */ - (id)initWithRequest:(NSURLRequest *)request - options:(SDWebImageDownloaderOptions)options - progress:(SDWebImageDownloaderProgressBlock)progressBlock - completed:(SDWebImageDownloaderCompletedBlock)completedBlock - cancelled:(SDWebImageNoParamsBlock)cancelBlock; + options:(SDWebImageDownloaderOptions)options; + +/** + * Adds handlers for progress and completion. Returns a tokent that can be passed to -cancel: to cancel this set of + * callbacks. + * + * @param progressBlock the block executed when a new chunk of data arrives. + * @note the progress block is executed on a background queue + * @param completedBlock the block executed when the download is done. + * @note the completed block is executed on the main queue for success. If errors are found, there is a chance the block will be executed on a background queue + * + * @return the token to use to cancel this set of handlers + */ +- (id)addHandlersForProgress:(SDWebImageDownloaderProgressBlock)progressBlock + completed:(SDWebImageDownloaderCompletedBlock)completedBlock; + +/** + * Cancels a set of callbacks. Once all callbacks are canceled, the operation is cancelled. + * + * @param token the token representing a set of callbacks to cancel + * + * @return YES if the operation was stopped because this was the last token to be canceled. NO otherwise. + */ +- (BOOL)cancel:(id)token; @end diff --git a/SDWebImage/SDWebImageDownloaderOperation.m b/SDWebImage/SDWebImageDownloaderOperation.m index 5a8bd11f..51f83a8a 100644 --- a/SDWebImage/SDWebImageDownloaderOperation.m +++ b/SDWebImage/SDWebImageDownloaderOperation.m @@ -17,17 +17,19 @@ NSString *const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDown NSString *const SDWebImageDownloadStopNotification = @"SDWebImageDownloadStopNotification"; NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinishNotification"; +static NSString *const kProgressCallbackKey = @"progress"; +static NSString *const kCompletedCallbackKey = @"completed"; + @interface SDWebImageDownloaderOperation () -@property (copy, nonatomic) SDWebImageDownloaderProgressBlock progressBlock; -@property (copy, nonatomic) SDWebImageDownloaderCompletedBlock completedBlock; -@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock; +@property (strong, nonatomic) NSMutableArray *callbackBlocks; @property (assign, nonatomic, getter = isExecuting) BOOL executing; @property (assign, nonatomic, getter = isFinished) BOOL finished; @property (strong, nonatomic) NSMutableData *imageData; @property (strong, nonatomic) NSURLConnection *connection; @property (strong, atomic) NSThread *thread; +@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue; #if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId; @@ -45,26 +47,61 @@ NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinis @synthesize finished = _finished; - (id)initWithRequest:(NSURLRequest *)request - options:(SDWebImageDownloaderOptions)options - progress:(SDWebImageDownloaderProgressBlock)progressBlock - completed:(SDWebImageDownloaderCompletedBlock)completedBlock - cancelled:(SDWebImageNoParamsBlock)cancelBlock { + options:(SDWebImageDownloaderOptions)options { if ((self = [super init])) { _request = request; _shouldDecompressImages = YES; _shouldUseCredentialStorage = YES; _options = options; - _progressBlock = [progressBlock copy]; - _completedBlock = [completedBlock copy]; - _cancelBlock = [cancelBlock copy]; + _callbackBlocks = [NSMutableArray new]; _executing = NO; _finished = NO; _expectedSize = 0; responseFromCached = YES; // Initially wrong until `connection:willCacheResponse:` is called or not called + _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationBarrierQueue", DISPATCH_QUEUE_CONCURRENT); } return self; } +- (void)dealloc { + SDDispatchQueueRelease(_barrierQueue); +} + +- (id)addHandlersForProgress:(SDWebImageDownloaderProgressBlock)progressBlock + completed:(SDWebImageDownloaderCompletedBlock)completedBlock { + NSMutableDictionary *callbacks = [NSMutableDictionary new]; + if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; + if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; + dispatch_barrier_async(self.barrierQueue, ^{ + [self.callbackBlocks addObject:callbacks]; + }); + return callbacks; +} + +- (NSArray *)callbacksForKey:(NSString *)key { + __block NSMutableArray *callbacks = nil; + dispatch_sync(self.barrierQueue, ^{ + // We need to remove [NSNull null] because there might not always be a progress block for each callback + callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy]; + [callbacks removeObjectIdenticalTo:[NSNull null]]; + }); + return callbacks; +} + +- (BOOL)cancel:(id)token { + __block BOOL shouldCancel = NO; + dispatch_barrier_sync(self.barrierQueue, ^{ + [self.callbackBlocks removeObjectIdenticalTo:token]; + if (self.callbackBlocks.count == 0) { + shouldCancel = YES; + } + }); + if (shouldCancel) { + [self cancel]; + } + return shouldCancel; +} + - (void)start { @synchronized (self) { if (self.isCancelled) { @@ -100,8 +137,8 @@ NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinis [self.connection start]; if (self.connection) { - if (self.progressBlock) { - self.progressBlock(0, NSURLResponseUnknownLength); + for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { + progressBlock(0, NSURLResponseUnknownLength); } dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self]; @@ -123,8 +160,8 @@ NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinis } } else { - if (self.completedBlock) { - self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES); + for (SDWebImageDownloaderCompletedBlock completedBlock in [self callbacksForKey:kCompletedCallbackKey]) { + completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES); } } @@ -161,7 +198,6 @@ NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinis - (void)cancelInternal { if (self.isFinished) return; [super cancel]; - if (self.cancelBlock) self.cancelBlock(); if (self.connection) { [self.connection cancel]; @@ -185,9 +221,9 @@ NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinis } - (void)reset { - self.cancelBlock = nil; - self.completedBlock = nil; - self.progressBlock = nil; + dispatch_barrier_async(self.barrierQueue, ^{ + [self.callbackBlocks removeAllObjects]; + }); self.connection = nil; self.imageData = nil; self.thread = nil; @@ -217,8 +253,8 @@ NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinis if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) { NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0; self.expectedSize = expected; - if (self.progressBlock) { - self.progressBlock(0, expected); + for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { + progressBlock(0, expected); } self.imageData = [[NSMutableData alloc] initWithCapacity:expected]; @@ -241,8 +277,8 @@ NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinis [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; }); - if (self.completedBlock) { - self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES); + for (SDWebImageDownloaderCompletedBlock completedBlock in [self callbacksForKey:kCompletedCallbackKey]) { + completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES); } CFRunLoopStop(CFRunLoopGetCurrent()); [self done]; @@ -252,7 +288,7 @@ NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinis - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.imageData appendData:data]; - if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) { + if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) { // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/ // Thanks to the author @Nyx0uf @@ -319,8 +355,8 @@ NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinis } CGImageRelease(partialImageRef); dispatch_main_sync_safe(^{ - if (self.completedBlock) { - self.completedBlock(image, nil, nil, NO); + for (SDWebImageDownloaderCompletedBlock completedBlock in [self callbacksForKey:kCompletedCallbackKey]) { + completedBlock(image, nil, nil, NO); } }); } @@ -329,8 +365,8 @@ NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinis CFRelease(imageSource); } - if (self.progressBlock) { - self.progressBlock(self.imageData.length, self.expectedSize); + for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { + progressBlock(self.imageData.length, self.expectedSize); } } @@ -362,7 +398,7 @@ NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinis } - (void)connectionDidFinishLoading:(NSURLConnection *)aConnection { - SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock; + NSArray *completionBlocks = [[self callbacksForKey:kCompletedCallbackKey] copy]; @synchronized(self) { CFRunLoopStop(CFRunLoopGetCurrent()); self.thread = nil; @@ -376,10 +412,12 @@ NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinis if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) { responseFromCached = NO; } - - if (completionBlock) { + + if (completionBlocks.count > 0) { if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) { - completionBlock(nil, nil, nil, YES); + for (SDWebImageDownloaderCompletedBlock completionBlock in completionBlocks) { + completionBlock(nil, nil, nil, YES); + } } else if (self.imageData) { UIImage *image = [UIImage sd_imageWithData:self.imageData]; NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; @@ -391,17 +429,20 @@ NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinis image = [UIImage decodedImageWithImage:image]; } } - if (CGSizeEqualToSize(image.size, CGSizeZero)) { - completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES); - } - else { - completionBlock(image, self.imageData, nil, YES); + for (SDWebImageDownloaderCompletedBlock completionBlock in completionBlocks) { + if (CGSizeEqualToSize(image.size, CGSizeZero)) { + completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES); + } + else { + completionBlock(image, self.imageData, nil, YES); + } } } else { - completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES); + for (SDWebImageDownloaderCompletedBlock completionBlock in completionBlocks) { + completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES); + } } } - self.completionBlock = nil; [self done]; } @@ -415,8 +456,8 @@ NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinis }); } - if (self.completedBlock) { - self.completedBlock(nil, nil, error, YES); + for (SDWebImageDownloaderCompletedBlock completedBlock in [self callbacksForKey:kCompletedCallbackKey]) { + completedBlock(nil, nil, error, YES); } self.completionBlock = nil; [self done]; diff --git a/SDWebImage/SDWebImageManager.m b/SDWebImage/SDWebImageManager.m index 61d958ad..6e2c9f51 100644 --- a/SDWebImage/SDWebImageManager.m +++ b/SDWebImage/SDWebImageManager.m @@ -179,7 +179,7 @@ // ignore image read from NSURLCache if image if cached but force refreshing downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse; } - id subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { + id subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { __strong __typeof(weakOperation) strongOperation = weakOperation; if (!strongOperation || strongOperation.isCancelled) { // Do nothing if the operation was cancelled @@ -255,8 +255,8 @@ } }]; operation.cancelBlock = ^{ - [subOperation cancel]; - + [self.imageDownloader cancel:subOperation]; + @synchronized (self.runningOperations) { __strong __typeof(weakOperation) strongOperation = weakOperation; if (strongOperation) { diff --git a/Tests/SDWebImage Tests.xcodeproj/project.pbxproj b/Tests/SDWebImage Tests.xcodeproj/project.pbxproj index ebf9601b..5b08344d 100644 --- a/Tests/SDWebImage Tests.xcodeproj/project.pbxproj +++ b/Tests/SDWebImage Tests.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 0D87E1F83BD319CEC7622E9F /* libPods-Tests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0462A7F023A057322E59B3C5 /* libPods-Tests.a */; }; 5F7F38AD1AE2A77A00B0E330 /* TestImage.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 5F7F38AC1AE2A77A00B0E330 /* TestImage.jpg */; }; + 1E3C51E919B46E370092B5E6 /* SDWebImageDownloaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E3C51E819B46E370092B5E6 /* SDWebImageDownloaderTests.m */; }; DA248D57195472AA00390AB0 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA248D56195472AA00390AB0 /* XCTest.framework */; }; DA248D59195472AA00390AB0 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA248D58195472AA00390AB0 /* Foundation.framework */; }; DA248D5B195472AA00390AB0 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA248D5A195472AA00390AB0 /* UIKit.framework */; }; @@ -23,6 +24,7 @@ 5F7F38AC1AE2A77A00B0E330 /* TestImage.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = TestImage.jpg; sourceTree = ""; }; 700B00151041D7EE118B1ABD /* Pods-Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Tests/Pods-Tests.debug.xcconfig"; sourceTree = ""; }; A0085854E7D88C98F2F6C9FC /* Pods-Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Tests/Pods-Tests.release.xcconfig"; sourceTree = ""; }; + 1E3C51E819B46E370092B5E6 /* SDWebImageDownloaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDWebImageDownloaderTests.m; sourceTree = ""; }; DA248D53195472AA00390AB0 /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DA248D56195472AA00390AB0 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; DA248D58195472AA00390AB0 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; @@ -96,6 +98,7 @@ DA248D68195475D800390AB0 /* SDImageCacheTests.m */, DA248D6A195476AC00390AB0 /* SDWebImageManagerTests.m */, DA91BEBB19795BC9006F2536 /* UIImageMultiFormatTests.m */, + 1E3C51E819B46E370092B5E6 /* SDWebImageDownloaderTests.m */, ); path = Tests; sourceTree = ""; @@ -223,6 +226,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1E3C51E919B46E370092B5E6 /* SDWebImageDownloaderTests.m in Sources */, DA248D69195475D800390AB0 /* SDImageCacheTests.m in Sources */, DA248D6B195476AC00390AB0 /* SDWebImageManagerTests.m in Sources */, DA91BEBC19795BC9006F2536 /* UIImageMultiFormatTests.m in Sources */, diff --git a/Tests/Tests/SDWebImageDownloaderTests.m b/Tests/Tests/SDWebImageDownloaderTests.m new file mode 100644 index 00000000..233711fa --- /dev/null +++ b/Tests/Tests/SDWebImageDownloaderTests.m @@ -0,0 +1,87 @@ +// +// SDWebImageDownloaderTests.m +// SDWebImage Tests +// +// Created by Matt Galloway on 01/09/2014. +// +// + +#define EXP_SHORTHAND // required by Expecta + + +#import +#import + +#import "SDWebImageDownloader.h" + +@interface SDWebImageDownloaderTests : XCTestCase + +@end + +@implementation SDWebImageDownloaderTests + +- (void)setUp +{ + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown +{ + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +- (void)testThatDownloadingSameURLTwiceAndCancellingFirstWorks { + XCTestExpectation *expectation = [self expectationWithDescription:@"Correct image downloads"]; + + NSURL *imageURL = [NSURL URLWithString:@"http://static2.dmcdn.net/static/video/656/177/44771656:jpeg_preview_small.jpg?20120509154705"]; + + id token1 = [[SDWebImageDownloader sharedDownloader] downloadImageWithURL:imageURL + options:0 + progress:nil + completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { + XCTFail(@"Shouldn't have completed here."); + }]; + expect(token1).toNot.beNil(); + + id token2 = [[SDWebImageDownloader sharedDownloader] downloadImageWithURL:imageURL + options:0 + progress:nil + completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { + [expectation fulfill]; + }]; + expect(token2).toNot.beNil(); + + [[SDWebImageDownloader sharedDownloader] cancel:token1]; + + [self waitForExpectationsWithTimeout:5. handler:nil]; +} + +- (void)testThatCancelingDownloadThenRequestingAgainWorks { + XCTestExpectation *expectation = [self expectationWithDescription:@"Correct image downloads"]; + + NSURL *imageURL = [NSURL URLWithString:@"http://static2.dmcdn.net/static/video/656/177/44771656:jpeg_preview_small.jpg?20120509154705"]; + + id token1 = [[SDWebImageDownloader sharedDownloader] downloadImageWithURL:imageURL + options:0 + progress:nil + completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { + XCTFail(@"Shouldn't have completed here."); + }]; + expect(token1).toNot.beNil(); + + [[SDWebImageDownloader sharedDownloader] cancel:token1]; + + id token2 = [[SDWebImageDownloader sharedDownloader] downloadImageWithURL:imageURL + options:0 + progress:nil + completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { + [expectation fulfill]; + }]; + expect(token2).toNot.beNil(); + + [self waitForExpectationsWithTimeout:5. handler:nil]; +} + +@end