diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index f8b5aef2ec..c5beb9347b 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -331,6 +331,40 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) #pragma mark - Core +- (void)__tearDown:(BOOL)tearDown subnodesOfNode:(ASDisplayNode *)node +{ + for (ASDisplayNode *subnode in node.subnodes) { + if (tearDown) { + [subnode __unloadNode]; + } else { + [subnode __loadNode]; + } + } +} + +- (void)__unloadNode +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + + if (_flags.layerBacked) + _pendingViewState = [_ASPendingState pendingViewStateFromLayer:_layer]; + else + _pendingViewState = [_ASPendingState pendingViewStateFromView:_view]; + + [_view removeFromSuperview]; + _view = nil; + if (_flags.layerBacked) + _layer.delegate = nil; + [_layer removeFromSuperlayer]; + _layer = nil; +} + +- (void)__loadNode +{ + [self layer]; +} + - (ASDisplayNode *)__rasterizedContainerNode { ASDisplayNode *node = self.supernode; @@ -340,7 +374,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) } node = node.supernode; } - + return nil; } @@ -619,11 +653,22 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) { ASDisplayNodeAssertThreadAffinity(self); ASDN::MutexLocker l(_propertyLock); - + if (_flags.shouldRasterizeDescendants == flag) return; - + _flags.shouldRasterizeDescendants = flag; + + if (self.isNodeLoaded) { + //recursively tear down or build up subnodes + [self recursivelyClearContents]; + [self __tearDown:flag subnodesOfNode:self]; + if (flag == NO) { + [self _addSubnodeViewsAndLayers]; + } + + [self recursivelyDisplayImmediately]; + } } - (CGFloat)contentsScaleForDisplay @@ -653,6 +698,15 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) [[self asyncLayer] displayImmediately]; } +- (void)recursivelyDisplayImmediately +{ + ASDN::MutexLocker l(_propertyLock); + for (ASDisplayNode *child in _subnodes) { + [child recursivelyDisplayImmediately]; + } + [self displayImmediately]; +} + - (void)__setNeedsLayout { ASDisplayNodeAssertThreadAffinity(self); @@ -690,6 +744,29 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) } } +- (void)__setSafeFrame:(CGRect)rect +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + + BOOL useLayer = (_layer && ASDisplayNodeThreadIsMain()); + + CGPoint origin = (useLayer ? _layer.bounds.origin : self.bounds.origin); + CGPoint anchorPoint = (useLayer ? _layer.anchorPoint : self.anchorPoint); + + CGRect bounds = (CGRect){ origin, rect.size }; + CGPoint position = CGPointMake(rect.origin.x + rect.size.width * anchorPoint.x, + rect.origin.y + rect.size.height * anchorPoint.y); + + if (useLayer) { + _layer.bounds = bounds; + _layer.position = position; + } else { + self.bounds = bounds; + self.position = position; + } +} + // These private methods ensure that subclasses are not required to call super in order for _renderingSubnodes to be properly managed. - (void)__layout @@ -1637,10 +1714,10 @@ void recursivelyEnsureDisplayForLayer(CALayer *layer) // Assume that _layout was flattened and is 1-level deep. for (ASLayout *subnodeLayout in _layout.sublayouts) { ASDisplayNodeAssert([_subnodes containsObject:subnodeLayout.layoutableObject], @"Cached sublayouts must only contain subnodes' layout."); - ((ASDisplayNode *)subnodeLayout.layoutableObject).frame = CGRectMake(subnodeLayout.position.x, - subnodeLayout.position.y, - subnodeLayout.size.width, - subnodeLayout.size.height); + [((ASDisplayNode *)subnodeLayout.layoutableObject) __setSafeFrame:CGRectMake(subnodeLayout.position.x, + subnodeLayout.position.y, + subnodeLayout.size.width, + subnodeLayout.size.height)]; } } diff --git a/AsyncDisplayKit/ASEditableTextNode.mm b/AsyncDisplayKit/ASEditableTextNode.mm index e6b5399edf..5b929ccfb3 100644 --- a/AsyncDisplayKit/ASEditableTextNode.mm +++ b/AsyncDisplayKit/ASEditableTextNode.mm @@ -217,7 +217,7 @@ - (void)setTypingAttributes:(NSDictionary *)typingAttributes { - if (_typingAttributes == typingAttributes) + if (ASObjectIsEqual(typingAttributes, _typingAttributes)) return; _typingAttributes = [typingAttributes copy]; diff --git a/AsyncDisplayKit/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm index 2e8a8b963c..9d40a38e71 100644 --- a/AsyncDisplayKit/ASImageNode.mm +++ b/AsyncDisplayKit/ASImageNode.mm @@ -18,6 +18,7 @@ #import "ASImageNode+CGExtras.h" #import "ASInternalHelpers.h" +#import "ASEqualityHelpers.h" @interface _ASImageNodeDrawParameters : NSObject @@ -123,7 +124,7 @@ - (void)setImage:(UIImage *)image { ASDN::MutexLocker l(_imageLock); - if (_image != image) { + if (!ASObjectIsEqual(_image, image)) { _image = image; ASDN::MutexUnlocker u(_imageLock); diff --git a/AsyncDisplayKit/ASMultiplexImageNode.h b/AsyncDisplayKit/ASMultiplexImageNode.h index c1ddedae02..2178893f35 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.h +++ b/AsyncDisplayKit/ASMultiplexImageNode.h @@ -215,6 +215,18 @@ didFinishDownloadingImageWithIdentifier:(ASImageIdentifier)imageIdentifier */ - (nullable NSURL *)multiplexImageNode:(ASMultiplexImageNode *)imageNode URLForImageIdentifier:(ASImageIdentifier)imageIdentifier; +/** + * @abstract A PHAsset for the specific asset local identifier + * @param imageNode The sender. + * @param assetLocalIdentifier The local identifier for a PHAsset that this image node is loading. + * + * @discussion This optional method can improve image performance if your data source already has the PHAsset available. + * If this method is not implemented, or returns nil, the image node will request the asset from the Photos framework. + * @note This method may be called from any thread. + * @return A PHAsset corresponding to `assetLocalIdentifier`, or nil if none is available. + */ +- (PHAsset *)multiplexImageNode:(ASMultiplexImageNode *)imageNode assetForLocalIdentifier:(NSString *)assetLocalIdentifier; + @end #pragma mark - diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index 4ffb8a1275..648c9bb7a0 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -57,6 +57,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent struct { unsigned int image:1; unsigned int URL:1; + unsigned int asset:1; } _dataSourceFlags; // Image flags. @@ -66,7 +67,8 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent id _loadedImageIdentifier; id _loadingImageIdentifier; id _displayedImageIdentifier; - + __weak NSOperation *_phImageRequestOperation; + // Networking. id _downloadIdentifier; } @@ -168,12 +170,19 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent return [self initWithCache:nil downloader:nil]; // satisfy compiler } +- (void)dealloc +{ + [_phImageRequestOperation cancel]; +} + #pragma mark - ASDisplayNode Overrides - (void)clearContents { [super clearContents]; // This actually clears the contents, so we need to do this first for our displayedImageIdentifier to be meaningful. [self _setDisplayedImageIdentifier:nil withImage:nil]; + [_phImageRequestOperation cancel]; + if (_downloadIdentifier) { [_downloader cancelImageDownloadForIdentifier:_downloadIdentifier]; _downloadIdentifier = nil; @@ -185,6 +194,14 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent [super clearFetchedData]; if ([self _shouldClearFetchedImageData]) { + + [_phImageRequestOperation cancel]; + + if (_downloadIdentifier) { + [_downloader cancelImageDownloadForIdentifier:_downloadIdentifier]; + _downloadIdentifier = nil; + } + // setting this to nil makes the node fetch images the next time its display starts _loadedImageIdentifier = nil; self.image = nil; @@ -250,6 +267,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent _dataSource = dataSource; _dataSourceFlags.image = [_dataSource respondsToSelector:@selector(multiplexImageNode:imageForImageIdentifier:)]; _dataSourceFlags.URL = [_dataSource respondsToSelector:@selector(multiplexImageNode:URLForImageIdentifier:)]; + _dataSourceFlags.asset = [_dataSource respondsToSelector:@selector(multiplexImageNode:assetForLocalIdentifier:)]; } #pragma mark - @@ -268,7 +286,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent { OSSpinLockLock(&_imageIdentifiersLock); - if (_imageIdentifiers == imageIdentifiers) { + if ([_imageIdentifiers isEqual:imageIdentifiers]) { OSSpinLockUnlock(&_imageIdentifiersLock); return; } @@ -372,7 +390,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent OSSpinLockLock(&_imageIdentifiersLock); // If we've already loaded the best identifier, we've got nothing else to do. - id bestImageIdentifier = ([_imageIdentifiers count] > 0) ? _imageIdentifiers[0] : nil; + id bestImageIdentifier = _imageIdentifiers.firstObject; if (!bestImageIdentifier || [_loadedImageIdentifier isEqual:bestImageIdentifier]) { OSSpinLockUnlock(&_imageIdentifiersLock); return nil; @@ -518,17 +536,52 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent ASDisplayNodeAssertNotNil(request, @"request is required"); ASDisplayNodeAssertNotNil(completionBlock, @"completionBlock is required"); - // This is sometimes called on main but there's no reason to stay there - dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ - // Get the PHAsset itself. - PHFetchResult *assetFetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[request.assetIdentifier] options:nil]; - if ([assetFetchResult count] == 0) { + /* + * Locking rationale: + * As of iOS 9, Photos.framework will eventually deadlock if you hit it with concurrent fetch requests. rdar://22984886 + * Concurrent image requests are OK, but metadata requests aren't, so we limit ourselves to one at a time. + */ + static NSLock *phRequestLock; + static NSOperationQueue *phImageRequestQueue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + phRequestLock = [NSLock new]; + phImageRequestQueue = [NSOperationQueue new]; + phImageRequestQueue.maxConcurrentOperationCount = 10; + phImageRequestQueue.name = @"org.AsyncDisplayKit.MultiplexImageNode.phImageRequestQueue"; + }); + + // Each ASMultiplexImageNode can have max 1 inflight Photos image request operation + [_phImageRequestOperation cancel]; + + __weak __typeof(self) weakSelf = self; + NSOperation *newImageRequestOp = [NSBlockOperation blockOperationWithBlock:^{ + __strong __typeof(weakSelf) strongSelf = weakSelf; + if (strongSelf == nil) { return; } + + PHAsset *imageAsset = nil; + + // Try to get the asset immediately from the data source. + if (_dataSourceFlags.asset) { + imageAsset = [strongSelf.dataSource multiplexImageNode:strongSelf assetForLocalIdentifier:request.assetIdentifier]; + } + + // Fall back to locking and getting the PHAsset. + if (imageAsset == nil) { + [phRequestLock lock]; + // -[PHFetchResult dealloc] plays a role in the deadlock mentioned above, so we make sure the PHFetchResult is deallocated inside the critical section + @autoreleasepool { + imageAsset = [PHAsset fetchAssetsWithLocalIdentifiers:@[request.assetIdentifier] options:nil].firstObject; + } + [phRequestLock unlock]; + } + + if (imageAsset == nil) { // Error. completionBlock(nil, nil); return; } - PHAsset *imageAsset = [assetFetchResult firstObject]; PHImageRequestOptions *options = [request.options copy]; // We don't support opportunistic delivery – one request, one image. @@ -542,7 +595,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent options.synchronous = YES; } - PHImageManager *imageManager = self.imageManager ?: PHImageManager.defaultManager; + PHImageManager *imageManager = strongSelf.imageManager ?: PHImageManager.defaultManager; [imageManager requestImageForAsset:imageAsset targetSize:request.targetSize contentMode:request.contentMode options:options resultHandler:^(UIImage *image, NSDictionary *info) { if (NSThread.isMainThread) { dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ @@ -552,7 +605,9 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent completionBlock(image, info[PHImageErrorKey]); } }]; - }); + }]; + _phImageRequestOperation = newImageRequestOp; + [phImageRequestQueue addOperation:newImageRequestOp]; } - (void)_fetchImageWithIdentifierFromCache:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image))completionBlock diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index d41fd60976..3d2fad8906 100644 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -11,7 +11,7 @@ #import "ASBasicImageDownloader.h" #import "ASDisplayNode+Subclasses.h" #import "ASThread.h" - +#import "ASEqualityHelpers.h" @interface ASNetworkImageNode () { @@ -70,7 +70,7 @@ { ASDN::MutexLocker l(_lock); - if (URL == _URL || [URL isEqual:_URL]) { + if (ASObjectIsEqual(URL, _URL)) { return; } @@ -96,7 +96,7 @@ { ASDN::MutexLocker l(_lock); - if (defaultImage == _defaultImage || [defaultImage isEqual:_defaultImage]) { + if (ASObjectIsEqual(defaultImage, _defaultImage)) { return; } _defaultImage = defaultImage; diff --git a/AsyncDisplayKit/ASScrollNode.m b/AsyncDisplayKit/ASScrollNode.m index ade0ee9e5e..afead9ba8a 100644 --- a/AsyncDisplayKit/ASScrollNode.m +++ b/AsyncDisplayKit/ASScrollNode.m @@ -28,7 +28,7 @@ { return [super initWithViewBlock:^UIView *{ return [[ASScrollView alloc] init]; - }]; + } didLoadBlock:nil]; } @end diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index 5023a06f63..a88920892e 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -17,8 +17,10 @@ #import #import +#import "ASInternalHelpers.h" #import "ASTextNodeRenderer.h" #import "ASTextNodeShadower.h" +#import "ASEqualityHelpers.h" static const NSTimeInterval ASTextNodeHighlightFadeOutDuration = 0.15; static const NSTimeInterval ASTextNodeHighlightFadeInDuration = 0.1; @@ -315,7 +317,7 @@ static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncation #pragma mark - Modifying User Text - (void)setAttributedString:(NSAttributedString *)attributedString { - if (attributedString == _attributedString) { + if (ASObjectIsEqual(attributedString, _attributedString)) { return; } @@ -343,20 +345,28 @@ static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncation self.isAccessibilityElement = YES; } }); + + if (attributedString.length > 0) { + CGFloat screenScale = ASScreenScale(); + self.ascender = round([[attributedString attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL] ascender] * screenScale)/screenScale; + self.descender = round([[attributedString attribute:NSFontAttributeName atIndex:attributedString.length - 1 effectiveRange:NULL] descender] * screenScale)/screenScale; + } } #pragma mark - Text Layout - (void)setExclusionPaths:(NSArray *)exclusionPaths { - if ((_exclusionPaths == nil && exclusionPaths != nil) || (![_exclusionPaths isEqualToArray:exclusionPaths])) { - _exclusionPaths = exclusionPaths; - [self _invalidateRenderer]; - [self invalidateCalculatedLayout]; - ASDisplayNodeRespectThreadAffinityOfNode(self, ^{ - [self setNeedsDisplay]; - }); + if (ASObjectIsEqual(exclusionPaths, _exclusionPaths)) { + return; } + + _exclusionPaths = [exclusionPaths copy]; + [self _invalidateRenderer]; + [self invalidateCalculatedLayout]; + ASDisplayNodeRespectThreadAffinityOfNode(self, ^{ + [self setNeedsDisplay]; + }); } - (NSArray *)exclusionPaths @@ -965,28 +975,22 @@ static NSAttributedString *DefaultTruncationAttributedString() - (void)setTruncationAttributedString:(NSAttributedString *)truncationAttributedString { - // No-op if they're exactly equal (avoid redrawing) - if (_truncationAttributedString == truncationAttributedString) { + if (ASObjectIsEqual(_truncationAttributedString, truncationAttributedString)) { return; } - if (![_truncationAttributedString isEqual:truncationAttributedString]) { - _truncationAttributedString = [truncationAttributedString copy]; - [self _invalidateTruncationString]; - } + _truncationAttributedString = [truncationAttributedString copy]; + [self _invalidateTruncationString]; } - (void)setAdditionalTruncationMessage:(NSAttributedString *)additionalTruncationMessage { - // Short circuit if we're setting to nil (prevent redrawing when we don't need to) - if (_additionalTruncationMessage == additionalTruncationMessage) { + if (ASObjectIsEqual(_additionalTruncationMessage, additionalTruncationMessage)) { return; } - if (![_additionalTruncationMessage isEqual:additionalTruncationMessage]) { - _additionalTruncationMessage = [additionalTruncationMessage copy]; - [self _invalidateTruncationString]; - } + _additionalTruncationMessage = [additionalTruncationMessage copy]; + [self _invalidateTruncationString]; } - (void)setTruncationMode:(NSLineBreakMode)truncationMode diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index 7b07ff76ef..6a5b361ff0 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -96,6 +96,25 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; #pragma mark - Cell Layout +/* + * FIXME: Shouldn't this method, as well as `_layoutNodes:atIndexPaths:withAnimationOptions:` use the word "measure" instead? + * + * Once nodes have loaded their views, we can't layout in the background so this is a chance + * to do so immediately on the main thread. + */ +- (void)_layoutNodesWithMainThreadAffinity:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths { + NSAssert(NSThread.isMainThread, @"Main thread layout must be on the main thread."); + + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, __unused BOOL * stop) { + ASCellNode *node = nodes[idx]; + if (node.isNodeLoaded) { + ASSizeRange constrainedSize = [_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPath]; + [node measureWithSizeRange:constrainedSize]; + node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); + } + }]; +} + - (void)_layoutNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"Cell node layout must be initiated from edit transaction queue"); @@ -110,15 +129,22 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; NSInteger batchCount = MIN(kASDataControllerSizingCountPerProcessor, indexPaths.count - j); for (NSUInteger k = j; k < j + batchCount; k++) { - nodeBoundSizes[k] = [_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPaths[k]]; + ASCellNode *node = nodes[k]; + if (!node.isNodeLoaded) { + nodeBoundSizes[k] = [_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPaths[k]]; + } } dispatch_group_async(layoutGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ for (NSUInteger k = j; k < j + batchCount; k++) { ASCellNode *node = nodes[k]; - ASSizeRange constrainedSize = nodeBoundSizes[k]; - [node measureWithSizeRange:constrainedSize]; - node.frame = CGRectMake(0, 0, node.calculatedSize.width, node.calculatedSize.height); + // Only measure nodes whose views aren't loaded, since we're in the background. + // We should already have measured loaded nodes before we left the main thread, using _layoutNodesWithMainThreadAffinity: + if (!node.isNodeLoaded) { + ASSizeRange constrainedSize = nodeBoundSizes[k]; + [node measureWithSizeRange:constrainedSize]; + node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); + } } }); } @@ -245,6 +271,9 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; NSMutableArray *updatedIndexPaths = [NSMutableArray array]; [self _populateFromEntireDataSourceWithMutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; + // Measure nodes whose views are loaded before we leave the main thread + [self _layoutNodesWithMainThreadAffinity:updatedNodes atIndexPaths:updatedIndexPaths]; + [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - reloadData"); @@ -399,6 +428,9 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; NSMutableArray *updatedIndexPaths = [NSMutableArray array]; [self _populateFromDataSourceWithSectionIndexSet:indexSet mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; + // Measure nodes whose views are loaded before we leave the main thread + [self _layoutNodesWithMainThreadAffinity:updatedNodes atIndexPaths:updatedIndexPaths]; + [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - insertSections: %@", indexSet); NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:indexSet.count]; @@ -448,6 +480,9 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // For example, if an initial -reloadData call is quickly followed by -reloadSections, sizing the initial set may not be done // at this time. Thus _editingNodes could be empty and crash in ASIndexPathsForMultidimensional[...] + // Measure nodes whose views are loaded before we leave the main thread + [self _layoutNodesWithMainThreadAffinity:updatedNodes atIndexPaths:updatedIndexPaths]; + [_editingTransactionQueue addOperationWithBlock:^{ NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, sections); @@ -482,9 +517,9 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // update the section of indexpaths NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:newSection]; NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; - [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + for (NSIndexPath *indexPath in indexPaths) { [updatedIndexPaths addObject:[sectionIndexPath indexPathByAddingIndex:[indexPath indexAtPosition:indexPath.length - 1]]]; - }]; + } // Don't re-calculate size for moving [self _insertNodes:nodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; @@ -510,6 +545,9 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [nodes addObject:[_dataSource dataController:self nodeAtIndexPath:sortedIndexPaths[i]]]; } + // Measure nodes whose views are loaded before we leave the main thread + [self _layoutNodesWithMainThreadAffinity:nodes atIndexPaths:indexPaths]; + [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - insertRows: %@", indexPaths); [self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; @@ -527,6 +565,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue waitUntilAllOperationsAreFinished]; // sort indexPath in order to avoid messing up the index when deleting + // FIXME: Shouldn't deletes be sorted in descending order? NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; [_editingTransactionQueue addOperationWithBlock:^{ @@ -547,11 +586,18 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // Reloading requires re-fetching the data. Load it on the current calling thread, locking the data source. [self accessDataSourceWithBlock:^{ NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; - [indexPaths sortedArrayUsingSelector:@selector(compare:)]; - [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - [nodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; - }]; + // FIXME: This doesn't currently do anything + // FIXME: Shouldn't deletes be sorted in descending order? + [indexPaths sortedArrayUsingSelector:@selector(compare:)]; + + for (NSIndexPath *indexPath in indexPaths) { + [nodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; + } + + // Measure nodes whose views are loaded before we leave the main thread + [self _layoutNodesWithMainThreadAffinity:nodes atIndexPaths:indexPaths]; + [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - reloadRows: %@", indexPaths); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; diff --git a/AsyncDisplayKit/Details/ASTextNodeCoreTextAdditions.m b/AsyncDisplayKit/Details/ASTextNodeCoreTextAdditions.m index 55efebdce8..5ba091b3ea 100644 --- a/AsyncDisplayKit/Details/ASTextNodeCoreTextAdditions.m +++ b/AsyncDisplayKit/Details/ASTextNodeCoreTextAdditions.m @@ -149,7 +149,7 @@ NSAttributedString *ASCleanseAttributedStringOfCoreTextAttributes(NSAttributedSt return cleanAttributedString; } else { - return dirtyAttributedString; + return [dirtyAttributedString copy]; } } diff --git a/AsyncDisplayKit/Layout/ASStackLayoutDefines.h b/AsyncDisplayKit/Layout/ASStackLayoutDefines.h index 82d1aa0daf..0ca7bb3c49 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutDefines.h +++ b/AsyncDisplayKit/Layout/ASStackLayoutDefines.h @@ -45,9 +45,9 @@ typedef NS_ENUM(NSUInteger, ASStackLayoutAlignItems) { ASStackLayoutAlignItemsCenter, /** Expand children to fill cross axis */ ASStackLayoutAlignItemsStretch, - /** Children align to their first baseline. Only available for horizontal stack nodes */ + /** Children align to their first baseline. Only available for horizontal stack spec */ ASStackLayoutAlignItemsBaselineFirst, - /** Children align to their last baseline. Only available for horizontal stack nodes */ + /** Children align to their last baseline. Only available for horizontal stack spec */ ASStackLayoutAlignItemsBaselineLast, }; @@ -66,8 +66,4 @@ typedef NS_ENUM(NSUInteger, ASStackLayoutAlignSelf) { ASStackLayoutAlignSelfCenter, /** Expand to fill cross axis */ ASStackLayoutAlignSelfStretch, - /** Children align to their first baseline. Only available for horizontal stack nodes */ - ASStackLayoutAlignSelfBaselineFirst, - /** Children align to their last baseline. Only available for horizontal stack nodes */ - ASStackLayoutAlignSelfBaselineLast, }; diff --git a/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm b/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm index 3f0abf42fa..a1d27b3c5e 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm @@ -101,7 +101,6 @@ std::vector> stackChildren = std::vector>(); for (id child in self.children) { stackChildren.push_back(child); - needsBaselinePass |= child.alignSelf == ASStackLayoutAlignSelfBaselineFirst || child.alignSelf == ASStackLayoutAlignSelfBaselineLast; } const auto unpositionedLayout = ASStackUnpositionedLayout::compute(stackChildren, style, constrainedSize); @@ -109,12 +108,21 @@ CGSize finalSize = CGSizeZero; NSArray *sublayouts = nil; - if (needsBaselinePass) { - const auto baselinePositionedLayout = ASStackBaselinePositionedLayout::compute(positionedLayout, style, constrainedSize); + + // regardless of whether or not this stack aligns to baseline, we should let ASStackBaselinePositionedLayout::compute find the max ascender + // and min descender in case this spec is a child in another spec that wants to align to a baseline. + const auto baselinePositionedLayout = ASStackBaselinePositionedLayout::compute(positionedLayout, style, constrainedSize); + if (self.direction == ASStackLayoutDirectionVertical) { + ASDN::MutexLocker l(_propertyLock); + self.ascender = [[self.children firstObject] ascender]; + self.descender = [[self.children lastObject] descender]; + } else { ASDN::MutexLocker l(_propertyLock); self.ascender = baselinePositionedLayout.ascender; self.descender = baselinePositionedLayout.descender; - + } + + if (needsBaselinePass) { finalSize = directionSize(style.direction, unpositionedLayout.stackDimensionSum, baselinePositionedLayout.crossSize); sublayouts = [NSArray arrayWithObjects:&baselinePositionedLayout.sublayouts[0] count:baselinePositionedLayout.sublayouts.size()]; } else { diff --git a/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm index 98b1c2b6e6..e74cd00e35 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm @@ -117,8 +117,8 @@ static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync, // Get the display block for this node. asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:NO isCancelledBlock:isCancelledBlock rasterizing:YES]; - // We'll display something if there is a display block and/or a background color. - BOOL shouldDisplay = displayBlock || backgroundColor; + // We'll display something if there is a display block, clipping, translation and/or a background color. + BOOL shouldDisplay = displayBlock || backgroundColor || CGPointEqualToPoint(CGPointZero, frame.origin) == NO || clipsToBounds; // If we should display, then push a transform, draw the background color, and draw the contents. // The transform is popped in a block added after the recursion into subnodes. @@ -131,8 +131,12 @@ static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync, CGContextTranslateCTM(context, frame.origin.x, frame.origin.y); //support cornerRadius - if (rasterizingFromAscendent && cornerRadius && clipsToBounds) { - [[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:self.cornerRadius] addClip]; + if (rasterizingFromAscendent && clipsToBounds) { + if (cornerRadius) { + [[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius] addClip]; + } else { + [[UIBezierPath bezierPathWithRect:bounds] addClip]; + } } // Fill background if any. diff --git a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm index 977c98ec40..4909abdd46 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm @@ -143,22 +143,7 @@ ASDisplayNodeAssert(CATransform3DIsIdentity(self.transform), @"-[ASDisplayNode setFrame:] - self.transform must be identity in order to set the frame property. (From Apple's UIView documentation: If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.)"); #endif - BOOL useLayer = (_layer && ASDisplayNodeThreadIsMain()); - - CGPoint origin = (useLayer ? _layer.bounds.origin : self.bounds.origin); - CGPoint anchorPoint = (useLayer ? _layer.anchorPoint : self.anchorPoint); - - CGRect bounds = (CGRect){ origin, rect.size }; - CGPoint position = CGPointMake(rect.origin.x + rect.size.width * anchorPoint.x, - rect.origin.y + rect.size.height * anchorPoint.y); - - if (useLayer) { - _layer.bounds = bounds; - _layer.position = position; - } else { - self.bounds = bounds; - self.position = position; - } + [self __setSafeFrame:rect]; } - (void)setNeedsDisplay diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index 8283438272..c2daf155be 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -126,6 +126,11 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) { - (ASLayout *)__measureWithSizeRange:(ASSizeRange)constrainedSize; - (void)__setNeedsLayout; +/** + * Sets a new frame to this node by changing its bounds and position. This method can be safely called even if the transform property + * contains a non-identity transform, because bounds and position can be changed in such case. + */ +- (void)__setSafeFrame:(CGRect)rect; - (void)__layout; - (void)__setSupernode:(ASDisplayNode *)supernode; diff --git a/AsyncDisplayKit/Private/ASStackBaselinePositionedLayout.mm b/AsyncDisplayKit/Private/ASStackBaselinePositionedLayout.mm index 4e81dd60b5..57fd1b4c6d 100644 --- a/AsyncDisplayKit/Private/ASStackBaselinePositionedLayout.mm +++ b/AsyncDisplayKit/Private/ASStackBaselinePositionedLayout.mm @@ -93,7 +93,7 @@ ASStackBaselinePositionedLayout ASStackBaselinePositionedLayout::compute(const A const auto ascenderIt = std::max_element(positionedLayout.sublayouts.begin(), positionedLayout.sublayouts.end(), [&](const ASLayout *a, const ASLayout *b){ return a.layoutableObject.ascender < b.layoutableObject.ascender; }); - const CGFloat maxAscender = baselineIt == positionedLayout.sublayouts.end() ? 0 : (*ascenderIt).layoutableObject.ascender; + const CGFloat maxAscender = ascenderIt == positionedLayout.sublayouts.end() ? 0 : (*ascenderIt).layoutableObject.ascender; /* Step 3: Take each child and update its layout position based on the baseline offset. @@ -103,33 +103,40 @@ ASStackBaselinePositionedLayout ASStackBaselinePositionedLayout::compute(const A spacing between the two nodes is from the baseline, not the bounding box. */ - CGPoint p = CGPointZero; - BOOL first = YES; - auto stackedChildren = AS::map(positionedLayout.sublayouts, [&](ASLayout *l) -> ASLayout *{ - __weak id child = l.layoutableObject; - p = p + directionPoint(style.direction, child.spacingBefore, 0); - if (first) { - // if this is the first item use the previously computed start point - p = l.position; - } else { - // otherwise add the stack spacing - p = p + directionPoint(style.direction, style.spacing, 0); - } - first = NO; - - // Find the difference between this node's baseline and the max baseline of all the children. Add this difference to the child's y position. - l.position = p + CGPointMake(0, baselineOffset(style, l, maxAscender, maxBaseline)); - - // If we are a vertical stack, add the item's descender (it is negative) to the offset for the next node. This will ensure we are spacing - // node from baselines and not bounding boxes. - CGFloat spacingAfterBaseline = 0; - if (style.direction == ASStackLayoutDirectionVertical) { - spacingAfterBaseline = child.descender; - } - p = p + directionPoint(style.direction, stackDimension(style.direction, l.size) + child.spacingAfter + spacingAfterBaseline, 0); - - return l; - }); + std::vector stackedChildren; + // Only change positions of layouts this stackSpec is aligning to a baseline. Otherwise we are only here to + // compute the min/max descender/ascender for this stack spec. + if (style.baselineRelativeArrangement || style.alignItems == ASStackLayoutAlignItemsBaselineFirst || style.alignItems == ASStackLayoutAlignItemsBaselineLast) { + CGPoint p = CGPointZero; + BOOL first = YES; + stackedChildren = AS::map(positionedLayout.sublayouts, [&](ASLayout *l) -> ASLayout *{ + __weak id child = l.layoutableObject; + p = p + directionPoint(style.direction, child.spacingBefore, 0); + if (first) { + // if this is the first item use the previously computed start point + p = l.position; + } else { + // otherwise add the stack spacing + p = p + directionPoint(style.direction, style.spacing, 0); + } + first = NO; + + // Find the difference between this node's baseline and the max baseline of all the children. Add this difference to the child's y position. + l.position = p + CGPointMake(0, baselineOffset(style, l, maxAscender, maxBaseline)); + + // If we are a vertical stack, add the item's descender (it is negative) to the offset for the next node. This will ensure we are spacing + // node from baselines and not bounding boxes. + CGFloat spacingAfterBaseline = 0; + if (style.direction == ASStackLayoutDirectionVertical) { + spacingAfterBaseline = child.descender; + } + p = p + directionPoint(style.direction, stackDimension(style.direction, l.size) + child.spacingAfter + spacingAfterBaseline, 0); + + return l; + }); + } else { + stackedChildren = positionedLayout.sublayouts; + } /* Step 4: Since we have been mucking with positions, there is a chance that our cross size has changed. Imagine a node with a font size of 40 diff --git a/AsyncDisplayKit/Private/ASStackLayoutSpecUtilities.h b/AsyncDisplayKit/Private/ASStackLayoutSpecUtilities.h index 2b073c106a..a61218cfe4 100644 --- a/AsyncDisplayKit/Private/ASStackLayoutSpecUtilities.h +++ b/AsyncDisplayKit/Private/ASStackLayoutSpecUtilities.h @@ -63,10 +63,6 @@ inline ASStackLayoutAlignItems alignment(ASStackLayoutAlignSelf childAlignment, return ASStackLayoutAlignItemsStart; case ASStackLayoutAlignSelfStretch: return ASStackLayoutAlignItemsStretch; - case ASStackLayoutAlignSelfBaselineFirst: - return ASStackLayoutAlignItemsBaselineFirst; - case ASStackLayoutAlignSelfBaselineLast: - return ASStackLayoutAlignItemsBaselineLast; case ASStackLayoutAlignSelfAuto: default: return stackAlignment; diff --git a/AsyncDisplayKit/Private/_ASPendingState.h b/AsyncDisplayKit/Private/_ASPendingState.h index 904730b93d..4dd1146d24 100644 --- a/AsyncDisplayKit/Private/_ASPendingState.h +++ b/AsyncDisplayKit/Private/_ASPendingState.h @@ -27,4 +27,7 @@ - (void)applyToView:(UIView *)view; - (void)applyToLayer:(CALayer *)layer; ++ (_ASPendingState *)pendingViewStateFromLayer:(CALayer *)layer; ++ (_ASPendingState *)pendingViewStateFromView:(UIView *)view; + @end diff --git a/AsyncDisplayKit/Private/_ASPendingState.m b/AsyncDisplayKit/Private/_ASPendingState.m index b92f97d802..f8c2426971 100644 --- a/AsyncDisplayKit/Private/_ASPendingState.m +++ b/AsyncDisplayKit/Private/_ASPendingState.m @@ -796,4 +796,212 @@ view.accessibilityIdentifier = accessibilityIdentifier; } ++ (_ASPendingState *)pendingViewStateFromLayer:(CALayer *)layer +{ + _ASPendingState *pendingState = [[_ASPendingState alloc] init]; + + pendingState.anchorPoint = layer.anchorPoint; + (pendingState->_flags).setAnchorPoint = YES; + + pendingState.position = layer.position; + (pendingState->_flags).setPosition = YES; + + pendingState.zPosition = layer.zPosition; + (pendingState->_flags).setZPosition = YES; + + pendingState.bounds = layer.bounds; + (pendingState->_flags).setBounds = YES; + + pendingState.contentsScale = layer.contentsScale; + (pendingState->_flags).setContentsScale = YES; + + pendingState.transform = layer.transform; + (pendingState->_flags).setTransform = YES; + + pendingState.sublayerTransform = layer.sublayerTransform; + (pendingState->_flags).setSublayerTransform = YES; + + pendingState.contents = layer.contents; + (pendingState->_flags).setContents = YES; + + pendingState.clipsToBounds = layer.masksToBounds; + (pendingState->_flags).setClipsToBounds = YES; + + pendingState.backgroundColor = layer.backgroundColor; + (pendingState->_flags).setBackgroundColor = YES; + + pendingState.opaque = layer.opaque; + (pendingState->_flags).setOpaque = YES; + + pendingState.hidden = layer.hidden; + (pendingState->_flags).setHidden = YES; + + pendingState.alpha = layer.opacity; + (pendingState->_flags).setAlpha = YES; + + pendingState.cornerRadius = layer.cornerRadius; + (pendingState->_flags).setCornerRadius = YES; + + pendingState.contentMode = ASDisplayNodeUIContentModeFromCAContentsGravity(layer.contentsGravity); + (pendingState->_flags).setContentMode = YES; + + pendingState.shadowColor = layer.shadowColor; + (pendingState->_flags).setShadowColor = YES; + + pendingState.shadowOpacity = layer.shadowOpacity; + (pendingState->_flags).setShadowOpacity = YES; + + pendingState.shadowOffset = layer.shadowOffset; + (pendingState->_flags).setShadowOffset = YES; + + pendingState.shadowRadius = layer.shadowRadius; + (pendingState->_flags).setShadowRadius = YES; + + pendingState.borderWidth = layer.borderWidth; + (pendingState->_flags).setBorderWidth = YES; + + pendingState.borderColor = layer.borderColor; + (pendingState->_flags).setBorderColor = YES; + + pendingState.needsDisplayOnBoundsChange = layer.needsDisplayOnBoundsChange; + (pendingState->_flags).setNeedsDisplayOnBoundsChange = YES; + + pendingState.allowsEdgeAntialiasing = layer.allowsEdgeAntialiasing; + (pendingState->_flags).setAllowsEdgeAntialiasing = YES; + + pendingState.edgeAntialiasingMask = layer.edgeAntialiasingMask; + (pendingState->_flags).setEdgeAntialiasingMask = YES; + + return pendingState; +} + ++ (_ASPendingState *)pendingViewStateFromView:(UIView *)view +{ + _ASPendingState *pendingState = [[_ASPendingState alloc] init]; + + CALayer *layer = view.layer; + + pendingState.anchorPoint = layer.anchorPoint; + (pendingState->_flags).setAnchorPoint = YES; + + pendingState.position = layer.position; + (pendingState->_flags).setPosition = YES; + + pendingState.zPosition = layer.zPosition; + (pendingState->_flags).setZPosition = YES; + + pendingState.bounds = view.bounds; + (pendingState->_flags).setBounds = YES; + + pendingState.contentsScale = layer.contentsScale; + (pendingState->_flags).setContentsScale = YES; + + pendingState.transform = layer.transform; + (pendingState->_flags).setTransform = YES; + + pendingState.sublayerTransform = layer.sublayerTransform; + (pendingState->_flags).setSublayerTransform = YES; + + pendingState.contents = layer.contents; + (pendingState->_flags).setContents = YES; + + pendingState.clipsToBounds = view.clipsToBounds; + (pendingState->_flags).setClipsToBounds = YES; + + pendingState.backgroundColor = layer.backgroundColor; + (pendingState->_flags).setBackgroundColor = YES; + + pendingState.tintColor = view.tintColor; + (pendingState->_flags).setTintColor = YES; + + pendingState.opaque = layer.opaque; + (pendingState->_flags).setOpaque = YES; + + pendingState.hidden = view.hidden; + (pendingState->_flags).setHidden = YES; + + pendingState.alpha = view.alpha; + (pendingState->_flags).setAlpha = YES; + + pendingState.cornerRadius = layer.cornerRadius; + (pendingState->_flags).setCornerRadius = YES; + + pendingState.contentMode = view.contentMode; + (pendingState->_flags).setContentMode = YES; + + pendingState.userInteractionEnabled = view.userInteractionEnabled; + (pendingState->_flags).setUserInteractionEnabled = YES; + + pendingState.exclusiveTouch = view.exclusiveTouch; + (pendingState->_flags).setExclusiveTouch = YES; + + pendingState.shadowColor = layer.shadowColor; + (pendingState->_flags).setShadowColor = YES; + + pendingState.shadowOpacity = layer.shadowOpacity; + (pendingState->_flags).setShadowOpacity = YES; + + pendingState.shadowOffset = layer.shadowOffset; + (pendingState->_flags).setShadowOffset = YES; + + pendingState.shadowRadius = layer.shadowRadius; + (pendingState->_flags).setShadowRadius = YES; + + pendingState.borderWidth = layer.borderWidth; + (pendingState->_flags).setBorderWidth = YES; + + pendingState.borderColor = layer.borderColor; + (pendingState->_flags).setBorderColor = YES; + + pendingState.autoresizingMask = view.autoresizingMask; + (pendingState->_flags).setAutoresizingMask = YES; + + pendingState.autoresizesSubviews = view.autoresizesSubviews; + (pendingState->_flags).setAutoresizesSubviews = YES; + + pendingState.needsDisplayOnBoundsChange = layer.needsDisplayOnBoundsChange; + (pendingState->_flags).setNeedsDisplayOnBoundsChange = YES; + + pendingState.allowsEdgeAntialiasing = layer.allowsEdgeAntialiasing; + (pendingState->_flags).setAllowsEdgeAntialiasing = YES; + + pendingState.edgeAntialiasingMask = layer.edgeAntialiasingMask; + (pendingState->_flags).setEdgeAntialiasingMask = YES; + + pendingState.isAccessibilityElement = view.isAccessibilityElement; + (pendingState->_flags).setIsAccessibilityElement = YES; + + pendingState.accessibilityLabel = view.accessibilityLabel; + (pendingState->_flags).setAccessibilityLabel = YES; + + pendingState.accessibilityHint = view.accessibilityHint; + (pendingState->_flags).setAccessibilityHint = YES; + + pendingState.accessibilityValue = view.accessibilityValue; + (pendingState->_flags).setAccessibilityValue = YES; + + pendingState.accessibilityTraits = view.accessibilityTraits; + (pendingState->_flags).setAccessibilityTraits = YES; + + pendingState.accessibilityFrame = view.accessibilityFrame; + (pendingState->_flags).setAccessibilityFrame = YES; + + pendingState.accessibilityLanguage = view.accessibilityLanguage; + (pendingState->_flags).setAccessibilityLanguage = YES; + + pendingState.accessibilityElementsHidden = view.accessibilityElementsHidden; + (pendingState->_flags).setAccessibilityElementsHidden = YES; + + pendingState.accessibilityViewIsModal = view.accessibilityViewIsModal; + (pendingState->_flags).setAccessibilityViewIsModal = YES; + + pendingState.shouldGroupAccessibilityChildren = view.shouldGroupAccessibilityChildren; + (pendingState->_flags).setShouldGroupAccessibilityChildren = YES; + + pendingState.accessibilityIdentifier = view.accessibilityIdentifier; + (pendingState->_flags).setAccessibilityIdentifier = YES; + + return pendingState; +} + @end diff --git a/AsyncDisplayKitTests/ASSnapshotTestCase.h b/AsyncDisplayKitTests/ASSnapshotTestCase.h index 054560152c..dae712c29b 100644 --- a/AsyncDisplayKitTests/ASSnapshotTestCase.h +++ b/AsyncDisplayKitTests/ASSnapshotTestCase.h @@ -14,6 +14,12 @@ { \ [ASSnapshotTestCase hackilySynchronouslyRecursivelyRenderNode:node__]; \ FBSnapshotVerifyLayer(node__.layer, identifier__); \ + [node__ setShouldRasterizeDescendants:YES]; \ + [ASSnapshotTestCase hackilySynchronouslyRecursivelyRenderNode:node__]; \ + FBSnapshotVerifyLayer(node__.layer, identifier__); \ + [node__ setShouldRasterizeDescendants:NO]; \ + [ASSnapshotTestCase hackilySynchronouslyRecursivelyRenderNode:node__]; \ + FBSnapshotVerifyLayer(node__.layer, identifier__); \ } @interface ASSnapshotTestCase : FBSnapshotTestCase