diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index ccca6c7045..e6a0d0d0a2 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -17,6 +17,8 @@ 051943151A1575670030A7D0 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943141A1575670030A7D0 /* Photos.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 052EE0661A159FEF002C6279 /* ASMultiplexImageNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 052EE06B1A15A0D8002C6279 /* TestResources in Resources */ = {isa = PBXBuildFile; fileRef = 052EE06A1A15A0D8002C6279 /* TestResources */; }; + 055B9FA81A1C154B00035D6D /* ASNetworkImageNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 055B9FA61A1C154B00035D6D /* ASNetworkImageNode.h */; }; + 055B9FA91A1C154B00035D6D /* ASNetworkImageNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 055B9FA71A1C154B00035D6D /* ASNetworkImageNode.mm */; }; 055F1A3419ABD3E3004DAFF1 /* ASTableView.h in Headers */ = {isa = PBXBuildFile; fileRef = 055F1A3219ABD3E3004DAFF1 /* ASTableView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 055F1A3519ABD3E3004DAFF1 /* ASTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 055F1A3319ABD3E3004DAFF1 /* ASTableView.m */; }; 055F1A3819ABD413004DAFF1 /* ASRangeController.h in Headers */ = {isa = PBXBuildFile; fileRef = 055F1A3619ABD413004DAFF1 /* ASRangeController.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -170,6 +172,8 @@ 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASMultiplexImageNodeTests.m; sourceTree = ""; }; 052EE06A1A15A0D8002C6279 /* TestResources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = TestResources; sourceTree = ""; }; 053011A719B9882B00A9F2D0 /* ASRangeControllerInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASRangeControllerInternal.h; sourceTree = ""; }; + 055B9FA61A1C154B00035D6D /* ASNetworkImageNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASNetworkImageNode.h; sourceTree = ""; }; + 055B9FA71A1C154B00035D6D /* ASNetworkImageNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASNetworkImageNode.mm; sourceTree = ""; }; 055F1A3219ABD3E3004DAFF1 /* ASTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTableView.h; sourceTree = ""; }; 055F1A3319ABD3E3004DAFF1 /* ASTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableView.m; sourceTree = ""; }; 055F1A3619ABD413004DAFF1 /* ASRangeController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRangeController.h; sourceTree = ""; }; @@ -349,6 +353,8 @@ 058D09DE195D050800B7D73C /* ASImageNode.mm */, 0516FA3E1A1563D200B4EBED /* ASMultiplexImageNode.h */, 0516FA3F1A1563D200B4EBED /* ASMultiplexImageNode.mm */, + 055B9FA61A1C154B00035D6D /* ASNetworkImageNode.h */, + 055B9FA71A1C154B00035D6D /* ASNetworkImageNode.mm */, 055F1A3219ABD3E3004DAFF1 /* ASTableView.h */, 0574D5E119C110610097DC25 /* ASTableViewProtocols.h */, 055F1A3319ABD3E3004DAFF1 /* ASTableView.m */, @@ -545,6 +551,7 @@ 058D0A64195D05DC00B7D73C /* ASTextNodeWordKerner.h in Headers */, 058D0A65195D05DC00B7D73C /* ASTextNodeWordKerner.m in Headers */, 058D0A66195D05DC00B7D73C /* NSMutableAttributedString+TextKitAdditions.h in Headers */, + 055B9FA81A1C154B00035D6D /* ASNetworkImageNode.h in Headers */, 058D0A67195D05DC00B7D73C /* NSMutableAttributedString+TextKitAdditions.m in Headers */, 058D0A68195D05EC00B7D73C /* _ASAsyncTransaction.h in Headers */, 058D0A69195D05EC00B7D73C /* _ASAsyncTransaction.m in Headers */, @@ -708,6 +715,7 @@ 058D0A15195D050800B7D73C /* ASDisplayNodeExtras.mm in Sources */, 058D0A1F195D050800B7D73C /* ASTextNodeTextKitHelpers.mm in Sources */, 055F1A3519ABD3E3004DAFF1 /* ASTableView.m in Sources */, + 055B9FA91A1C154B00035D6D /* ASNetworkImageNode.mm in Sources */, 058D0A1D195D050800B7D73C /* ASTextNodeRenderer.mm in Sources */, 058D0A2A195D050800B7D73C /* ASDisplayNode+UIViewBridge.mm in Sources */, AC3C4A521A1139C100143C57 /* ASCollectionView.m in Sources */, diff --git a/AsyncDisplayKit/ASNetworkImageNode.h b/AsyncDisplayKit/ASNetworkImageNode.h new file mode 100644 index 0000000000..36425abd25 --- /dev/null +++ b/AsyncDisplayKit/ASNetworkImageNode.h @@ -0,0 +1,91 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +@protocol ASNetworkImageNodeDelegate; + + +/** + * ASNetworkImageNode is a simple image node that can download and display an image from the network, with support for a + * placeholder image (). The currently-displayed image is always available in the inherited ASImageNode + * property. + * + * @see ASMultiplexImageNode for a more powerful counterpart to this class. + */ +@interface ASNetworkImageNode : ASImageNode + +/** + * The designated initializer. + * + * @param cache The object that implements a cache of images for the image node. + * @param downloader The object that implements image downloading for the image node. Must not be nil. + * + * @discussion If `cache` is nil, the receiver will not attempt to retrieve images from a cache before downloading them. + * + * @result An initialized ASNetworkImageNode. + */ +- (instancetype)initWithCache:(id)cache downloader:(id)downloader; +- (instancetype)init NS_UNAVAILABLE; + +/** + * The delegate, which must conform to the protocol. + */ +@property (atomic, assign, readwrite) id delegate; + +/** + * A placeholder image to display while the URL is loading. + */ +@property (atomic, retain, readwrite) UIImage *defaultImage; + +/** + * The URL of a new image to download and display. + * + * @discussion Changing this property will reset the displayed image to a placeholder () while loading. + */ +@property (atomic, retain, readwrite) NSURL *URL; + +/** + * Download and display a new image. + * + * @param reset Whether to display a placeholder () while loading the new image. + */ +- (void)setURL:(NSURL *)URL resetToDefault:(BOOL)reset; + +/** + * If is a local file, set this property to YES to take advantage of UIKit's image cacheing. Defaults to YES. + */ +@property (nonatomic, assign, readwrite) BOOL shouldCacheImage; + +@end + + +#pragma mark - +@protocol ASNetworkImageNodeDelegate + +/** + * Notification that the image node finished downloading an image. + * + * @param imageNode The sender. + * @param image The newly-loaded image. + * + * @discussion Called on a background queue. + */ +- (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image; + +@optional + +/** + * Notification that the image node finished decoding an image. + * + * @param imageNode The sender. + */ +- (void)imageNodeDidFinishDecoding:(ASNetworkImageNode *)imageNode; + +@end diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm new file mode 100644 index 0000000000..bed2e86750 --- /dev/null +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -0,0 +1,255 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASNetworkImageNode.h" + +#import +#import + + +@interface ASNetworkImageNode () +{ + ASDN::RecursiveMutex _lock; + id _cache; + id _downloader; + + // Only access any of these with _lock. + id _delegate; + + NSURL *_URL; + UIImage *_defaultImage; + + NSUUID *_cacheUUID; + id _imageDownload; + + BOOL _imageLoaded; +} + +@end + + +@implementation ASNetworkImageNode + +- (instancetype)initWithCache:(id)cache downloader:(id)downloader +{ + if (!(self = [super init])) + return nil; + + _cache = cache; + _downloader = downloader; + _shouldCacheImage = YES; + + return self; +} + +- (instancetype)init +{ + ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); +} + +- (void)dealloc +{ + [self _cancelImageDownload]; +} + +#pragma mark - Public methods -- must lock + +- (void)setURL:(NSURL *)URL +{ + [self setURL:URL resetToDefault:YES]; +} + +- (void)setURL:(NSURL *)URL resetToDefault:(BOOL)reset +{ + ASDN::MutexLocker l(_lock); + + if (URL == _URL || [URL isEqual:_URL]) { + return; + } + + [self _cancelImageDownload]; + _imageLoaded = NO; + + _URL = URL; + + if (reset || _URL == nil) + self.image = _defaultImage; + + if (self.nodeLoaded && self.layer.superlayer) + [self _lazilyLoadImageIfNecessary]; +} + +- (NSURL *)URL +{ + ASDN::MutexLocker l(_lock); + return _URL; +} + +- (void)setDefaultImage:(UIImage *)defaultImage +{ + ASDN::MutexLocker l(_lock); + + if (defaultImage == _defaultImage || [defaultImage isEqual:_defaultImage]) { + return; + } + _defaultImage = defaultImage; + + if (!_imageLoaded) { + self.image = _defaultImage; + } +} + +- (UIImage *)defaultImage +{ + ASDN::MutexLocker l(_lock); + return _defaultImage; +} + +- (void)setDelegate:(id)delegate +{ + ASDN::MutexLocker l(_lock); + _delegate = delegate; +} + +- (id)delegate +{ + ASDN::MutexLocker l(_lock); + return _delegate; +} + +- (void)didExitHierarchy +{ + [super didExitHierarchy]; + + { + ASDN::MutexLocker l(_lock); + + [self _cancelImageDownload]; + self.image = _defaultImage; + _imageLoaded = NO; + } +} + +- (void)willEnterHierarchy +{ + [super willEnterHierarchy]; + + { + ASDN::MutexLocker l(_lock); + [self _lazilyLoadImageIfNecessary]; + } +} + +#pragma mark - Private methods -- only call with lock. + +- (void)_cancelImageDownload +{ + if (!_imageDownload) { + return; + } + + [_downloader cancelImageDownloadForIdentifier:_imageDownload]; + _imageDownload = nil; + + _cacheUUID = nil; +} + +- (void)_downloadImageWithCompletion:(void (^)(CGImageRef))finished +{ + _imageDownload = [_downloader downloadImageWithURL:_URL + callbackQueue:dispatch_get_main_queue() + downloadProgressBlock:NULL + completion:^(CGImageRef responseImage, NSError *error) { + if (finished != NULL) { + finished(responseImage); + } + }]; +} + +- (void)_lazilyLoadImageIfNecessary +{ + if (!_imageLoaded && _URL != nil && _imageDownload == nil) { + if (_URL.isFileURL) { + { + ASDN::MutexLocker l(_lock); + + dispatch_async(dispatch_get_main_queue(), ^{ + _imageLoaded = YES; + + if (self.shouldCacheImage) { + self.image = [UIImage imageNamed:_URL.path]; + } else { + self.image = [UIImage imageWithContentsOfFile:_URL.path]; + } + + [_delegate imageNode:self didLoadImage:self.image]; + }); + } + } else { + // The delegate must be retained, as nothing prevents it from being deallocated during the delay before completionBlock is executed. + // Clients (the delegate) /should/ set our delegate property to nil in their -dealloc, but don't always do this. + __block id delegate = _delegate; + + void (^finished)(CGImageRef) = ^(CGImageRef responseImage) { + { + ASDN::MutexLocker l(_lock); + + if (responseImage != NULL) { + _imageLoaded = YES; + self.image = [UIImage imageWithCGImage:responseImage]; + } + + _imageDownload = nil; + + _cacheUUID = nil; + } + + if (responseImage != NULL) { + [delegate imageNode:self didLoadImage:self.image]; + } + }; + + if (_cache != nil) { + NSUUID *cacheUUID = [NSUUID UUID]; + _cacheUUID = cacheUUID; + + void (^cacheCompletion)(CGImageRef) = ^(CGImageRef image) { + // If the cache UUID changed, that means this request was cancelled. + if (![_cacheUUID isEqual:cacheUUID]) { + return; + } + + if (image == NULL && _downloader != nil) { + [self _downloadImageWithCompletion:finished]; + } else { + finished(image); + } + }; + + [_cache fetchCachedImageWithURL:_URL + callbackQueue:dispatch_get_main_queue() + completion:cacheCompletion]; + } else { + [self _downloadImageWithCompletion:finished]; + } + } + } +} + +#pragma mark - ASDisplayNode+Subclasses + +- (void)asyncdisplaykit_asyncTransactionContainerStateDidChange +{ + if (self.asyncdisplaykit_asyncTransactionContainerState == ASAsyncTransactionContainerStateNoTransactions) { + if (self.layer.contents != nil && [self.delegate respondsToSelector:@selector(imageNodeDidFinishDecoding:)]) { + [self.delegate imageNodeDidFinishDecoding:self]; + } + } +} + +@end