diff --git a/CHANGELOG.md b/CHANGELOG.md index d1386609be..ed264d5115 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - [ASCollectionView] Improve index space translation of Flow Layout Delegate methods. [Scott Goodson](https://github.com/appleguy) - [Animated Image] Adds support for animated WebP as well as improves GIF handling. [#605](https://github.com/TextureGroup/Texture/pull/605) [Garrett Moon](https://github.com/garrettmoon) - [ASCollectionView] Check if batch fetching is needed if batch fetching parameter has been changed. [#624](https://github.com/TextureGroup/Texture/pull/624) [Garrett Moon](https://github.com/garrettmoon) +- [ASNetworkImageNode] New delegate callback to tell the consumer whether the image was loaded from cache or download. [Adlai Holler](https://github.com/Adlai-Holler) ## 2.6 - [Xcode 9] Updated to require Xcode 9 (to fix warnings) [Garrett Moon](https://github.com/garrettmoon) diff --git a/Source/ASNetworkImageNode.h b/Source/ASNetworkImageNode.h index dc911bede6..455806cd27 100644 --- a/Source/ASNetworkImageNode.h +++ b/Source/ASNetworkImageNode.h @@ -130,6 +130,21 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - + +typedef NS_ENUM(NSInteger, ASNetworkImageSource) { + ASNetworkImageSourceUnspecified = 0, + ASNetworkImageSourceSynchronousCache, + ASNetworkImageSourceAsynchronousCache, + ASNetworkImageSourceFileURL, + ASNetworkImageSourceDownload, +}; + +/// A struct that carries details about ASNetworkImageNode's image loads. +typedef struct { + /// The source from which the image was loaded. + ASNetworkImageSource imageSource; +} ASNetworkImageNodeDidLoadInfo; + /** * The methods declared by the ASNetworkImageNodeDelegate protocol allow the adopting delegate to respond to * notifications such as finished decoding and downloading an image. @@ -137,6 +152,18 @@ NS_ASSUME_NONNULL_BEGIN @protocol ASNetworkImageNodeDelegate @optional +/** + * Notification that the image node finished downloading an image, with additional info. + * If implemented, this method will be called instead of `imageNode:didLoadImage:`. + * + * @param imageNode The sender. + * @param image The newly-loaded image. + * @param info Misc information about the image load. + * + * @discussion Called on a background queue. + */ +- (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image info:(ASNetworkImageNodeDidLoadInfo)info; + /** * Notification that the image node finished downloading an image. * diff --git a/Source/ASNetworkImageNode.mm b/Source/ASNetworkImageNode.mm index 2b22ea5261..0a7ee0fbde 100755 --- a/Source/ASNetworkImageNode.mm +++ b/Source/ASNetworkImageNode.mm @@ -56,6 +56,7 @@ unsigned int delegateDidFailWithError:1; unsigned int delegateDidFinishDecoding:1; unsigned int delegateDidLoadImage:1; + unsigned int delegateDidLoadImageWithInfo:1; } _delegateFlags; @@ -305,6 +306,7 @@ _delegateFlags.delegateDidFailWithError = [delegate respondsToSelector:@selector(imageNode:didFailWithError:)]; _delegateFlags.delegateDidFinishDecoding = [delegate respondsToSelector:@selector(imageNodeDidFinishDecoding:)]; _delegateFlags.delegateDidLoadImage = [delegate respondsToSelector:@selector(imageNode:didLoadImage:)]; + _delegateFlags.delegateDidLoadImageWithInfo = [delegate respondsToSelector:@selector(imageNode:didLoadImage:info:)]; } - (id)delegate @@ -353,8 +355,18 @@ if (result) { [self _locked_setCurrentImageQuality:1.0]; [self _locked__setImage:result]; - _imageLoaded = YES; + + // Call out to the delegate. + if (_delegateFlags.delegateDidLoadImageWithInfo) { + ASDN::MutexUnlocker l(__instanceLock__); + ASNetworkImageNodeDidLoadInfo info = {}; + info.imageSource = ASNetworkImageSourceSynchronousCache; + [_delegate imageNode:self didLoadImage:result info:info]; + } else if (_delegateFlags.delegateDidLoadImage) { + ASDN::MutexUnlocker l(__instanceLock__); + [_delegate imageNode:self didLoadImage:result]; + } break; } } @@ -688,14 +700,19 @@ [self _locked_setCurrentImageQuality:1.0]; - if (_delegateFlags.delegateDidLoadImage) { + if (_delegateFlags.delegateDidLoadImageWithInfo) { + ASDN::MutexUnlocker u(__instanceLock__); + ASNetworkImageNodeDidLoadInfo info = {}; + info.imageSource = ASNetworkImageSourceFileURL; + [delegate imageNode:self didLoadImage:self.image info:info]; + } else if (_delegateFlags.delegateDidLoadImage) { ASDN::MutexUnlocker u(__instanceLock__); [delegate imageNode:self didLoadImage:self.image]; } }); } else { __weak __typeof__(self) weakSelf = self; - auto finished = ^(id imageContainer, NSError *error, id downloadIdentifier) { + auto finished = ^(id imageContainer, NSError *error, id downloadIdentifier, ASNetworkImageSource imageSource) { __typeof__(self) strongSelf = weakSelf; if (strongSelf == nil) { @@ -732,7 +749,12 @@ strongSelf->_cacheUUID = nil; if (imageContainer != nil) { - if (strongSelf->_delegateFlags.delegateDidLoadImage) { + if (strongSelf->_delegateFlags.delegateDidLoadImageWithInfo) { + ASDN::MutexUnlocker u(strongSelf->__instanceLock__); + ASNetworkImageNodeDidLoadInfo info = {}; + info.imageSource = imageSource; + [delegate imageNode:strongSelf didLoadImage:strongSelf.image info:info]; + } else if (strongSelf->_delegateFlags.delegateDidLoadImage) { ASDN::MutexUnlocker u(strongSelf->__instanceLock__); [delegate imageNode:strongSelf didLoadImage:strongSelf.image]; } @@ -763,10 +785,12 @@ } if ([imageContainer asdk_image] == nil && _downloader != nil) { - [self _downloadImageWithCompletion:finished]; + [self _downloadImageWithCompletion:^(id imageContainer, NSError *error, id downloadIdentifier) { + finished(imageContainer, error, downloadIdentifier, ASNetworkImageSourceDownload); + }]; } else { as_log_verbose(ASImageLoadingLog(), "Decached image for %@ img: %@ urls: %@", self, [imageContainer asdk_image], URLs); - finished(imageContainer, nil, nil); + finished(imageContainer, nil, nil, ASNetworkImageSourceAsynchronousCache); } }; @@ -780,7 +804,9 @@ completion:completion]; } } else { - [self _downloadImageWithCompletion:finished]; + [self _downloadImageWithCompletion:^(id imageContainer, NSError *error, id downloadIdentifier) { + finished(imageContainer, error, downloadIdentifier, ASNetworkImageSourceDownload); + }]; } } } diff --git a/Source/Details/ASPINRemoteImageDownloader.m b/Source/Details/ASPINRemoteImageDownloader.m index 05107f8e00..b57b72102d 100644 --- a/Source/Details/ASPINRemoteImageDownloader.m +++ b/Source/Details/ASPINRemoteImageDownloader.m @@ -202,15 +202,11 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; callbackQueue:(dispatch_queue_t)callbackQueue completion:(ASImageCacherCompletion)completion { - // We do not check the cache here and instead check it in downloadImageWithURL to avoid checking the cache twice. - // If we're targeting the main queue and we're on the main thread, complete immediately. - if (ASDisplayNodeThreadIsMain() && callbackQueue == dispatch_get_main_queue()) { - completion(nil); - } else { - dispatch_async(callbackQueue, ^{ - completion(nil); - }); - } + [[self sharedPINRemoteImageManager] imageFromCacheWithURL:URL processorKey:nil options:PINRemoteImageManagerDownloadOptionsSkipDecode completion:^(PINRemoteImageManagerResult * _Nonnull result) { + [ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{ + completion(result.image); + }]; + }]; } - (void)cachedImageWithURLs:(NSArray *)URLs @@ -256,51 +252,38 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress completion:(ASImageDownloaderCompletion)completion { - PINRemoteImageManagerProgressDownload progressDownload = ^(int64_t completedBytes, int64_t totalBytes) { - if (downloadProgress == nil) { return; } - - /// If we're targeting the main queue and we're on the main thread, call immediately. - if (ASDisplayNodeThreadIsMain() && callbackQueue == dispatch_get_main_queue()) { - downloadProgress(completedBytes / (CGFloat)totalBytes); - } else { - dispatch_async(callbackQueue, ^{ - downloadProgress(completedBytes / (CGFloat)totalBytes); - }); - } - }; - - PINRemoteImageManagerImageCompletion imageCompletion = ^(PINRemoteImageManagerResult * _Nonnull result) { - /// If we're targeting the main queue and we're on the main thread, complete immediately. - if (ASDisplayNodeThreadIsMain() && callbackQueue == dispatch_get_main_queue()) { + PINRemoteImageManagerProgressDownload progressDownload = ^(int64_t completedBytes, int64_t totalBytes) { + if (downloadProgress == nil) { return; } + + [ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{ + downloadProgress(completedBytes / (CGFloat)totalBytes); + }]; + }; + + PINRemoteImageManagerImageCompletion imageCompletion = ^(PINRemoteImageManagerResult * _Nonnull result) { + [ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{ #if PIN_ANIMATED_AVAILABLE - if (result.alternativeRepresentation) { - completion(result.alternativeRepresentation, result.error, result.UUID); - } else { - completion(result.image, result.error, result.UUID); - } + if (result.alternativeRepresentation) { + completion(result.alternativeRepresentation, result.error, result.UUID); + } else { + completion(result.image, result.error, result.UUID); + } #else - completion(result.image, result.error, result.UUID); + completion(result.image, result.error, result.UUID); #endif - } else { - dispatch_async(callbackQueue, ^{ -#if PIN_ANIMATED_AVAILABLE - if (result.alternativeRepresentation) { - completion(result.alternativeRepresentation, result.error, result.UUID); - } else { - completion(result.image, result.error, result.UUID); - } -#else - completion(result.image, result.error, result.UUID); -#endif - }); - } - }; - - return [[self sharedPINRemoteImageManager] downloadImageWithURLs:URLs - options:PINRemoteImageManagerDownloadOptionsSkipDecode - progressImage:nil - progressDownload:progressDownload - completion:imageCompletion]; + }]; + }; + + // add "IgnoreCache" option since we have a caching API so we already checked it, not worth checking again. + // PINRemoteImage is responsible for coalescing downloads, and even if it wasn't, the tiny probability of + // extra downloads isn't worth the effort of rechecking caches every single time. In order to provide + // feedback to the consumer about whether images are cached, we can't simply make the cache a no-op and + // check the cache as part of this download. + return [[self sharedPINRemoteImageManager] downloadImageWithURLs:URLs + options:PINRemoteImageManagerDownloadOptionsSkipDecode | PINRemoteImageManagerDownloadOptionsIgnoreCache + progressImage:nil + progressDownload:progressDownload + completion:imageCompletion]; } - (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier @@ -369,5 +352,29 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; return nil; } +#pragma mark - Private + +/** + * If on main thread and queue is main, perform now. + * If queue is nil, assert and perform now. + * Otherwise, dispatch async to queue. + */ ++ (void)_performWithCallbackQueue:(dispatch_queue_t)queue work:(void (^)())work +{ + if (work == nil) { + // No need to assert here, really. We aren't expecting any feedback from this method. + return; + } + + if (ASDisplayNodeThreadIsMain() && queue == dispatch_get_main_queue()) { + work(); + } else if (queue == nil) { + ASDisplayNodeFailAssert(@"Callback queue should not be nil."); + work(); + } else { + dispatch_async(queue, work); + } +} + @end #endif