diff --git a/AsyncDisplayKit.podspec b/AsyncDisplayKit.podspec index f23bdaa276..ebb7a56d9a 100644 --- a/AsyncDisplayKit.podspec +++ b/AsyncDisplayKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'AsyncDisplayKit' - spec.version = '2.0-rc.1' + spec.version = '2.0-rc.2' spec.license = { :type => 'BSD' } spec.homepage = 'http://asyncdisplaykit.org' spec.authors = { 'Scott Goodson' => 'scottgoodson@gmail.com' } diff --git a/AsyncDisplayKit/ASCellNode.mm b/AsyncDisplayKit/ASCellNode.mm index ed4be865f8..f77a8e4994 100644 --- a/AsyncDisplayKit/ASCellNode.mm +++ b/AsyncDisplayKit/ASCellNode.mm @@ -123,8 +123,8 @@ static NSMutableSet *__cellClassesForVisibilityNotifications = nil; // See +init - (void)_locked_displayNodeDidInvalidateSizeNewSize:(CGSize)newSize { CGSize oldSize = self.bounds.size; + [super _locked_displayNodeDidInvalidateSizeNewSize:newSize]; if (CGSizeEqualToSize(oldSize, newSize) == NO) { - self.frame = {self.frame.origin, newSize}; [self didRelayoutFromOldSize:oldSize toNewSize:newSize]; } } diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 78edb47237..9574950f0c 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -644,7 +644,9 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; return nil; } - if (indexPath.item >= [self numberOfItemsInSection:section]) { + NSInteger item = indexPath.item; + // item == NSNotFound means e.g. "scroll to this section" and is acceptable + if (item != NSNotFound && item >= [self numberOfItemsInSection:section]) { ASDisplayNodeFailAssert(@"Collection view index path has invalid item %lu in section %lu, item count = %lu", (unsigned long)indexPath.item, (unsigned long)section, (unsigned long)[self numberOfItemsInSection:section]); return nil; } diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index 6759f80ffc..34239d505f 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -18,11 +18,11 @@ #import "ASAvailability.h" #import "ASDisplayNode+Subclasses.h" #import "ASDisplayNode+FrameworkPrivate.h" +#import "ASDisplayNodeExtras.h" #import "ASLog.h" #import "ASPhotosFrameworkImageRequest.h" #import "ASEqualityHelpers.h" #import "ASInternalHelpers.h" -#import "ASDisplayNodeExtras.h" #if !AS_IOS8_SDK_OR_LATER #error ASMultiplexImageNode can be used on iOS 7, but must be linked against the iOS 8 SDK. @@ -235,7 +235,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent // setting this to nil makes the node fetch images the next time its display starts _loadedImageIdentifier = nil; - self.image = nil; + [self _setImage:nil]; } - (void)didEnterPreloadState @@ -327,6 +327,17 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent #pragma mark - Core +- (void)setImage:(UIImage *)image +{ + ASDisplayNodeAssert(NO, @"Setting the image directly on an ASMultiplexImageNode is unsafe. It will be cleared in didExitPreloadRange and will have no way to restore in didEnterPreloadRange"); + super.image = image; +} + +- (void)_setImage:(UIImage *)image +{ + super.image = image; +} + - (void)setDelegate:(id )delegate { if (_delegate == delegate) @@ -522,7 +533,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { return; } - strongSelf.image = progressImage; + [strongSelf _setImage:progressImage]; }; } [_downloader setProgressImageBlock:progress callbackQueue:dispatch_get_main_queue() withDownloadIdentifier:_downloadIdentifier]; @@ -540,7 +551,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent if (shouldReleaseImageOnBackgroundThread) { ASPerformBackgroundDeallocation(image); } - self.image = nil; + [self _setImage:nil]; } #pragma mark - @@ -869,7 +880,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent UIImage *previousImage = self.image; self.loadedImageIdentifier = imageIdentifier; - self.image = image; + [self _setImage:image]; if (_delegateFlags.updatedImage) { [_delegate multiplexImageNode:self didUpdateImage:image withIdentifier:imageIdentifier fromImage:previousImage withIdentifier:previousIdentifier]; diff --git a/AsyncDisplayKit/ASNetworkImageNode.h b/AsyncDisplayKit/ASNetworkImageNode.h index 9d2e22390a..2e5c7983cb 100644 --- a/AsyncDisplayKit/ASNetworkImageNode.h +++ b/AsyncDisplayKit/ASNetworkImageNode.h @@ -28,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN @interface ASNetworkImageNode : ASImageNode /** - * The designated initializer. Cache and Downloader are WEAK references. + * The designated initializer. Cache and Downloader are WEAK references. * * @param cache The object that implements a cache of images for the image node. Weak reference. * @param downloader The object that implements image downloading for the image node. Must not be nil. Weak reference. @@ -40,7 +40,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithCache:(nullable id)cache downloader:(id)downloader NS_DESIGNATED_INITIALIZER; /** - * Convenience initialiser. + * Convenience initializer. * * @return An ASNetworkImageNode configured to use the NSURLSession-powered ASBasicImageDownloader, and no extra cache. */ @@ -51,6 +51,17 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nullable, nonatomic, weak, readwrite) id delegate; +/** + * The image to display. + * + * @discussion By setting an image to the image property the ASNetworkImageNode will act like a plain ASImageNode. + * As soon as the URL is set the ASNetworkImageNode will act like an ASNetworkImageNode and the image property + * will be managed internally. This means the image property will be cleared out and replaced by the placeholder + * () image while loading and the final image after the new image data was downloaded and processed. + * If you want to use a placholder image functionality use the defaultImage property instead. + */ +@property (nullable, nonatomic, strong) UIImage *image; + /** * A placeholder image to display while the URL is loading. */ @@ -59,7 +70,9 @@ NS_ASSUME_NONNULL_BEGIN /** * The URL of a new image to download and display. * - * @discussion Changing this property will reset the displayed image to a placeholder () while loading. + * @discussion By setting an URL, the image property of this node will be managed internally. This means previously + * directly set images to the image property will be cleared out and replaced by the placeholder () image + * while loading and the final image after the new image data was downloaded and processed. */ @property (nullable, nonatomic, strong, readwrite) NSURL *URL; @@ -67,8 +80,11 @@ NS_ASSUME_NONNULL_BEGIN * Download and display a new image. * * @param URL The URL of a new image to download and display. - * * @param reset Whether to display a placeholder () while loading the new image. + * + * @discussion By setting an URL, the image property of this node will be managed internally. This means previously + * directly set images to the image property will be cleared out and replaced by the placeholder () image + * while loading and the final image after the new image data was downloaded and processed. */ - (void)setURL:(nullable NSURL *)URL resetToDefault:(BOOL)reset; diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index aef194180b..a2053d8c2c 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -14,12 +14,12 @@ #import "ASBasicImageDownloader.h" #import "ASDisplayNodeInternal.h" +#import "ASDisplayNodeExtras.h" #import "ASDisplayNode+Subclasses.h" #import "ASDisplayNode+FrameworkPrivate.h" #import "ASEqualityHelpers.h" #import "ASInternalHelpers.h" #import "ASImageContainerProtocolCategories.h" -#import "ASDisplayNodeExtras.h" #if PIN_REMOTE_IMAGE #import "ASPINRemoteImageDownloader.h" @@ -44,6 +44,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; id _downloadIdentifierForProgressBlock; BOOL _imageLoaded; + BOOL _imageWasSetExternally; CGFloat _currentImageQuality; CGFloat _renderedImageQuality; BOOL _shouldRenderProgressImages; @@ -69,10 +70,13 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; unsigned int cacheSupportsSynchronousFetch:1; } _cacheFlags; } + @end @implementation ASNetworkImageNode +@dynamic image; + - (instancetype)initWithCache:(id)cache downloader:(id)downloader { if (!(self = [super init])) @@ -118,6 +122,25 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; #pragma mark - Public methods -- must lock +/// Setter for public image property. It has the side effect to set an internal _imageWasSetExternally that prevents setting an image internally. Setting an image internally should happen with the _setImage: method +- (void)setImage:(UIImage *)image +{ + ASDN::MutexLocker l(__instanceLock__); + + _imageWasSetExternally = (image != nil); + if (_imageWasSetExternally) { + [self _cancelDownloadAndClearImage]; + _URL = nil; + } + + [self _setImage:image]; +} + +- (void)_setImage:(UIImage *)image +{ + super.image = image; +} + - (void)setURL:(NSURL *)URL { [self setURL:URL resetToDefault:YES]; @@ -127,6 +150,8 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; { ASDN::MutexLocker l(__instanceLock__); + _imageWasSetExternally = NO; + if (ASObjectIsEqual(URL, _URL)) { return; } @@ -138,7 +163,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; BOOL hasURL = _URL == nil; if (reset || hasURL) { - self.image = _defaultImage; + [self _setImage:_defaultImage]; /* We want to maintain the order that currentImageQuality is set regardless of the calling thread, so always use a dispatch_async to ensure that we queue the operations in the correct order. (see comment in displayDidFinish) */ @@ -173,7 +198,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; dispatch_async(dispatch_get_main_queue(), ^{ self.currentImageQuality = hasURL ? 0.0 : 1.0; }); - self.image = defaultImage; + [self _setImage:defaultImage]; } } @@ -258,7 +283,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (_imageLoaded == NO && _URL && _downloadIdentifier == nil) { UIImage *result = [[_cache synchronouslyFetchedCachedImageWithURL:_URL] asdk_image]; if (result) { - self.image = result; + [self _setImage:result]; _imageLoaded = YES; dispatch_async(dispatch_get_main_queue(), ^{ _currentImageQuality = 1.0; @@ -314,12 +339,12 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; { ASDN::MutexLocker l(__instanceLock__); - - [self _cancelImageDownload]; - [self _clearImage]; - if (_cacheFlags.cacheSupportsClearing) { - [_cache clearFetchedImageFromCacheWithURL:_URL]; + // If the image was set explicitly we don't want to remove it while exiting the preload state + if (_imageWasSetExternally) { + return; } + + [self _cancelDownloadAndClearImage]; } } @@ -329,6 +354,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; { ASDN::MutexLocker l(__instanceLock__); + // Image was set externally no need to load an image [self _lazilyLoadImageIfNecessary]; } } @@ -342,7 +368,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (ASObjectIsEqual(_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { return; } - self.image = progressImage; + [self _setImage:progressImage]; dispatch_async(dispatch_get_main_queue(), ^{ // See comment in -displayDidFinish for why this must be dispatched to main self.currentImageQuality = progress; @@ -385,25 +411,13 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; _downloadIdentifierForProgressBlock = newDownloadIDForProgressBlock; } -- (void)_clearImage +- (void)_cancelDownloadAndClearImage { - // Destruction of bigger images on the main thread can be expensive - // and can take some time, so we dispatch onto a bg queue to - // actually dealloc. - UIImage *image = self.image; - CGSize imageSize = image.size; - BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width || - imageSize.height > kMinReleaseImageOnBackgroundSize.height; - if (shouldReleaseImageOnBackgroundThread) { - ASPerformBackgroundDeallocation(image); + [self _cancelImageDownload]; + [self _clearImage]; + if (_cacheFlags.cacheSupportsClearing) { + [_cache clearFetchedImageFromCacheWithURL:_URL]; } - self.animatedImage = nil; - self.image = _defaultImage; - _imageLoaded = NO; - // See comment in -displayDidFinish for why this must be dispatched to main - dispatch_async(dispatch_get_main_queue(), ^{ - self.currentImageQuality = 0.0; - }); } - (void)_cancelImageDownload @@ -420,6 +434,27 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; _cacheUUID = nil; } +- (void)_clearImage +{ + // Destruction of bigger images on the main thread can be expensive + // and can take some time, so we dispatch onto a bg queue to + // actually dealloc. + UIImage *image = self.image; + CGSize imageSize = image.size; + BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width || + imageSize.height > kMinReleaseImageOnBackgroundSize.height; + if (shouldReleaseImageOnBackgroundThread) { + ASPerformBackgroundDeallocation(image); + } + self.animatedImage = nil; + [self _setImage:_defaultImage]; + _imageLoaded = NO; + // See comment in -displayDidFinish for why this must be dispatched to main + dispatch_async(dispatch_get_main_queue(), ^{ + self.currentImageQuality = 0.0; + }); +} + - (void)_downloadImageWithCompletion:(void (^)(id imageContainer, NSError*, id downloadIdentifier))finished { ASPerformBlockOnBackgroundThread(^{ @@ -458,7 +493,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; dispatch_async(dispatch_get_main_queue(), ^{ if (self.shouldCacheImage) { - self.image = [UIImage imageNamed:_URL.path.lastPathComponent]; + [self _setImage:[UIImage imageNamed:_URL.path.lastPathComponent]]; } else { // First try to load the path directly, for efficiency assuming a developer who // doesn't want caching is trying to be as minimal as possible. @@ -488,7 +523,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (animatedImage != nil) { self.animatedImage = animatedImage; } else { - self.image = nonAnimatedImage; + [self _setImage:nonAnimatedImage]; } } @@ -524,7 +559,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if ([imageContainer asdk_animatedImageData] && _downloaderFlags.downloaderImplementsAnimatedImage) { strongSelf.animatedImage = [_downloader animatedImageWithData:[imageContainer asdk_animatedImageData]]; } else { - strongSelf.image = [imageContainer asdk_image]; + [strongSelf _setImage:[imageContainer asdk_image]]; } dispatch_async(dispatch_get_main_queue(), ^{ strongSelf->_currentImageQuality = 1.0; diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index b98b7a7ec4..99d491ea8e 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -581,7 +581,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; return nil; } - if (indexPath.item >= [self numberOfRowsInSection:section]) { + NSInteger item = indexPath.item; + // item == NSNotFound means e.g. "scroll to this section" and is acceptable + if (item != NSNotFound && item >= [self numberOfRowsInSection:section]) { ASDisplayNodeFailAssert(@"Table view index path has invalid item %lu in section %lu, item count = %lu", (unsigned long)indexPath.item, (unsigned long)section, (unsigned long)[self numberOfRowsInSection:section]); return nil; } diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index 9eb503a13e..07079628e4 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -50,6 +50,76 @@ struct ASTextNodeDrawParameter { UIColor *backgroundColor; }; +#pragma mark - ASTextKitRenderer + +// Not used at the moment but handy to have +/*ASDISPLAYNODE_INLINE NSUInteger ASHashFromCGRect(CGRect rect) +{ + return ((*(NSUInteger *)&rect.origin.x << 10 ^ *(NSUInteger *)&rect.origin.y) + (*(NSUInteger *)&rect.size.width << 10 ^ *(NSUInteger *)&rect.size.height)); +}*/ + +ASDISPLAYNODE_INLINE NSUInteger ASHashFromCGSize(CGSize size) +{ + return ((*(NSUInteger *)&size.width << 10 ^ *(NSUInteger *)&size.height)); +} + +@interface ASTextNodeRendererKey : NSObject +@property (assign, nonatomic) ASTextKitAttributes attributes; +@property (assign, nonatomic) CGSize constrainedSize; +@end + +@implementation ASTextNodeRendererKey + +- (NSUInteger)hash +{ + return _attributes.hash() ^ ASHashFromCGSize(_constrainedSize); +} + +- (BOOL)isEqual:(ASTextNodeRendererKey *)object +{ + if (self == object) { + return YES; + } + + return _attributes == object.attributes && CGSizeEqualToSize(_constrainedSize, object.constrainedSize); +} + +@end + +static NSCache *sharedRendererCache() +{ + static dispatch_once_t onceToken; + static NSCache *__rendererCache = nil; + dispatch_once(&onceToken, ^{ + __rendererCache = [[NSCache alloc] init]; + __rendererCache.countLimit = 500; // 500 renders cache + }); + return __rendererCache; +} + +/** + The concept here is that neither the node nor layout should ever have a strong reference to the renderer object. + This is to reduce memory load when loading thousands and thousands of text nodes into memory at once. Instead + we maintain a LRU renderer cache that is queried via a unique key based on text kit attributes and constrained size. + */ + +static ASTextKitRenderer *rendererForAttributes(ASTextKitAttributes attributes, CGSize constrainedSize) +{ + NSCache *cache = sharedRendererCache(); + + ASTextNodeRendererKey *key = [[ASTextNodeRendererKey alloc] init]; + key.attributes = attributes; + key.constrainedSize = constrainedSize; + + ASTextKitRenderer *renderer = [cache objectForKey:key]; + if (renderer == nil) { + renderer = [[ASTextKitRenderer alloc] initWithTextKitAttributes:attributes constrainedSize:constrainedSize]; + [cache setObject:renderer forKey:key]; + } + + return renderer; +} + @interface ASTextNode () @end @@ -73,10 +143,6 @@ struct ASTextNodeDrawParameter { NSRange _highlightRange; ASHighlightOverlayLayer *_activeHighlightLayer; - CGSize _constrainedSize; - - ASTextKitRenderer *_renderer; - ASTextNodeDrawParameter _drawParameter; UILongPressGestureRecognizer *_longPressGestureRecognizer; @@ -123,8 +189,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; self.isAccessibilityElement = YES; self.accessibilityTraits = UIAccessibilityTraitStaticText; - _constrainedSize = CGSizeMake(-INFINITY, -INFINITY); - // Placeholders // Disabled by default in ASDisplayNode, but add a few options for those who toggle // on the special placeholder behavior of ASTextNode. @@ -139,8 +203,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; { CGColorRelease(_shadowColor); - [self _invalidateRenderer]; - if (_longPressGestureRecognizer) { _longPressGestureRecognizer.delegate = nil; [_longPressGestureRecognizer removeTarget:nil action:NULL]; @@ -181,27 +243,12 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; #pragma mark - ASDisplayNode -// FIXME: Re-evaluate if it is still the right decision to clear the renderer at this stage. -// This code was written before TextKit and when 512MB devices were still the overwhelming majority. -- (void)displayDidFinish -{ - [super displayDidFinish]; - - // We invalidate our renderer here to clear the very high memory cost of - // keeping this around. _invalidateRenderer will dealloc this onto a bg - // thread resulting in less stutters on the main thread than if it were - // to be deallocated in dealloc. This is also helpful in opportunistically - // reducing memory consumption and reducing the overall footprint of the app. - [self _invalidateRenderer]; -} - - (void)clearContents { // We discard the backing store and renderer to prevent the very large // memory overhead of maintaining these for all text nodes. They can be // regenerated when layout is necessary. [super clearContents]; // ASDisplayNode will set layer.contents = nil - [self _invalidateRenderer]; } - (void)didLoad @@ -218,45 +265,23 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } } -- (void)setFrame:(CGRect)frame -{ - [super setFrame:frame]; - [self _invalidateRendererIfNeededForBoundsSize:frame.size]; -} - -- (void)setBounds:(CGRect)bounds -{ - [super setBounds:bounds]; - [self _invalidateRendererIfNeededForBoundsSize:bounds.size]; -} - #pragma mark - Renderer Management - (ASTextKitRenderer *)_renderer { - return [self _rendererWithBounds:self.threadSafeBounds]; + CGSize constrainedSize = self.threadSafeBounds.size; + return [self _rendererWithBoundsSlow:{.size = constrainedSize}]; } -- (ASTextKitRenderer *)_rendererWithBounds:(CGRect)bounds +- (ASTextKitRenderer *)_rendererWithBoundsSlow:(CGRect)bounds { ASDN::MutexLocker l(__instanceLock__); - - if (_renderer == nil) { - CGSize constrainedSize; - if (_constrainedSize.width != -INFINITY) { - constrainedSize = _constrainedSize; - } else { - constrainedSize = bounds.size; - constrainedSize.width -= (_textContainerInset.left + _textContainerInset.right); - constrainedSize.height -= (_textContainerInset.top + _textContainerInset.bottom); - } - - _renderer = [[ASTextKitRenderer alloc] initWithTextKitAttributes:[self _rendererAttributes] - constrainedSize:constrainedSize]; - } - return _renderer; + bounds.size.width -= (_textContainerInset.left + _textContainerInset.right); + bounds.size.height -= (_textContainerInset.top + _textContainerInset.bottom); + return rendererForAttributes([self _rendererAttributes], bounds.size); } + - (ASTextKitAttributes)_rendererAttributes { ASDN::MutexLocker l(__instanceLock__); @@ -276,38 +301,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; }; } -- (void)_invalidateRendererIfNeeded -{ - [self _invalidateRendererIfNeededForBoundsSize:self.threadSafeBounds.size]; -} - -- (void)_invalidateRendererIfNeededForBoundsSize:(CGSize)boundsSize -{ - if ([self _needInvalidateRendererForBoundsSize:boundsSize]) { - // Our bounds have changed to a size that is not identical to our constraining size, - // so our previous layout information is invalid, and TextKit may draw at the - // incorrect origin. - { - ASDN::MutexLocker l(__instanceLock__); - _constrainedSize = CGSizeMake(-INFINITY, -INFINITY); - } - [self _invalidateRenderer]; - } -} - -- (void)_invalidateRenderer -{ - ASDN::MutexLocker l(__instanceLock__); - - if (_renderer) { - // Destruction of the layout managers/containers/text storage is quite - // expensive, and can take some time, so we dispatch onto a bg queue to - // actually dealloc. - ASPerformBackgroundDeallocation(_renderer); - _renderer = nil; - } -} - #pragma mark - Layout and Sizing - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset @@ -327,60 +320,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; return _textContainerInset; } -- (BOOL)_needInvalidateRendererForBoundsSize:(CGSize)boundsSize -{ - ASDN::MutexLocker l(__instanceLock__); - - if (_renderer == nil) { - return YES; - } - - // If the size is not the same as the constraint we provided to the renderer, start out assuming we need - // a new one. However, there are common cases where the constrained size doesn't need to be the same as calculated. - CGSize rendererConstrainedSize = _renderer.constrainedSize; - - //inset bounds - boundsSize.width -= _textContainerInset.left + _textContainerInset.right; - boundsSize.height -= _textContainerInset.top + _textContainerInset.bottom; - - if (CGSizeEqualToSize(boundsSize, rendererConstrainedSize)) { - return NO; - } else { - // It is very common to have a constrainedSize with a concrete, specific width but +Inf height. - // In this case, as long as the text node has bounds as large as the full calculatedLayout suggests, - // it means that the text has all the room it needs (as it was not vertically bounded). So, we will not - // experience truncation and don't need to recreate the renderer with the size it already calculated, - // as this would essentially serve to set its constrainedSize to be its calculatedSize (unnecessary). - ASLayout *layout = self.calculatedLayout; - if (layout != nil && CGSizeEqualToSize(boundsSize, layout.size)) { - return (boundsSize.width != rendererConstrainedSize.width); - } else { - return YES; - } - } -} - -- (void)calculatedLayoutDidChange -{ - [super calculatedLayoutDidChange]; - - ASLayout *layout = self.calculatedLayout; - - if (layout != nil) { - ASDN::MutexLocker l(__instanceLock__); - CGSize layoutSize = layout.size; - - // Apply textContainerInset - layoutSize.width -= (_textContainerInset.left + _textContainerInset.right); - layoutSize.height -= (_textContainerInset.top + _textContainerInset.bottom); - - if (CGSizeEqualToSize(_constrainedSize, layoutSize) == NO) { - _constrainedSize = layoutSize; - [self _invalidateRenderer]; - } - } -} - - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { ASDN::MutexLocker l(__instanceLock__); @@ -390,27 +329,18 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // Cache the original constrained size for final size calculateion CGSize originalConstrainedSize = constrainedSize; - - // Adjust constrainedSize for textContainerInset before assigning it - constrainedSize.width -= (_textContainerInset.left + _textContainerInset.right); - constrainedSize.height -= (_textContainerInset.top + _textContainerInset.bottom); - - _constrainedSize = constrainedSize; - - if (_renderer != nil && CGSizeEqualToSize(constrainedSize, _renderer.constrainedSize) == NO) { - [self _invalidateRenderer]; - } [self setNeedsDisplay]; - CGSize size = [self _renderer].size; + ASTextKitRenderer *renderer = [self _rendererWithBoundsSlow:{.size = constrainedSize}]; + CGSize size = renderer.size; if (_attributedText.length > 0) { self.style.ascender = [[self class] ascenderWithAttributedString:_attributedText]; self.style.descender = [[_attributedText attribute:NSFontAttributeName atIndex:_attributedText.length - 1 effectiveRange:NULL] descender]; - if (_renderer.currentScaleFactor > 0 && _renderer.currentScaleFactor < 1.0) { + if (renderer.currentScaleFactor > 0 && renderer.currentScaleFactor < 1.0) { // while not perfect, this is a good estimate of what the ascender of the scaled font will be. - self.style.ascender *= _renderer.currentScaleFactor; - self.style.descender *= _renderer.currentScaleFactor; + self.style.ascender *= renderer.currentScaleFactor; + self.style.descender *= renderer.currentScaleFactor; } } @@ -461,9 +391,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // Without this, the size calculation of the text with truncation applied will // not take into account the attributes of attributedText in the last line [self _updateComposedTruncationText]; - - // We need an entirely new renderer - [self _invalidateRenderer]; } NSUInteger length = attributedText.length; @@ -495,7 +422,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } _exclusionPaths = [exclusionPaths copy]; - [self _invalidateRenderer]; [self setNeedsLayout]; [self setNeedsDisplay]; } @@ -536,7 +462,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; CGContextTranslateCTM(context, _textContainerInset.left, _textContainerInset.top); - ASTextKitRenderer *renderer = [self _rendererWithBounds:drawParameterBounds]; + ASTextKitRenderer *renderer = [self _rendererWithBoundsSlow:drawParameterBounds]; // Fill background if (backgroundColor != nil) { @@ -790,11 +716,12 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; if (highlightTargetLayer != nil) { ASDN::MutexLocker l(__instanceLock__); + ASTextKitRenderer *renderer = [self _renderer]; - NSArray *highlightRects = [[self _renderer] rectsForTextRange:highlightRange measureOption:ASTextKitRendererMeasureOptionBlock]; + NSArray *highlightRects = [renderer rectsForTextRange:highlightRange measureOption:ASTextKitRendererMeasureOptionBlock]; NSMutableArray *converted = [NSMutableArray arrayWithCapacity:highlightRects.count]; for (NSValue *rectValue in highlightRects) { - UIEdgeInsets shadowPadding = _renderer.shadower.shadowPadding; + UIEdgeInsets shadowPadding = renderer.shadower.shadowPadding; CGRect rendererRect = ASTextNodeAdjustRenderRectForShadowPadding(rectValue.CGRectValue, shadowPadding); // The rects returned from renderer don't have `textContainerInset`, @@ -1119,7 +1046,6 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI CGColorRelease(_shadowColor); _shadowColor = CGColorRetain(shadowColor); _cachedShadowUIColor = [UIColor colorWithCGColor:shadowColor]; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1137,7 +1063,6 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI if (!CGSizeEqualToSize(_shadowOffset, shadowOffset)) { _shadowOffset = shadowOffset; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1155,7 +1080,6 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI if (_shadowOpacity != shadowOpacity) { _shadowOpacity = shadowOpacity; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1173,7 +1097,6 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI if (_shadowRadius != shadowRadius) { _shadowRadius = shadowRadius; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1232,7 +1155,6 @@ static NSAttributedString *DefaultTruncationAttributedString() if (_truncationMode != truncationMode) { _truncationMode = truncationMode; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1251,7 +1173,6 @@ static NSAttributedString *DefaultTruncationAttributedString() if ([_pointSizeScaleFactors isEqualToArray:pointSizeScaleFactors] == NO) { _pointSizeScaleFactors = pointSizeScaleFactors; - [self _invalidateRenderer]; [self setNeedsDisplay]; }} @@ -1261,7 +1182,6 @@ static NSAttributedString *DefaultTruncationAttributedString() if (_maximumNumberOfLines != maximumNumberOfLines) { _maximumNumberOfLines = maximumNumberOfLines; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1285,7 +1205,6 @@ static NSAttributedString *DefaultTruncationAttributedString() - (void)_invalidateTruncationText { [self _updateComposedTruncationText]; - [self _invalidateRenderer]; [self setNeedsDisplay]; } diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index 1a355a1e11..b6dca2d0f6 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -475,7 +475,7 @@ static NSString * const kRate = @"rate"; ASDN::MutexLocker l(__instanceLock__); if (ASObjectIsEqual(assetURL, self.assetURL) == NO) { - [self _setAndFetchAsset:[AVURLAsset assetWithURL:assetURL] url:assetURL]; + [self locked_setAndFetchAsset:[AVURLAsset assetWithURL:assetURL] url:assetURL]; } } @@ -497,7 +497,7 @@ static NSString * const kRate = @"rate"; ASDN::MutexLocker l(__instanceLock__); if (ASAssetIsEqual(asset, _asset) == NO) { - [self _setAndFetchAsset:asset url:nil]; + [self locked_setAndFetchAsset:asset url:nil]; } } @@ -507,9 +507,10 @@ static NSString * const kRate = @"rate"; return _asset; } -- (void)_setAndFetchAsset:(AVAsset *)asset url:(NSURL *)assetURL +- (void)locked_setAndFetchAsset:(AVAsset *)asset url:(NSURL *)assetURL { [self didExitPreloadState]; + self.videoPlaceholderImage = nil; _asset = asset; _assetURL = assetURL; [self setNeedsPreload]; diff --git a/AsyncDisplayKit/Layout/ASLayout.mm b/AsyncDisplayKit/Layout/ASLayout.mm index a64d81be2e..0df753b7fa 100644 --- a/AsyncDisplayKit/Layout/ASLayout.mm +++ b/AsyncDisplayKit/Layout/ASLayout.mm @@ -47,6 +47,16 @@ static inline NSString * descriptionIndents(NSUInteger indents) */ @property (nonatomic, getter=isFlattened) BOOL flattened; +/* + * Caches all sublayouts if set to YES or destroys the sublayout cache if set to NO. Defaults to YES + */ +@property (nonatomic, assign) BOOL retainSublayoutLayoutElements; + +/** + * Array for explicitly retain sublayout layout elements in case they are created and references in layoutSpecThatFits: and no one else will hold a strong reference on it + */ +@property (nonatomic, strong) NSMutableArray> *sublayoutLayoutElements; + @end @implementation ASLayout @@ -69,6 +79,7 @@ static inline NSString * descriptionIndents(NSUInteger indents) #endif _layoutElement = layoutElement; + // Read this now to avoid @c weak overhead later. _layoutElementType = layoutElement.layoutElementType; @@ -88,7 +99,9 @@ static inline NSString * descriptionIndents(NSUInteger indents) _sublayouts = sublayouts != nil ? [sublayouts copy] : @[]; _flattened = NO; + _retainSublayoutLayoutElements = NO; } + return self; } @@ -137,6 +150,28 @@ static inline NSString * descriptionIndents(NSUInteger indents) sublayouts:layout.sublayouts]; } +#pragma mark - Sublayout Elements Caching + +- (void)setRetainSublayoutLayoutElements:(BOOL)retainSublayoutLayoutElements +{ + if (_retainSublayoutLayoutElements != retainSublayoutLayoutElements) { + _retainSublayoutLayoutElements = retainSublayoutLayoutElements; + + if (retainSublayoutLayoutElements == NO) { + _sublayoutLayoutElements = nil; + } else { + // Add sublayouts layout elements to an internal array to retain it while the layout lives + NSUInteger sublayoutCount = _sublayouts.count; + if (sublayoutCount > 0) { + _sublayoutLayoutElements = [NSMutableArray arrayWithCapacity:sublayoutCount]; + for (ASLayout *sublayout in _sublayouts) { + [_sublayoutLayoutElements addObject:sublayout.layoutElement]; + } + } + } + } +} + #pragma mark - Layout Flattening - (ASLayout *)filteredNodeLayoutTree @@ -170,8 +205,10 @@ static inline NSString * descriptionIndents(NSUInteger indents) } queue.insert(queue.cbegin(), sublayoutContexts.begin(), sublayoutContexts.end()); } - - return [ASLayout layoutWithLayoutElement:_layoutElement size:_size sublayouts:flattenedSublayouts]; + + ASLayout *layout = [ASLayout layoutWithLayoutElement:_layoutElement size:_size position:CGPointZero sublayouts:flattenedSublayouts]; + layout.retainSublayoutLayoutElements = YES; + return layout; } #pragma mark - Accessors diff --git a/AsyncDisplayKit/TextKit/ASTextKitAttributes.h b/AsyncDisplayKit/TextKit/ASTextKitAttributes.h index 0021ec94cb..5b77fcfc52 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitAttributes.h +++ b/AsyncDisplayKit/TextKit/ASTextKitAttributes.h @@ -110,7 +110,8 @@ struct ASTextKitAttributes { && maximumNumberOfLines == other.maximumNumberOfLines && shadowOpacity == other.shadowOpacity && shadowRadius == other.shadowRadius - && [pointSizeScaleFactors isEqualToArray:other.pointSizeScaleFactors] + && (pointSizeScaleFactors == other.pointSizeScaleFactors + || [pointSizeScaleFactors isEqualToArray:other.pointSizeScaleFactors]) && CGSizeEqualToSize(shadowOffset, other.shadowOffset) && ASObjectIsEqual(exclusionPaths, other.exclusionPaths) && ASObjectIsEqual(avoidTailTruncationSet, other.avoidTailTruncationSet) diff --git a/AsyncDisplayKit/TextKit/ASTextKitContext.h b/AsyncDisplayKit/TextKit/ASTextKitContext.h index 58257efbab..9e90fac710 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitContext.h +++ b/AsyncDisplayKit/TextKit/ASTextKitContext.h @@ -29,8 +29,6 @@ exclusionPaths:(NSArray *)exclusionPaths constrainedSize:(CGSize)constrainedSize; -@property (nonatomic, assign, readwrite) CGSize constrainedSize; - /** All operations on TextKit values MUST occur within this locked context. Simultaneous access (even non-mutative) to TextKit components may cause crashes. diff --git a/AsyncDisplayKit/TextKit/ASTextKitContext.mm b/AsyncDisplayKit/TextKit/ASTextKitContext.mm index ba7477c500..7eca01d531 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitContext.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitContext.mm @@ -54,18 +54,6 @@ return self; } -- (CGSize)constrainedSize -{ - ASDN::MutexSharedLocker l(__instanceLock__); - return _textContainer.size; -} - -- (void)setConstrainedSize:(CGSize)constrainedSize -{ - ASDN::MutexSharedLocker l(__instanceLock__); - _textContainer.size = constrainedSize; -} - - (void)performBlockWithLockedTextKitComponents:(void (^)(NSLayoutManager *, NSTextStorage *, NSTextContainer *))block diff --git a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm index d4246b2a1c..1967b98264 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm @@ -37,7 +37,6 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() @implementation ASTextKitRenderer { CGSize _calculatedSize; - BOOL _sizeIsCalculated; } @synthesize attributes = _attributes, context = _context, shadower = _shadower, truncater = _truncater, fontSizeAdjuster = _fontSizeAdjuster; @@ -49,62 +48,38 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() if (self = [super init]) { _constrainedSize = constrainedSize; _attributes = attributes; - _sizeIsCalculated = NO; _currentScaleFactor = 1; - } - return self; -} - -- (ASTextKitShadower *)shadower -{ - if (!_shadower) { - ASTextKitAttributes attributes = _attributes; + + // As the renderer should be thread safe, create all subcomponents in the initialization method _shadower = [ASTextKitShadower shadowerWithShadowOffset:attributes.shadowOffset shadowColor:attributes.shadowColor shadowOpacity:attributes.shadowOpacity shadowRadius:attributes.shadowRadius]; - } - return _shadower; -} - -- (ASTextKitTailTruncater *)truncater -{ - if (!_truncater) { - ASTextKitAttributes attributes = _attributes; - NSCharacterSet *avoidTailTruncationSet = attributes.avoidTailTruncationSet ? : _defaultAvoidTruncationCharacterSet(); - _truncater = [[ASTextKitTailTruncater alloc] initWithContext:[self context] - truncationAttributedString:attributes.truncationAttributedString - avoidTailTruncationSet:avoidTailTruncationSet]; - } - return _truncater; -} - -- (ASTextKitFontSizeAdjuster *)fontSizeAdjuster -{ - if (!_fontSizeAdjuster) { - ASTextKitAttributes attributes = _attributes; - // We must inset the constrained size by the size of the shadower. - CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:_constrainedSize]; - _fontSizeAdjuster = [[ASTextKitFontSizeAdjuster alloc] initWithContext:[self context] - constrainedSize:shadowConstrainedSize - textKitAttributes:attributes]; - } - return _fontSizeAdjuster; -} - -- (ASTextKitContext *)context -{ - if (!_context) { - ASTextKitAttributes attributes = _attributes; + // We must inset the constrained size by the size of the shadower. CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:_constrainedSize]; + _context = [[ASTextKitContext alloc] initWithAttributedString:attributes.attributedString lineBreakMode:attributes.lineBreakMode maximumNumberOfLines:attributes.maximumNumberOfLines exclusionPaths:attributes.exclusionPaths constrainedSize:shadowConstrainedSize]; + + NSCharacterSet *avoidTailTruncationSet = attributes.avoidTailTruncationSet ?: _defaultAvoidTruncationCharacterSet(); + _truncater = [[ASTextKitTailTruncater alloc] initWithContext:[self context] + truncationAttributedString:attributes.truncationAttributedString + avoidTailTruncationSet:avoidTailTruncationSet]; + + ASTextKitAttributes attributes = _attributes; + // We must inset the constrained size by the size of the shadower. + _fontSizeAdjuster = [[ASTextKitFontSizeAdjuster alloc] initWithContext:[self context] + constrainedSize:shadowConstrainedSize + textKitAttributes:attributes]; + + // Calcualate size immediately + [self _calculateSize]; } - return _context; + return self; } - (NSStringDrawingContext *)stringDrawingContext @@ -127,10 +102,6 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() - (CGSize)size { - if (!_sizeIsCalculated) { - [self _calculateSize]; - _sizeIsCalculated = YES; - } return _calculatedSize; } @@ -222,12 +193,6 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() { // We add an assertion so we can track the rare conditions where a graphics context is not present ASDisplayNodeAssertNotNil(context, @"This is no good without a context."); - - // This renderer may not be the one that did the sizing. If that is the case its truncation and currentScaleFactor may not have been evaluated. - // If there's any possibility we need to truncate or scale (i.e. width is not infinite), perform the size calculation. - if (_sizeIsCalculated == NO && isinf(_constrainedSize.width) == NO) { - [self _calculateSize]; - } bounds = CGRectIntersection(bounds, { .size = _constrainedSize }); CGRect shadowInsetBounds = [[self shadower] insetRectWithConstrainedRect:bounds]; @@ -298,9 +263,7 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() - (std::vector)visibleRanges { - ASTextKitTailTruncater *truncater = [self truncater]; - [truncater truncate]; - return truncater.visibleRanges; + return _truncater.visibleRanges; } @end diff --git a/AsyncDisplayKitTests/ASDisplayNodeLayoutTests.mm b/AsyncDisplayKitTests/ASDisplayNodeLayoutTests.mm index e608989bba..394d570f86 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeLayoutTests.mm +++ b/AsyncDisplayKitTests/ASDisplayNodeLayoutTests.mm @@ -121,4 +121,54 @@ XCTAssertThrows([ASLayout layoutWithLayoutElement:displayNode size:CGSizeMake(INFINITY, INFINITY)]); } +- (void)testThatLayoutElementCreatedInLayoutSpecThatFitsDoNotGetDeallocated +{ + const CGSize kSize = CGSizeMake(300, 300); + + ASDisplayNode *subNode = [[ASDisplayNode alloc] init]; + subNode.automaticallyManagesSubnodes = YES; + subNode.layoutSpecBlock = ^(ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + ASTextNode *textNode = [ASTextNode new]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Test Test Test Test Test Test Test Test"]; + ASInsetLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:textNode]; + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:insetSpec]; + }; + + ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; + rootNode.automaticallyManagesSubnodes = YES; + rootNode.layoutSpecBlock = ^(ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + ASTextNode *textNode = [ASTextNode new]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Test Test Test Test Test"]; + ASInsetLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:textNode]; + + return [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical + spacing:0.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStretch + children:@[insetSpec, subNode]]; + }; + + rootNode.frame = CGRectMake(0, 0, kSize.width, kSize.height); + [rootNode view]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Execute measure and layout pass"]; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + [rootNode layoutThatFits:ASSizeRangeMake(kSize)]; + + dispatch_async(dispatch_get_main_queue(), ^{ + XCTAssertNoThrow([rootNode.view layoutIfNeeded]); + [expectation fulfill]; + }); + }); + + [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; +} + @end diff --git a/AsyncDisplayKitTests/ASMultiplexImageNodeTests.m b/AsyncDisplayKitTests/ASMultiplexImageNodeTests.m index 4fec68aadf..49a424b5c9 100644 --- a/AsyncDisplayKitTests/ASMultiplexImageNodeTests.m +++ b/AsyncDisplayKitTests/ASMultiplexImageNodeTests.m @@ -302,4 +302,10 @@ [mockDelegate verify]; } -@end \ No newline at end of file +- (void)testThatSettingAnImageExternallyWillThrow +{ + ASMultiplexImageNode *multiplexImageNode = [[ASMultiplexImageNode alloc] init]; + XCTAssertThrows(multiplexImageNode.image = [UIImage imageNamed:@""]); +} + +@end diff --git a/AsyncDisplayKitTests/ASNetworkImageNodeTests.m b/AsyncDisplayKitTests/ASNetworkImageNodeTests.m index b7086e6c85..18896487b1 100644 --- a/AsyncDisplayKitTests/ASNetworkImageNodeTests.m +++ b/AsyncDisplayKitTests/ASNetworkImageNodeTests.m @@ -71,6 +71,17 @@ [downloader verifyWithDelay:5]; } +- (void)testThatSettingAnImageWillStayForEnteringAndExitingPreloadState +{ + UIImage *image = [[UIImage alloc] init]; + ASNetworkImageNode *networkImageNode = [[ASNetworkImageNode alloc] init]; + networkImageNode.image = image; + [networkImageNode enterInterfaceState:ASInterfaceStatePreload]; + XCTAssertEqualObjects(image, networkImageNode.image); + [networkImageNode exitInterfaceState:ASInterfaceStatePreload]; + XCTAssertEqualObjects(image, networkImageNode.image); +} + @end @implementation ASTestImageCache diff --git a/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m b/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m index 2a2a1b704d..c0acf7ad4c 100644 --- a/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m +++ b/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m @@ -18,6 +18,13 @@ @implementation ASTextNodeSnapshotTests +- (void)setUp +{ + [super setUp]; + + self.recordMode = NO; +} + - (void)testTextContainerInset { // trivial test case to ensure ASSnapshotTestCase works diff --git a/AsyncDisplayKitTests/ASTextNodeTests.m b/AsyncDisplayKitTests/ASTextNodeTests.m index ff1620cd72..7395e15b3b 100644 --- a/AsyncDisplayKitTests/ASTextNodeTests.m +++ b/AsyncDisplayKitTests/ASTextNodeTests.m @@ -171,7 +171,9 @@ ASTextNodeTestDelegate *delegate = [ASTextNodeTestDelegate new]; _textNode.delegate = delegate; - [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))]; + ASLayout *layout = [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))]; + _textNode.frame = CGRectMake(0, 0, layout.size.width, layout.size.height); + NSRange returnedLinkRange; NSString *returnedAttributeName; NSString *returnedLinkAttributeValue = [_textNode linkAttributeValueAtPoint:CGPointMake(3, 3) attributeName:&returnedAttributeName range:&returnedLinkRange]; diff --git a/AsyncDisplayKitTests/ASVideoNodeTests.m b/AsyncDisplayKitTests/ASVideoNodeTests.m index edbc57ece1..3c835c5e2f 100644 --- a/AsyncDisplayKitTests/ASVideoNodeTests.m +++ b/AsyncDisplayKitTests/ASVideoNodeTests.m @@ -410,7 +410,6 @@ [_videoNode didExitPreloadState]; XCTAssertNil(_videoNode.player); XCTAssertNil(_videoNode.currentItem); - XCTAssertNil(_videoNode.image); } - (void)testDelegateProperlySetForClassHierarchy diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize@2x.png index 4f8364937f..65c801d4df 100644 Binary files a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize@2x.png and b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize@2x.png differ diff --git a/build.sh b/build.sh index 0c6ed44c77..ce07dc7312 100755 --- a/build.sh +++ b/build.sh @@ -292,4 +292,12 @@ if [ "$MODE" = "framework" ]; then exit 0 fi +if [ "$MODE" = "cocoapods-lint" ]; then + echo "Verifying that podspec lints." + + set -o pipefail && pod lib lint + trap - EXIT + exit 0 +fi + echo "Unrecognised mode '$MODE'."