Merge commit '4355f4d2eef9472642a8a8029aeef1c7fdad034f' into debug-drawrect

This commit is contained in:
Peter
2016-12-12 01:10:39 +03:00
22 changed files with 346 additions and 290 deletions

View File

@@ -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' }

View File

@@ -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];
}
}

View File

@@ -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;
}

View File

@@ -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 <ASMultiplexImageNodeDelegate>)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];

View File

@@ -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<ASImageCacheProtocol>)cache downloader:(id<ASImageDownloaderProtocol>)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<ASNetworkImageNodeDelegate> 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
* (<defaultImage>) 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 (<defaultImage>) 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 (<defaultImage>) 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 (<defaultImage>) 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 (<defaultImage>) image
* while loading and the final image after the new image data was downloaded and processed.
*/
- (void)setURL:(nullable NSURL *)URL resetToDefault:(BOOL)reset;

View File

@@ -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<ASImageCacheProtocol>)cache downloader:(id<ASImageDownloaderProtocol>)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 <ASImageContainerProtocol> 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;

View File

@@ -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;
}

View File

@@ -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 () <UIGestureRecognizerDelegate>
@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];
}

View File

@@ -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];

View File

@@ -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<id<ASLayoutElement>> *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

View File

@@ -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)

View File

@@ -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.

View File

@@ -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

View File

@@ -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<NSRange>)visibleRanges
{
ASTextKitTailTruncater *truncater = [self truncater];
[truncater truncate];
return truncater.visibleRanges;
return _truncater.visibleRanges;
}
@end

View File

@@ -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

View File

@@ -302,4 +302,10 @@
[mockDelegate verify];
}
@end
- (void)testThatSettingAnImageExternallyWillThrow
{
ASMultiplexImageNode *multiplexImageNode = [[ASMultiplexImageNode alloc] init];
XCTAssertThrows(multiplexImageNode.image = [UIImage imageNamed:@""]);
}
@end

View File

@@ -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

View File

@@ -18,6 +18,13 @@
@implementation ASTextNodeSnapshotTests
- (void)setUp
{
[super setUp];
self.recordMode = NO;
}
- (void)testTextContainerInset
{
// trivial test case to ensure ASSnapshotTestCase works

View File

@@ -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];

View File

@@ -410,7 +410,6 @@
[_videoNode didExitPreloadState];
XCTAssertNil(_videoNode.player);
XCTAssertNil(_videoNode.currentItem);
XCTAssertNil(_videoNode.image);
}
- (void)testDelegateProperlySetForClassHierarchy

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -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'."