Merge branch 'master' of https://github.com/facebook/AsyncDisplayKit into ASVideoNode-overwrittenplaceholder

This commit is contained in:
Gareth Reese
2016-06-24 11:39:12 +01:00
64 changed files with 1663 additions and 703 deletions

61
.github/GITHUB_RULES.md vendored Normal file
View File

@@ -0,0 +1,61 @@
### Contribute to ASDK's Friendly Reputation
ASDK has earned its reputation as an exceptionally welcoming place for newbie & experienced developers alike through the extra time Scott takes to thank _everyone_ who posts a question, bug, feature request or PR, for their time and contribution to the project, no matter how large the contribution (or silly the question).
###PR Reviewing
Merge permissions granted to Scott Goodson (@appleguy), Michael Schneider (@maicki), Adlai Holler (@Adlai-Holler)
**PR Type** | **Required Reviewers**
--- | ---
Documentation | Anyone
Bug Fix | 2 (external PR) or 1 (internal PR) of the following (Scott, Michael, Adlai, Levi)
Refactoring | 1-3 depending on size / author familiarity with feature
New API | Scott + component owner + 1 additional
Breaking API | Scott + component owner + 1 additional
**Component** | **Experts For Reviewing**
--- | ---
ASTextNode + subclasses | Ricky / Oliver
ASImageNode + subclasses | Garrett / Scott / Michael
ASDataController / Table / Collection | Michael
ASRangeController | Scott
ASLayout | Huy
ASDisplayNode | Garret / Michael / Levi
ASVideoNode | #asvideonode channel
###PR Merging
BE CAUTIOUS, DON'T CAUSE A REGRESSION
Try to include as much as possible:
- Description / Screenshots
- Motivation & Context
- Methods of testing / Sample app
- What type of change it is (bug fix, new feature, breaking change)
- Tag @hannahmbanana on any documentation needs*
- Title the PR with the component in brackets - e.g. "[ASTextNode] fix threading issues..."
- New files need to include the required Facebook licensing header info.
- For future viewers / potential contributors, try to describe why this PR is helpful / useful / awesome / makes an impact on the current or future community
###What stays on GitHub vs goes to Ship?
GitHub:
- active bugs
- active community discussions
- unresolved community questions
- open issue about slack channel
- open issue with list of “up-for-grabs” tasks to get involved
Ship:
- feature requests
- documentation requests
- performance optimizations / refactoring
Comment for moving to Ship:
@\<FEATURE_REQUESTOR\> The community is planning an exciting long term road map for the project and getting organized around how to deliver these feature requests.
If you are interested in helping contribute to this component or any other, dont hesitate to send us an email at AsyncDisplayKit@gmail.com or ping us on ASDK's Slack (#1582). If you would like to contribute for a few weeks, we can also add you to our Ship bug tracker so that you can see what everyone is working on and actively coordinate with us.
As always, keep filing issues and submitting pull requests here on Github and we will only move things to the new tracker if they require long term coordination.

4
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,4 @@
// The more information you include, the faster we can help you out!
// Please include: a sample project or screenshots, code snippets
// AsyncDisplayKit version, and/or backtraces for any crashes (> bt all).
// Please delete these lines before posting. Thanks!

View File

@@ -1,4 +1,7 @@
language: objective-c
cache:
- bundler
- cocoapods
osx_image: xcode7.3
git:
depth: 10
@@ -6,7 +9,7 @@ before_install:
- brew update
- brew outdated xctool || brew upgrade xctool
- brew outdated carthage || brew upgrade carthage
- gem install cocoapods -v 0.38.2
- gem install cocoapods -v 1.0.1
- gem install xcpretty
- gem install xcpretty-travis-formatter
# - gem install slather
@@ -14,7 +17,9 @@ before_install:
install: echo "<3"
env:
- MODE=tests
- MODE=examples
- MODE=examples-pt1
- MODE=examples-pt2
- MODE=examples-pt3
- MODE=life-without-cocoapods
- MODE=framework
script: ./build.sh $MODE

View File

@@ -545,6 +545,7 @@
CC3B208C1C3F7A5400798563 /* ASWeakSet.m in Sources */ = {isa = PBXBuildFile; fileRef = CC3B20881C3F7A5400798563 /* ASWeakSet.m */; };
CC3B208E1C3F7D0A00798563 /* ASWeakSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.m */; };
CC3B20901C3F892D00798563 /* ASBridgedPropertiesTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC3B208F1C3F892D00798563 /* ASBridgedPropertiesTests.mm */; };
CC4981B31D1A02BE004E13CC /* ASTableViewThrashTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */; };
CC7FD9DE1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */; settings = {ATTRIBUTES = (Public, ); }; };
CC7FD9DF1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */; };
CC7FD9E11BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */; };
@@ -935,6 +936,7 @@
CC3B20881C3F7A5400798563 /* ASWeakSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASWeakSet.m; sourceTree = "<group>"; };
CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASWeakSetTests.m; sourceTree = "<group>"; };
CC3B208F1C3F892D00798563 /* ASBridgedPropertiesTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASBridgedPropertiesTests.mm; sourceTree = "<group>"; };
CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableViewThrashTests.m; sourceTree = "<group>"; };
CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPhotosFrameworkImageRequest.h; sourceTree = "<group>"; };
CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosFrameworkImageRequest.m; sourceTree = "<group>"; };
CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosFrameworkImageRequestTests.m; sourceTree = "<group>"; };
@@ -1201,6 +1203,7 @@
052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */,
058D0A32195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m */,
3C9C128419E616EF00E942A0 /* ASTableViewTests.m */,
CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */,
058D0A33195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.m */,
254C6B511BF8FE6D003EC431 /* ASTextKitTruncationTests.mm */,
254C6B531BF8FF2A003EC431 /* ASTextKitTests.mm */,
@@ -2171,6 +2174,7 @@
2538B6F31BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m in Sources */,
058D0A39195D057000B7D73C /* ASDisplayNodeAppearanceTests.m in Sources */,
058D0A3A195D057000B7D73C /* ASDisplayNodeTests.m in Sources */,
CC4981B31D1A02BE004E13CC /* ASTableViewThrashTests.m in Sources */,
058D0A3B195D057000B7D73C /* ASDisplayNodeTestsHelper.m in Sources */,
056D21551ABCEF50001107EF /* ASImageNodeSnapshotTests.m in Sources */,
AC026B581BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m in Sources */,

View File

@@ -99,6 +99,13 @@ typedef NS_ENUM(NSUInteger, ASCellNodeVisibilityEvent) {
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER;
- (void)touchesCancelled:(nullable NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER;
/**
* Called by the system when ASCellNode is used with an ASCollectionNode. It will not be called by ASTableNode.
* When the UICollectionViewLayout object returns a new UICollectionViewLayoutAttributes object, the corresponding ASCellNode will be updated.
* See UICollectionViewCell's applyLayoutAttributes: for a full description.
*/
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes;
/**
* @abstract Initializes a cell with a given view controller block.
*

View File

@@ -211,6 +211,11 @@
#pragma clang diagnostic pop
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
// To be overriden by subclasses
}
- (void)cellNodeVisibilityEvent:(ASCellNodeVisibilityEvent)event inScrollView:(UIScrollView *)scrollView withCellFrame:(CGRect)cellFrame
{
// To be overriden by subclasses

View File

@@ -407,8 +407,9 @@ NS_ASSUME_NONNULL_BEGIN
* due to the data access in async mode.
*
* @param collectionView The sender.
* @deprecated The data source is always accessed on the main thread, and this method will not be called.
*/
- (void)collectionViewLockDataSource:(ASCollectionView *)collectionView;
- (void)collectionViewLockDataSource:(ASCollectionView *)collectionView ASDISPLAYNODE_DEPRECATED;
/**
* Indicator to unlock the data source for data fetching in async mode.
@@ -416,8 +417,9 @@ NS_ASSUME_NONNULL_BEGIN
* due to the data access in async mode.
*
* @param collectionView The sender.
* @deprecated The data source is always accessed on the main thread, and this method will not be called.
*/
- (void)collectionViewUnlockDataSource:(ASCollectionView *)collectionView;
- (void)collectionViewUnlockDataSource:(ASCollectionView *)collectionView ASDISPLAYNODE_DEPRECATED;
@end
@@ -430,6 +432,17 @@ NS_ASSUME_NONNULL_BEGIN
@optional
/**
* Informs the delegate that the collection view will add the node
* at the given index path to the view hierarchy.
*
* @param collectionView The sender.
* @param indexPath The index path of the item that will be displayed.
*
* @warning AsyncDisplayKit processes collection view edits asynchronously. The index path
* passed into this method may not correspond to the same item in your data source
* if your data source has been updated since the last edit was processed.
*/
- (void)collectionView:(ASCollectionView *)collectionView willDisplayNodeForItemAtIndexPath:(NSIndexPath *)indexPath;
/**
@@ -440,6 +453,10 @@ NS_ASSUME_NONNULL_BEGIN
* @param collectionView The sender.
* @param node The node which was removed from the view hierarchy.
* @param indexPath The index path at which the node was located before it was removed.
*
* @warning AsyncDisplayKit processes collection view edits asynchronously. The index path
* passed into this method may not correspond to the same item in your data source
* if your data source has been updated since the last edit was processed.
*/
- (void)collectionView:(ASCollectionView *)collectionView didEndDisplayingNode:(ASCellNode *)node forItemAtIndexPath:(NSIndexPath *)indexPath;
@@ -475,6 +492,10 @@ NS_ASSUME_NONNULL_BEGIN
* Informs the delegate that the collection view did remove the node which was previously
* at the given index path from the view hierarchy.
*
* @warning AsyncDisplayKit processes collection view edits asynchronously. The index path
* passed into this method may not correspond to the same item in your data source
* if your data source has been updated since the last edit was processed.
*
* This method is deprecated. Use @c collectionView:didEndDisplayingNode:forItemAtIndexPath: instead.
*/
- (void)collectionView:(ASCollectionView *)collectionView didEndDisplayingNodeForItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED;

View File

@@ -58,6 +58,11 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
_node.highlighted = highlighted;
}
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
[_node applyLayoutAttributes:layoutAttributes];
}
@end
#pragma mark -
@@ -107,7 +112,6 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
BOOL _performingBatchUpdates;
NSMutableArray *_batchUpdateBlocks;
BOOL _asyncDataFetchingEnabled;
_ASCollectionViewNodeSizeInvalidationContext *_queuedNodeSizeInvalidationContext; // Main thread only
BOOL _isDeallocating;
@@ -150,14 +154,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
unsigned int asyncDataSourceNodeForItemAtIndexPath:1;
unsigned int asyncDataSourceNodeBlockForItemAtIndexPath:1;
unsigned int asyncDataSourceNumberOfSectionsInCollectionView:1;
unsigned int asyncDataSourceCollectionViewLockDataSource:1;
unsigned int asyncDataSourceCollectionViewUnlockDataSource:1;
unsigned int asyncDataSourceCollectionViewConstrainedSizeForNodeAtIndexPath:1;
} _asyncDataSourceFlags;
}
@property (atomic, assign) BOOL asyncDataSourceLocked;
// Used only when ASCollectionView is created directly rather than through ASCollectionNode.
// We create a node so that logic related to appearance, memory management, etc can be located there
// for both the node-based and view-based version of the table.
@@ -226,7 +226,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
_rangeController.delegate = self;
_rangeController.layoutController = _layoutController;
_dataController = [[ASCollectionDataController alloc] initWithAsyncDataFetching:NO];
_dataController = [[ASCollectionDataController alloc] init];
_dataController.delegate = _rangeController;
_dataController.dataSource = self;
_dataController.environmentDelegate = self;
@@ -235,9 +235,6 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
_leadingScreensForBatching = 2.0;
_asyncDataFetchingEnabled = NO;
_asyncDataSourceLocked = NO;
_performingBatchUpdates = NO;
_batchUpdateBlocks = [NSMutableArray array];
@@ -370,9 +367,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
_asyncDataSourceFlags.asyncDataSourceNodeForItemAtIndexPath = [_asyncDataSource respondsToSelector:@selector(collectionView:nodeForItemAtIndexPath:)];
_asyncDataSourceFlags.asyncDataSourceNodeBlockForItemAtIndexPath = [_asyncDataSource respondsToSelector:@selector(collectionView:nodeBlockForItemAtIndexPath:)];
_asyncDataSourceFlags.asyncDataSourceNumberOfSectionsInCollectionView = [_asyncDataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)];
_asyncDataSourceFlags.asyncDataSourceCollectionViewLockDataSource = [_asyncDataSource respondsToSelector:@selector(collectionViewLockDataSource:)];
_asyncDataSourceFlags.asyncDataSourceCollectionViewUnlockDataSource = [_asyncDataSource respondsToSelector:@selector(collectionViewUnlockDataSource:)];
_asyncDataSourceFlags.asyncDataSourceCollectionViewConstrainedSizeForNodeAtIndexPath = [_asyncDataSource respondsToSelector:@selector(collectionView:constrainedSizeForNodeAtIndexPath:)];;
_asyncDataSourceFlags.asyncDataSourceCollectionViewConstrainedSizeForNodeAtIndexPath = [_asyncDataSource respondsToSelector:@selector(collectionView:constrainedSizeForNodeAtIndexPath:)];
// Data-source must implement collectionView:nodeForItemAtIndexPath: or collectionView:nodeBlockForItemAtIndexPath:
ASDisplayNodeAssertTrue(_asyncDataSourceFlags.asyncDataSourceNodeBlockForItemAtIndexPath || _asyncDataSourceFlags.asyncDataSourceNodeForItemAtIndexPath);
@@ -932,26 +927,6 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
}
}
- (void)dataControllerLockDataSource
{
ASDisplayNodeAssert(!self.asyncDataSourceLocked, @"The data source has already been locked");
self.asyncDataSourceLocked = YES;
if (_asyncDataSourceFlags.asyncDataSourceCollectionViewLockDataSource) {
[_asyncDataSource collectionViewLockDataSource:self];
}
}
- (void)dataControllerUnlockDataSource
{
ASDisplayNodeAssert(self.asyncDataSourceLocked, @"The data source has already been unlocked");
self.asyncDataSourceLocked = NO;
if (_asyncDataSourceFlags.asyncDataSourceCollectionViewUnlockDataSource) {
[_asyncDataSource collectionViewUnlockDataSource:self];
}
}
- (id<ASEnvironment>)dataControllerEnvironment
{
if (self.collectionNode) {
@@ -1002,6 +977,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
- (NSArray *)visibleNodeIndexPathsForRangeController:(ASRangeController *)rangeController
{
ASDisplayNodeAssertMainThread();
// Calling visibleNodeIndexPathsForRangeController: will trigger UIKit to call reloadData if it never has, which can result
// in incorrect layout if performed at zero size. We can use the fact that nothing can be visible at zero size to return fast.
BOOL isZeroSized = CGRectEqualToRect(self.bounds, CGRectZero);
@@ -1065,6 +1041,11 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
_performingBatchUpdates = NO;
}
- (void)didCompleteUpdatesInRangeController:(ASRangeController *)rangeController
{
[self _checkForBatchFetching];
}
- (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssertMainThread();

View File

@@ -273,6 +273,9 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
_preferredFrameSize = CGSizeZero;
_environmentState = ASEnvironmentStateMakeDefault();
_flags.canClearContentsOfLayer = YES;
_flags.canCallNeedsDisplayOfLayer = NO;
}
- (instancetype)init
@@ -440,6 +443,15 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
}
view = [[_viewClass alloc] init];
}
// Update flags related to special handling of UIImageView layers. More details on the flags
if (_flags.synchronous) {
if ([view isKindOfClass:[UIImageView class]]) {
_flags.canClearContentsOfLayer = NO;
} else {
_flags.canCallNeedsDisplayOfLayer = YES;
}
}
return view;
}
@@ -1515,40 +1527,36 @@ static NSInteger incrementIfFound(NSInteger i) {
[subnode __setSupernode:nil];
}
// NOTE: You must not called this method while holding the receiver's property lock. This may cause deadlocks.
- (void)removeFromSupernode
{
ASDisplayNodeAssertThreadAffinity(self);
BOOL shouldRemoveFromSuperviewOrSuperlayer = NO;
_propertyLock.lock();
__weak ASDisplayNode *supernode = _supernode;
__weak UIView *view = _view;
__weak CALayer *layer = _layer;
BOOL layerBacked = _flags.layerBacked;
_propertyLock.unlock();
{
ASDN::MutexLocker l(_propertyLock);
if (!_supernode)
return;
if (supernode == nil) {
return;
}
[supernode _removeSubnode:self];
if (self.nodeLoaded && supernode.nodeLoaded) {
// Check to ensure that our view or layer is actually inside of our supernode; otherwise, don't remove it.
// Though _ASDisplayView decouples the supernode if it is inserted inside another view hierarchy, this is
// more difficult to guarantee with _ASDisplayLayer because CoreAnimation doesn't have a -didMoveToSuperlayer.
if (self.nodeLoaded && _supernode.nodeLoaded) {
if (_flags.layerBacked || _supernode.layerBacked) {
shouldRemoveFromSuperviewOrSuperlayer = (_layer.superlayer == _supernode.layer);
} else {
shouldRemoveFromSuperviewOrSuperlayer = (_view.superview == _supernode.view);
}
}
}
// Do this before removing the view from the hierarchy, as the node will clear its supernode pointer when its view is removed from the hierarchy.
// This call may result in the object being destroyed.
[_supernode _removeSubnode:self];
if (shouldRemoveFromSuperviewOrSuperlayer) {
ASPerformBlockOnMainThread(^{
ASDN::MutexLocker l(_propertyLock);
if (_flags.layerBacked) {
[_layer removeFromSuperlayer];
if (layerBacked || supernode.layerBacked) {
if (layer.superlayer == supernode.layer) {
[layer removeFromSuperlayer];
}
} else {
[_view removeFromSuperview];
if (view.superview == supernode.view) {
[view removeFromSuperview];
}
}
});
}
@@ -1792,10 +1800,18 @@ static NSInteger incrementIfFound(NSInteger i) {
}
}
// Helper method to summarize whether or not the node run through the display process
/// Helper method to summarize whether or not the node run through the display process
- (BOOL)__implementsDisplay
{
return _flags.implementsDrawRect || _flags.implementsImageDisplay || _flags.shouldRasterizeDescendants || _flags.implementsInstanceDrawRect || _flags.implementsInstanceImageDisplay;
return _flags.implementsDrawRect || _flags.implementsImageDisplay || _flags.shouldRasterizeDescendants ||
_flags.implementsInstanceDrawRect || _flags.implementsInstanceImageDisplay;
}
// Helper method to determine if it's save to call setNeedsDisplay on a layer without throwing away the content.
// For details look at the comment on the canCallNeedsDisplayOfLayer flag
- (BOOL)__canCallNeedsDisplayOfLayer
{
return _flags.canCallNeedsDisplayOfLayer;
}
- (BOOL)placeholderShouldPersist
@@ -1845,6 +1861,13 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
// (even a runloop observer at a late call order will not stop the next frame from compositing, showing placeholders).
ASDisplayNode *node = [layer asyncdisplaykit_node];
if ([node __canCallNeedsDisplayOfLayer]) {
// Layers for UIKit components that are wrapped wtihin a node needs to be set to be displayed as the contents of
// the layer get's cleared and would not be recreated otherwise
[layer setNeedsDisplay];
}
if ([node __implementsDisplay]) {
// For layers that do get displayed here, this immediately kicks off the work on the concurrent -[_ASDisplayLayer displayQueue].
// At the same time, it creates an associated _ASAsyncTransaction, which we can use to block on display completion. See ASDisplayNode+AsyncDisplay.mm.
@@ -2092,8 +2115,11 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
- (void)clearContents
{
// No-op if these haven't been created yet, as that guarantees they don't have contents that needs to be released.
_layer.contents = nil;
if (_flags.canClearContentsOfLayer) {
// No-op if these haven't been created yet, as that guarantees they don't have contents that needs to be released.
_layer.contents = nil;
}
_placeholderLayer.contents = nil;
_placeholderImage = nil;
}

View File

@@ -192,7 +192,9 @@
{
ASTextKitComponents *displayedComponents = [self isDisplayingPlaceholder] ? _placeholderTextKitComponents : _textKitComponents;
CGSize textSize = [displayedComponents sizeForConstrainedWidth:constrainedSize.width];
return CGSizeMake(fminf(ceilf(textSize.width), constrainedSize.width), fminf(ceilf(textSize.height), constrainedSize.height));
CGFloat width = ceilf(textSize.width + _textContainerInset.left + _textContainerInset.right);
CGFloat height = ceilf(textSize.height + _textContainerInset.top + _textContainerInset.bottom);
return CGSizeMake(fminf(width, constrainedSize.width), fminf(height, constrainedSize.height));
}
- (void)layout

View File

@@ -22,7 +22,7 @@
#import "ASInternalHelpers.h"
#import "ASWeakProxy.h"
NSString *const ASAnimatedImageDefaultRunLoopMode = NSDefaultRunLoopMode;
NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes;
@implementation ASImageNode (AnimatedImage)
@@ -45,6 +45,10 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSDefaultRunLoopMode;
};
}
if (animatedImage.playbackReady) {
[self animatedImageFileReady];
}
animatedImage.playbackReadyCallback = ^{
[weakSelf animatedImageFileReady];
};

View File

@@ -144,9 +144,11 @@ typedef UIImage * _Nullable (^asimagenode_modification_block_t)(UIImage *image);
@property (atomic, assign) BOOL animatedImagePaused;
/**
* @abstract The runloop mode used to animte th image.
* @abstract The runloop mode used to animate the image.
*
* @discussion Defaults to NSDefaultRunLoopMode. Another commonly used mode is NSRunLoopCommonModes.
* @discussion Defaults to NSRunLoopCommonModes. Another commonly used mode is NSDefaultRunLoopMode.
* Setting NSDefaultRunLoopMode will cause animation to pause while scrolling (if the ASImageNode is
* in a scroll view), which may improve scroll performance in some use cases.
*/
@property (atomic, strong) NSString *animatedImageRunLoopMode;

View File

@@ -26,46 +26,18 @@
#import "ASInternalHelpers.h"
#import "ASEqualityHelpers.h"
@interface _ASImageNodeDrawParameters : NSObject
@property (nonatomic, retain) UIImage *image;
@property (nonatomic, assign) BOOL opaque;
@property (nonatomic, assign) CGRect bounds;
@property (nonatomic, assign) CGFloat contentsScale;
@property (nonatomic, strong) UIColor *backgroundColor;
@property (nonatomic, assign) UIViewContentMode contentMode;
@end
// TODO: eliminate explicit parameters with a set of keys copied from the node
@implementation _ASImageNodeDrawParameters
- (instancetype)initWithImage:(UIImage *)image
bounds:(CGRect)bounds
opaque:(BOOL)opaque
contentsScale:(CGFloat)contentsScale
backgroundColor:(UIColor *)backgroundColor
contentMode:(UIViewContentMode)contentMode
{
if (!(self = [self init]))
return nil;
_image = image;
_opaque = opaque;
_bounds = bounds;
_contentsScale = contentsScale;
_backgroundColor = backgroundColor;
_contentMode = contentMode;
return self;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@ : %p opaque:%@ bounds:%@ contentsScale:%.2f backgroundColor:%@ contentMode:%@>", [self class], self, @(self.opaque), NSStringFromCGRect(self.bounds), self.contentsScale, self.backgroundColor, ASDisplayNodeNSStringFromUIContentMode(self.contentMode)];
}
@end
struct ASImageNodeDrawParameters {
BOOL opaque;
CGRect bounds;
CGFloat contentsScale;
UIColor *backgroundColor;
UIViewContentMode contentMode;
BOOL cropEnabled;
BOOL forceUpscaling;
CGRect cropRect;
CGRect cropDisplayBounds;
asimagenode_modification_block_t imageModificationBlock;
};
@implementation ASImageNode
{
@@ -75,18 +47,32 @@
void (^_displayCompletionBlock)(BOOL canceled);
ASDN::RecursiveMutex _imageLock;
// Drawing
ASImageNodeDrawParameters _drawParameter;
ASTextNode *_debugLabelNode;
// Cropping.
BOOL _cropEnabled; // Defaults to YES.
BOOL _forceUpscaling; //Defaults to NO.
CGRect _cropRect; // Defaults to CGRectMake(0.5, 0.5, 0, 0)
CGRect _cropDisplayBounds;
ASTextNode *_debugLabelNode;
CGRect _cropDisplayBounds; // Defaults to CGRectNull
}
@synthesize image = _image;
@synthesize imageModificationBlock = _imageModificationBlock;
#pragma mark - NSObject
+ (void)initialize
{
[super initialize];
if (self != [ASImageNode class]) {
// Prevent custom drawing in subclasses
ASDisplayNodeAssert(!ASSubclassOverridesClassSelector([ASImageNode class], self, @selector(displayWithParameters:isCancelled:)), @"Subclass %@ must not override displayWithParameters:isCancelled: method. Custom drawing in %@ subclass is not supported.", NSStringFromClass(self), NSStringFromClass([ASImageNode class]));
}
}
- (instancetype)init
{
if (!(self = [super init]))
@@ -124,6 +110,8 @@
return nil;
}
#pragma mark - Layout and Sizing
- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize
{
ASDN::MutexLocker l(_imageLock);
@@ -136,6 +124,8 @@
return CGSizeZero;
}
#pragma mark - Setter / Getter
- (void)setImage:(UIImage *)image
{
_imageLock.lock();
@@ -177,54 +167,72 @@
self.placeholderEnabled = placeholderColor != nil;
}
#pragma mark - Drawing
- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer
{
return [[_ASImageNodeDrawParameters alloc] initWithImage:self.image
bounds:self.bounds
opaque:self.opaque
contentsScale:self.contentsScaleForDisplay
backgroundColor:self.backgroundColor
contentMode:self.contentMode];
ASDN::MutexLocker l(_imageLock);
_drawParameter = {
.bounds = self.bounds,
.opaque = self.opaque,
.contentsScale = _contentsScaleForDisplay,
.backgroundColor = self.backgroundColor,
.contentMode = self.contentMode,
.cropEnabled = _cropEnabled,
.forceUpscaling = _forceUpscaling,
.cropRect = _cropRect,
.cropDisplayBounds = _cropDisplayBounds,
.imageModificationBlock = _imageModificationBlock
};
return nil;
}
- (NSDictionary *)debugLabelAttributes
{
return @{ NSFontAttributeName: [UIFont systemFontOfSize:15.0],
NSForegroundColorAttributeName: [UIColor redColor] };
return @{
NSFontAttributeName: [UIFont systemFontOfSize:15.0],
NSForegroundColorAttributeName: [UIColor redColor]
};
}
- (UIImage *)displayWithParameters:(_ASImageNodeDrawParameters *)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled
- (UIImage *)displayWithParameters:(id<NSObject> *)parameter isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled
{
UIImage *image = parameters.image;
if (!image) {
UIImage *image = self.image;
if (image == nil) {
return nil;
}
CGRect drawParameterBounds = CGRectZero;
BOOL forceUpscaling = NO;
BOOL cropEnabled = NO;
BOOL isOpaque = parameters.opaque;
UIColor *backgroundColor = parameters.backgroundColor;
UIViewContentMode contentMode = parameters.contentMode;
BOOL cropEnabled = YES;
BOOL isOpaque = NO;
UIColor *backgroundColor = nil;
UIViewContentMode contentMode = UIViewContentModeScaleAspectFill;
CGFloat contentsScale = 0.0;
CGRect cropDisplayBounds = CGRectZero;
CGRect cropRect = CGRectZero;
asimagenode_modification_block_t imageModificationBlock;
ASDN::MutexLocker l(_imageLock);
{
ASDN::MutexLocker l(_imageLock);
ASImageNodeDrawParameters drawParameter = _drawParameter;
// FIXME: There is a small risk of these values changing between the main thread creation of drawParameters, and the execution of this method.
// We should package these up into the draw parameters object. Might be easiest to create a struct for the non-objects and make it one property.
cropEnabled = _cropEnabled;
forceUpscaling = _forceUpscaling;
contentsScale = _contentsScaleForDisplay;
cropDisplayBounds = _cropDisplayBounds;
cropRect = _cropRect;
imageModificationBlock = _imageModificationBlock;
drawParameterBounds = drawParameter.bounds;
forceUpscaling = drawParameter.forceUpscaling;
cropEnabled = drawParameter.cropEnabled;
isOpaque = drawParameter.opaque;
backgroundColor = drawParameter.backgroundColor;
contentMode = drawParameter.contentMode;
contentsScale = drawParameter.contentsScale;
cropDisplayBounds = drawParameter.cropDisplayBounds;
cropRect = drawParameter.cropRect;
imageModificationBlock = drawParameter.imageModificationBlock;
}
BOOL hasValidCropBounds = cropEnabled && !CGRectIsNull(cropDisplayBounds) && !CGRectIsEmpty(cropDisplayBounds);
CGRect bounds = (hasValidCropBounds ? cropDisplayBounds : parameters.bounds);
CGRect bounds = (hasValidCropBounds ? cropDisplayBounds : drawParameterBounds);
ASDisplayNodeContextModifier preContextBlock = self.willDisplayNodeContentWithRenderingContext;
ASDisplayNodeContextModifier postContextBlock = self.didDisplayNodeContentWithRenderingContext;
@@ -359,7 +367,6 @@
}
}
#pragma mark -
- (void)setNeedsDisplayWithCompletion:(void (^ _Nullable)(BOOL canceled))displayCompletionBlock
{
if (self.displaySuspended) {
@@ -378,6 +385,7 @@
}
#pragma mark - Cropping
- (BOOL)isCropEnabled
{
ASDN::MutexLocker l(_imageLock);
@@ -462,6 +470,7 @@
}
#pragma mark - Debug
- (void)layout
{
[super layout];
@@ -477,6 +486,7 @@
@end
#pragma mark - Extras
extern asimagenode_modification_block_t ASImageNodeRoundBorderModificationBlock(CGFloat borderWidth, UIColor *borderColor)
{
return ^(UIImage *originalImage) {

View File

@@ -44,9 +44,11 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
CGFloat _currentImageQuality;
CGFloat _renderedImageQuality;
// TODO: Move this to flags
BOOL _delegateSupportsDidStartFetchingData;
BOOL _delegateSupportsDidFailWithError;
BOOL _delegateSupportsImageNodeDidFinishDecoding;
BOOL _delegateSupportsDidFinishDecoding;
BOOL _delegateSupportsDidLoadImage;
BOOL _shouldRenderProgressImages;
@@ -214,7 +216,8 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
_delegateSupportsDidStartFetchingData = [delegate respondsToSelector:@selector(imageNodeDidStartFetchingData:)];
_delegateSupportsDidFailWithError = [delegate respondsToSelector:@selector(imageNode:didFailWithError:)];
_delegateSupportsImageNodeDidFinishDecoding = [delegate respondsToSelector:@selector(imageNodeDidFinishDecoding:)];
_delegateSupportsDidFinishDecoding = [delegate respondsToSelector:@selector(imageNodeDidFinishDecoding:)];
_delegateSupportsDidLoadImage = [delegate respondsToSelector:@selector(imageNode:didLoadImage:)];
}
- (id<ASNetworkImageNodeDelegate>)delegate
@@ -494,7 +497,9 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
dispatch_async(dispatch_get_main_queue(), ^{
self.currentImageQuality = 1.0;
});
[_delegate imageNode:self didLoadImage:self.image];
if (_delegateSupportsDidLoadImage) {
[_delegate imageNode:self didLoadImage:self.image];
}
});
}
} else {
@@ -529,7 +534,9 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
strongSelf->_cacheUUID = nil;
if (imageContainer != nil) {
[strongSelf->_delegate imageNode:strongSelf didLoadImage:strongSelf.image];
if (strongSelf->_delegateSupportsDidLoadImage) {
[strongSelf->_delegate imageNode:strongSelf didLoadImage:strongSelf.image];
}
}
else if (error && strongSelf->_delegateSupportsDidFailWithError) {
[strongSelf->_delegate imageNode:strongSelf didFailWithError:error];
@@ -581,7 +588,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
[super displayDidFinish];
ASDN::MutexLocker l(_lock);
if (_delegateSupportsImageNodeDidFinishDecoding && self.layer.contents != nil) {
if (_delegateSupportsDidFinishDecoding && self.layer.contents != nil) {
/* We store the image quality in _currentImageQuality whenever _image is set. On the following displayDidFinish, we'll know that
_currentImageQuality is the quality of the image that has just finished rendering. In order for this to be accurate, we
need to be sure we are on main thread when we set _currentImageQuality. Otherwise, it is possible for _currentImageQuality

View File

@@ -44,18 +44,8 @@ NS_ASSUME_NONNULL_BEGIN
* The frame of the table view changes as table cells are added and deleted.
*
* @param style A constant that specifies the style of the table view. See UITableViewStyle for descriptions of valid constants.
*
* @param asyncDataFetchingEnabled This option is reserved for future use, and currently a no-op.
*
* @discussion If asyncDataFetching is enabled, the `ASTableView` will fetch data through `tableView:numberOfRowsInSection:` and
* `tableView:nodeForRowAtIndexPath:` in async mode from background thread. Otherwise, the methods will be invoked synchronically
* from calling thread.
* Enabling asyncDataFetching could avoid blocking main thread for `ASCellNode` allocation, which is frequently reported issue for
* large scale data. On another hand, the application code need take the responsibility to avoid data inconsistence. Specifically,
* we will lock the data source through `tableViewLockDataSource`, and unlock it by `tableViewUnlockDataSource` after the data fetching.
* The application should not update the data source while the data source is locked, to keep data consistence.
*/
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style asyncDataFetching:(BOOL)asyncDataFetchingEnabled;
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style;
/**
* Tuning parameters for a range type in full mode.
@@ -363,8 +353,9 @@ NS_ASSUME_NONNULL_BEGIN
* due to the data access in async mode.
*
* @param tableView The sender.
* @deprecated The data source is always accessed on the main thread, and this method will not be called.
*/
- (void)tableViewLockDataSource:(ASTableView *)tableView;
- (void)tableViewLockDataSource:(ASTableView *)tableView ASDISPLAYNODE_DEPRECATED;
/**
* Indicator to unlock the data source for data fetching in asyn mode.
@@ -372,8 +363,9 @@ NS_ASSUME_NONNULL_BEGIN
* due to the data access in async mode.
*
* @param tableView The sender.
* @deprecated The data source is always accessed on the main thread, and this method will not be called.
*/
- (void)tableViewUnlockDataSource:(ASTableView *)tableView;
- (void)tableViewUnlockDataSource:(ASTableView *)tableView ASDISPLAYNODE_DEPRECATED;
@end
@@ -390,6 +382,17 @@ NS_ASSUME_NONNULL_BEGIN
@optional
/**
* Informs the delegate that the table view will add the node
* at the given index path to the view hierarchy.
*
* @param tableView The sender.
* @param indexPath The index path of the row that will be displayed.
*
* @warning AsyncDisplayKit processes table view edits asynchronously. The index path
* passed into this method may not correspond to the same item in your data source
* if your data source has been updated since the last edit was processed.
*/
- (void)tableView:(ASTableView *)tableView willDisplayNodeForRowAtIndexPath:(NSIndexPath *)indexPath;
/**
@@ -434,6 +437,10 @@ NS_ASSUME_NONNULL_BEGIN
* Informs the delegate that the table view did remove the node which was previously
* at the given index path from the view hierarchy.
*
* @warning AsyncDisplayKit processes table view edits asynchronously. The index path
* passed into this method may not correspond to the same item in your data source
* if your data source has been updated since the last edit was processed.
*
* This method is deprecated. Use @c tableView:didEndDisplayingNode:forRowAtIndexPath: instead.
*/
- (void)tableView:(ASTableView *)tableView didEndDisplayingNodeForRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED;
@@ -443,4 +450,10 @@ NS_ASSUME_NONNULL_BEGIN
@protocol ASTableViewDelegate <ASTableDelegate>
@end
@interface ASTableView (Deprecated)
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style asyncDataFetching:(BOOL)asyncDataFetchingEnabled ASDISPLAYNODE_DEPRECATED;
@end
NS_ASSUME_NONNULL_END

View File

@@ -100,8 +100,6 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
ASRangeController *_rangeController;
BOOL _asyncDataFetchingEnabled;
ASBatchContext *_batchContext;
NSIndexPath *_pendingVisibleIndexPath;
@@ -133,12 +131,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
unsigned int asyncDataSourceNumberOfSectionsInTableView:1;
unsigned int asyncDataSourceTableViewNodeBlockForRowAtIndexPath:1;
unsigned int asyncDataSourceTableViewNodeForRowAtIndexPath:1;
unsigned int asyncDataSourceTableViewLockDataSource:1;
unsigned int asyncDataSourceTableViewUnlockDataSource:1;
} _asyncDataSourceFlags;
}
@property (atomic, assign) BOOL asyncDataSourceLocked;
@property (nonatomic, strong, readwrite) ASDataController *dataController;
// Used only when ASTableView is created directly rather than through ASTableNode.
@@ -177,16 +172,13 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
_rangeController.dataSource = self;
_rangeController.delegate = self;
_dataController = [[dataControllerClass alloc] initWithAsyncDataFetching:NO];
_dataController = [[dataControllerClass alloc] init];
_dataController.dataSource = self;
_dataController.delegate = _rangeController;
_dataController.environmentDelegate = self;
_layoutController.dataSource = _dataController;
_asyncDataFetchingEnabled = NO;
_asyncDataSourceLocked = NO;
_leadingScreensForBatching = 2.0;
_batchContext = [[ASBatchContext alloc] init];
@@ -290,8 +282,6 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
_asyncDataSourceFlags.asyncDataSourceNumberOfSectionsInTableView = [_asyncDataSource respondsToSelector:@selector(numberOfSectionsInTableView:)];
_asyncDataSourceFlags.asyncDataSourceTableViewNodeForRowAtIndexPath = [_asyncDataSource respondsToSelector:@selector(tableView:nodeForRowAtIndexPath:)];
_asyncDataSourceFlags.asyncDataSourceTableViewNodeBlockForRowAtIndexPath = [_asyncDataSource respondsToSelector:@selector(tableView:nodeBlockForRowAtIndexPath:)];
_asyncDataSourceFlags.asyncDataSourceTableViewLockDataSource = [_asyncDataSource respondsToSelector:@selector(tableViewLockDataSource:)];
_asyncDataSourceFlags.asyncDataSourceTableViewUnlockDataSource = [_asyncDataSource respondsToSelector:@selector(tableViewUnlockDataSource:)];
// Data source must implement tableView:nodeBlockForRowAtIndexPath: or tableView:nodeForRowAtIndexPath:
ASDisplayNodeAssertTrue(_asyncDataSourceFlags.asyncDataSourceTableViewNodeBlockForRowAtIndexPath || _asyncDataSourceFlags.asyncDataSourceTableViewNodeForRowAtIndexPath);
@@ -968,6 +958,11 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
}
}
- (void)didCompleteUpdatesInRangeController:(ASRangeController *)rangeController
{
[self _checkForBatchFetching];
}
- (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssertMainThread();
@@ -1078,28 +1073,6 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
CGSizeMake(_nodesConstrainedWidth, FLT_MAX));
}
- (void)dataControllerLockDataSource
{
ASDisplayNodeAssert(!self.asyncDataSourceLocked, @"The data source has already been locked");
self.asyncDataSourceLocked = YES;
if (_asyncDataSourceFlags.asyncDataSourceTableViewLockDataSource) {
[_asyncDataSource tableViewLockDataSource:self];
}
}
- (void)dataControllerUnlockDataSource
{
ASDisplayNodeAssert(self.asyncDataSourceLocked, @"The data source has already been unlocked");
self.asyncDataSourceLocked = NO;
if (_asyncDataSourceFlags.asyncDataSourceTableViewUnlockDataSource) {
[_asyncDataSource tableViewUnlockDataSource:self];
}
}
- (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section
{
return [_asyncDataSource tableView:self numberOfRowsInSection:section];

View File

@@ -11,22 +11,19 @@
#import "ASTextNode.h"
#import "ASTextNode+Beta.h"
#include <mutex>
#import <AsyncDisplayKit/_ASDisplayLayer.h>
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASDisplayNodeInternal.h>
#import <AsyncDisplayKit/ASHighlightOverlayLayer.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import "ASTextKitCoreTextAdditions.h"
#import "ASTextKitComponents.h"
#import "ASTextKitFontSizeAdjuster.h"
#import "ASTextKitRenderer.h"
#import "ASTextKitRenderer+Positioning.h"
#import "ASTextKitShadower.h"
#import "ASInternalHelpers.h"
#import "ASEqualityHelpers.h"
#import "ASLayout.h"
static const NSTimeInterval ASTextNodeHighlightFadeOutDuration = 0.15;
@@ -35,27 +32,10 @@ static const CGFloat ASTextNodeHighlightLightOpacity = 0.11;
static const CGFloat ASTextNodeHighlightDarkOpacity = 0.22;
static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncationAttribute";
@interface ASTextNodeDrawParameters : NSObject
@property (nonatomic, assign, readonly) CGRect bounds;
@property (nonatomic, strong, readonly) UIColor *backgroundColor;
@end
@implementation ASTextNodeDrawParameters
- (instancetype)initWithBounds:(CGRect)bounds
backgroundColor:(UIColor *)backgroundColor
{
if (self = [super init]) {
_bounds = bounds;
_backgroundColor = backgroundColor;
}
return self;
}
@end
struct ASTextNodeDrawParameter {
CGRect bounds;
UIColor *backgroundColor;
};
@interface ASTextNode () <UIGestureRecognizerDelegate, NSLayoutManagerDelegate>
@@ -73,21 +53,34 @@ static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncation
NSString *_highlightedLinkAttributeName;
id _highlightedLinkAttributeValue;
ASTextNodeHighlightStyle _highlightStyle;
NSRange _highlightRange;
ASHighlightOverlayLayer *_activeHighlightLayer;
ASDN::Mutex _rendererLock;
std::recursive_mutex _textLock;
CGSize _constrainedSize;
ASTextKitRenderer *_renderer;
ASTextNodeDrawParameter _drawParameter;
UILongPressGestureRecognizer *_longPressGestureRecognizer;
}
@dynamic placeholderEnabled;
#pragma mark - NSObject
+ (void)initialize
{
[super initialize];
if (self != [ASTextNode class]) {
// Prevent custom drawing in subclasses
ASDisplayNodeAssert(!ASSubclassOverridesClassSelector([ASTextNode class], self, @selector(drawRect:withParameters:isCancelled:isRasterizing:)), @"Subclass %@ must not override drawRect:withParameters:isCancelled:isRasterizing: method. Custom drawing in %@ subclass is not supported.", NSStringFromClass(self), NSStringFromClass([ASTextNode class]));
}
}
static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (instancetype)init
@@ -160,6 +153,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (NSString *)description
{
std::lock_guard<std::recursive_mutex> l(_textLock);
NSString *plainString = [[_attributedText string] stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
NSString *truncationString = [_composedTruncationText string];
if (plainString.length > 50)
@@ -195,10 +190,10 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (void)didLoad
{
[super didLoad];
// If we are view-backed and the delegate cares, support the long-press callback.
SEL longPressCallback = @selector(textNode:longPressedLinkAttribute:value:atPoint:textRange:);
if (!self.isLayerBacked && [self.delegate respondsToSelector:longPressCallback]) {
if (!self.isLayerBacked && [_delegate respondsToSelector:longPressCallback]) {
_longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(_handleLongPress:)];
_longPressGestureRecognizer.cancelsTouchesInView = self.longPressCancelsTouches;
_longPressGestureRecognizer.delegate = self;
@@ -227,7 +222,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (ASTextKitRenderer *)_rendererWithBounds:(CGRect)bounds
{
ASDN::MutexLocker l(_rendererLock);
std::lock_guard<std::recursive_mutex> l(_textLock);
if (_renderer == nil) {
CGSize constrainedSize = _constrainedSize.width != -INFINITY ? _constrainedSize : bounds.size;
_renderer = [[ASTextKitRenderer alloc] initWithTextKitAttributes:[self _rendererAttributes]
@@ -238,6 +234,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (ASTextKitAttributes)_rendererAttributes
{
std::lock_guard<std::recursive_mutex> l(_textLock);
return {
.attributedString = _attributedText,
.truncationAttributedString = _composedTruncationText,
@@ -250,9 +248,28 @@ 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.
{
std::lock_guard<std::recursive_mutex> l(_textLock);
_constrainedSize = CGSizeMake(-INFINITY, -INFINITY);
}
[self _invalidateRenderer];
}
}
- (void)_invalidateRenderer
{
ASDN::MutexLocker l(_rendererLock);
std::lock_guard<std::recursive_mutex> l(_textLock);
if (_renderer) {
// Destruction of the layout managers/containers/text storage is quite
@@ -267,27 +284,13 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
}
}
- (void)_invalidateRendererIfNeeded
{
[self _invalidateRendererIfNeededForBoundsSize:self.threadSafeBounds.size];
}
- (void)_invalidateRendererIfNeededForBoundsSize:(CGSize)boundsSize
{
if ([self _needInvalidateRendererForBoundsSize:boundsSize]) {
// Our bounds of frame 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.
_constrainedSize = CGSizeMake(-INFINITY, -INFINITY);
[self _invalidateRenderer];
}
}
#pragma mark - Layout and Sizing
- (BOOL)_needInvalidateRendererForBoundsSize:(CGSize)boundsSize
{
if (!_renderer) {
std::lock_guard<std::recursive_mutex> l(_textLock);
if (_renderer == nil) {
return YES;
}
@@ -322,6 +325,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
[super calculatedLayoutDidChange];
ASLayout *layout = self.calculatedLayout;
std::lock_guard<std::recursive_mutex> l(_textLock);
if (layout != nil) {
_constrainedSize = layout.size;
_renderer.constrainedSize = layout.size;
@@ -333,6 +338,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
ASDisplayNodeAssert(constrainedSize.width >= 0, @"Constrained width for text (%f) is too narrow", constrainedSize.width);
ASDisplayNodeAssert(constrainedSize.height >= 0, @"Constrained height for text (%f) is too short", constrainedSize.height);
std::lock_guard<std::recursive_mutex> l(_textLock);
_constrainedSize = constrainedSize;
// Instead of invalidating the renderer, in case this is a new call with a different constrained size,
@@ -341,7 +348,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
[self setNeedsDisplay];
CGSize size = [[self _renderer] size];
CGSize size = [self _renderer].size;
if (_attributedText.length > 0) {
CGFloat screenScale = ASScreenScale();
self.ascender = round([[_attributedText attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL] ascender] * screenScale)/screenScale;
@@ -359,6 +366,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (void)setAttributedText:(NSAttributedString *)attributedText
{
std::lock_guard<std::recursive_mutex> l(_textLock);
if (attributedText == nil) {
attributedText = [[NSAttributedString alloc] initWithString:@"" attributes:nil];
}
@@ -387,17 +396,19 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
[self invalidateCalculatedLayout];
[self setNeedsDisplay];
// Accessiblity
self.accessibilityLabel = _attributedText.string;
// We're an accessibility element by default if there is a string.
self.isAccessibilityElement = _attributedText.length != 0;
self.isAccessibilityElement = (_attributedText.length != 0); // We're an accessibility element by default if there is a string.
}
#pragma mark - Text Layout
- (void)setExclusionPaths:(NSArray *)exclusionPaths
{
std::lock_guard<std::recursive_mutex> l(_textLock);
if (ASObjectIsEqual(exclusionPaths, _exclusionPaths)) {
return;
}
@@ -410,34 +421,51 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (NSArray *)exclusionPaths
{
std::lock_guard<std::recursive_mutex> l(_textLock);
return _exclusionPaths;
}
#pragma mark - Drawing
- (void)drawRect:(CGRect)bounds withParameters:(ASTextNodeDrawParameters *)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing
- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer
{
std::lock_guard<std::recursive_mutex> l(_textLock);
_drawParameter = {
.backgroundColor = self.backgroundColor,
.bounds = self.bounds
};
return nil;
}
- (void)drawRect:(CGRect)bounds withParameters:(id <NSObject>)p isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing;
{
std::lock_guard<std::recursive_mutex> l(_textLock);
ASTextNodeDrawParameter drawParameter = _drawParameter;
CGRect drawParameterBounds = drawParameter.bounds;
UIColor *backgroundColor = isRasterizing ? nil : drawParameter.backgroundColor;
CGContextRef context = UIGraphicsGetCurrentContext();
ASDisplayNodeAssert(context, @"This is no good without a context.");
CGContextSaveGState(context);
ASTextKitRenderer *renderer = [self _rendererWithBounds:parameters.bounds];
ASTextKitRenderer *renderer = [self _rendererWithBounds:drawParameterBounds];
UIEdgeInsets shadowPadding = [self shadowPaddingWithRenderer:renderer];
CGPoint boundsOrigin = parameters.bounds.origin;
CGPoint boundsOrigin = drawParameterBounds.origin;
CGPoint textOrigin = CGPointMake(boundsOrigin.x - shadowPadding.left, boundsOrigin.y - shadowPadding.top);
// Fill background
if (!isRasterizing) {
UIColor *backgroundColor = parameters.backgroundColor;
if (backgroundColor) {
[backgroundColor setFill];
UIRectFillUsingBlendMode(CGContextGetClipBoundingBox(context), kCGBlendModeCopy);
}
if (backgroundColor != nil) {
[backgroundColor setFill];
UIRectFillUsingBlendMode(CGContextGetClipBoundingBox(context), kCGBlendModeCopy);
}
// Draw shadow
[[renderer shadower] setShadowInContext:context];
[renderer.shadower setShadowInContext:context];
// Draw text
bounds.origin = textOrigin;
@@ -446,11 +474,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
CGContextRestoreGState(context);
}
- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer
{
return [[ASTextNodeDrawParameters alloc] initWithBounds:self.threadSafeBounds backgroundColor:self.backgroundColor];
}
#pragma mark - Attributes
- (id)linkAttributeValueAtPoint:(CGPoint)point
@@ -470,6 +493,10 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
inAdditionalTruncationMessage:(out BOOL *)inAdditionalTruncationMessageOut
forHighlighting:(BOOL)highlighting
{
ASDisplayNodeAssertMainThread();
std::lock_guard<std::recursive_mutex> l(_textLock);
ASTextKitRenderer *renderer = [self _renderer];
NSRange visibleRange = renderer.firstVisibleRange;
NSAttributedString *attributedString = _attributedText;
@@ -562,6 +589,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
ASDisplayNodeAssertMainThread();
if (gestureRecognizer == _longPressGestureRecognizer) {
// Don't allow long press on truncation message
if ([self _pendingTruncationTap]) {
@@ -569,8 +598,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
}
// Ask our delegate if a long-press on an attribute is relevant
if ([self.delegate respondsToSelector:@selector(textNode:shouldLongPressLinkAttribute:value:atPoint:)]) {
return [self.delegate textNode:self
if ([_delegate respondsToSelector:@selector(textNode:shouldLongPressLinkAttribute:value:atPoint:)]) {
return [_delegate textNode:self
shouldLongPressLinkAttribute:_highlightedLinkAttributeName
value:_highlightedLinkAttributeValue
atPoint:[gestureRecognizer locationInView:self.view]];
@@ -591,8 +620,24 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
#pragma mark - Highlighting
- (ASTextNodeHighlightStyle)highlightStyle
{
std::lock_guard<std::recursive_mutex> l(_textLock);
return _highlightStyle;
}
- (void)setHighlightStyle:(ASTextNodeHighlightStyle)highlightStyle
{
std::lock_guard<std::recursive_mutex> l(_textLock);
_highlightStyle = highlightStyle;
}
- (NSRange)highlightRange
{
ASDisplayNodeAssertMainThread();
return _highlightRange;
}
@@ -672,7 +717,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
NSMutableArray *converted = [NSMutableArray arrayWithCapacity:highlightRects.count];
for (NSValue *rectValue in highlightRects) {
UIEdgeInsets shadowPadding = _renderer.shadower.shadowPadding;
CGRect rendererRect = [[self class] _adjustRendererRect:rectValue.CGRectValue forShadowPadding:shadowPadding];
CGRect rendererRect = ASTextNodeAdjustRenderRectForShadowPadding(rectValue.CGRectValue, shadowPadding);
CGRect highlightedRect = [self.layer convertRect:rendererRect toLayer:highlightTargetLayer];
// We set our overlay layer's frame to the bounds of the highlight target layer.
@@ -709,6 +754,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (void)_clearHighlightIfNecessary
{
ASDisplayNodeAssertMainThread();
if ([self _pendingLinkTap] || [self _pendingTruncationTap]) {
[self setHighlightRange:NSMakeRange(0, 0) animated:YES];
}
@@ -726,29 +773,12 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
#pragma mark - Text rects
+ (CGRect)_adjustRendererRect:(CGRect)rendererRect forShadowPadding:(UIEdgeInsets)shadowPadding
{
static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UIEdgeInsets shadowPadding) {
rendererRect.origin.x -= shadowPadding.left;
rendererRect.origin.y -= shadowPadding.top;
return rendererRect;
}
- (NSArray *)_rectsForTextRange:(NSRange)textRange measureOption:(ASTextKitRendererMeasureOption)measureOption
{
NSArray *rects = [[self _renderer] rectsForTextRange:textRange measureOption:measureOption];
NSMutableArray *adjustedRects = [NSMutableArray array];
for (NSValue *rectValue in rects) {
CGRect rect = [rectValue CGRectValue];
rect = [self.class _adjustRendererRect:rect forShadowPadding:self.shadowPadding];
NSValue *adjustedRectValue = [NSValue valueWithCGRect:rect];
[adjustedRects addObject:adjustedRectValue];
}
return adjustedRects;
}
- (NSArray *)rectsForTextRange:(NSRange)textRange
{
return [self _rectsForTextRange:textRange measureOption:ASTextKitRendererMeasureOptionCapHeight];
@@ -759,22 +789,46 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
return [self _rectsForTextRange:textRange measureOption:ASTextKitRendererMeasureOptionBlock];
}
- (NSArray *)_rectsForTextRange:(NSRange)textRange measureOption:(ASTextKitRendererMeasureOption)measureOption
{
std::lock_guard<std::recursive_mutex> l(_textLock);
NSArray *rects = [[self _renderer] rectsForTextRange:textRange measureOption:measureOption];
NSMutableArray *adjustedRects = [NSMutableArray array];
for (NSValue *rectValue in rects) {
CGRect rect = [rectValue CGRectValue];
rect = ASTextNodeAdjustRenderRectForShadowPadding(rect, self.shadowPadding);
NSValue *adjustedRectValue = [NSValue valueWithCGRect:rect];
[adjustedRects addObject:adjustedRectValue];
}
return adjustedRects;
}
- (CGRect)trailingRect
{
std::lock_guard<std::recursive_mutex> l(_textLock);
CGRect rect = [[self _renderer] trailingRect];
return [self.class _adjustRendererRect:rect forShadowPadding:self.shadowPadding];
return ASTextNodeAdjustRenderRectForShadowPadding(rect, self.shadowPadding);
}
- (CGRect)frameForTextRange:(NSRange)textRange
{
std::lock_guard<std::recursive_mutex> l(_textLock);
CGRect frame = [[self _renderer] frameForTextRange:textRange];
return [self.class _adjustRendererRect:frame forShadowPadding:self.shadowPadding];
return ASTextNodeAdjustRenderRectForShadowPadding(frame, self.shadowPadding);
}
#pragma mark - Placeholders
- (void)setPlaceholderColor:(UIColor *)placeholderColor
{
std::lock_guard<std::recursive_mutex> l(_textLock);
_placeholderColor = placeholderColor;
// prevent placeholders if we don't have a color
@@ -790,6 +844,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
return nil;
}
std::lock_guard<std::recursive_mutex> l(_textLock);
UIGraphicsBeginImageContext(size);
[self.placeholderColor setFill];
@@ -816,8 +872,10 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
#pragma mark - Touch Handling
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
ASDisplayNodeAssertMainThread();
if (!_passthroughNonlinkTouches) {
return [super pointInside:point withEvent:event];
}
@@ -846,9 +904,11 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
ASDisplayNodeAssertMainThread();
std::lock_guard<std::recursive_mutex> l(_textLock);
[super touchesBegan:touches withEvent:event];
CGPoint point = [[touches anyObject] locationInView:self.view];
@@ -866,8 +926,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
BOOL linkCrossesVisibleRange = (lastCharIndex > range.location) && (lastCharIndex < NSMaxRange(range) - 1);
if (inAdditionalTruncationMessage) {
ASTextKitRenderer *renderer = [self _renderer];
NSRange visibleRange = renderer.firstVisibleRange;
NSRange visibleRange = [self _renderer].firstVisibleRange;
NSRange truncationMessageRange = [self _additionalTruncationMessageRangeWithVisibleRange:visibleRange];
[self _setHighlightRange:truncationMessageRange forAttributeName:ASTextNodeTruncationTokenAttributeName value:nil animated:YES];
} else if (range.length && !linkCrossesVisibleRange && linkAttributeValue != nil && linkAttributeName != nil) {
@@ -878,15 +937,17 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
ASDisplayNodeAssertMainThread();
[super touchesCancelled:touches withEvent:event];
[self _clearHighlightIfNecessary];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
ASDisplayNodeAssertMainThread();
[super touchesEnded:touches withEvent:event];
if ([self _pendingLinkTap] && [_delegate respondsToSelector:@selector(textNode:tappedLinkAttribute:value:atPoint:textRange:)]) {
CGPoint point = [[touches anyObject] locationInView:self.view];
[_delegate textNode:self tappedLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue atPoint:point textRange:_highlightRange];
@@ -903,6 +964,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
ASDisplayNodeAssertMainThread();
[super touchesMoved:touches withEvent:event];
UITouch *touch = [touches anyObject];
@@ -928,22 +990,28 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (void)_handleLongPress:(UILongPressGestureRecognizer *)longPressRecognizer
{
ASDisplayNodeAssertMainThread();
// Respond to long-press when it begins, not when it ends.
if (longPressRecognizer.state == UIGestureRecognizerStateBegan) {
if ([self.delegate respondsToSelector:@selector(textNode:longPressedLinkAttribute:value:atPoint:textRange:)]) {
if ([_delegate respondsToSelector:@selector(textNode:longPressedLinkAttribute:value:atPoint:textRange:)]) {
CGPoint touchPoint = [_longPressGestureRecognizer locationInView:self.view];
[self.delegate textNode:self longPressedLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue atPoint:touchPoint textRange:_highlightRange];
[_delegate textNode:self longPressedLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue atPoint:touchPoint textRange:_highlightRange];
}
}
}
- (BOOL)_pendingLinkTap
{
std::lock_guard<std::recursive_mutex> l(_textLock);
return (_highlightedLinkAttributeValue != nil && ![self _pendingTruncationTap]) && _delegate != nil;
}
- (BOOL)_pendingTruncationTap
{
std::lock_guard<std::recursive_mutex> l(_textLock);
return [_highlightedLinkAttributeName isEqualToString:ASTextNodeTruncationTokenAttributeName];
}
@@ -951,11 +1019,15 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (CGColorRef)shadowColor
{
std::lock_guard<std::recursive_mutex> l(_textLock);
return _shadowColor;
}
- (void)setShadowColor:(CGColorRef)shadowColor
{
std::lock_guard<std::recursive_mutex> l(_textLock);
if (_shadowColor != shadowColor) {
if (shadowColor != NULL) {
CGColorRetain(shadowColor);
@@ -968,11 +1040,15 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (CGSize)shadowOffset
{
std::lock_guard<std::recursive_mutex> l(_textLock);
return _shadowOffset;
}
- (void)setShadowOffset:(CGSize)shadowOffset
{
std::lock_guard<std::recursive_mutex> l(_textLock);
if (!CGSizeEqualToSize(_shadowOffset, shadowOffset)) {
_shadowOffset = shadowOffset;
[self _invalidateRenderer];
@@ -982,11 +1058,15 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (CGFloat)shadowOpacity
{
std::lock_guard<std::recursive_mutex> l(_textLock);
return _shadowOpacity;
}
- (void)setShadowOpacity:(CGFloat)shadowOpacity
{
std::lock_guard<std::recursive_mutex> l(_textLock);
if (_shadowOpacity != shadowOpacity) {
_shadowOpacity = shadowOpacity;
[self _invalidateRenderer];
@@ -996,11 +1076,15 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (CGFloat)shadowRadius
{
std::lock_guard<std::recursive_mutex> l(_textLock);
return _shadowRadius;
}
- (void)setShadowRadius:(CGFloat)shadowRadius
{
std::lock_guard<std::recursive_mutex> l(_textLock);
if (_shadowRadius != shadowRadius) {
_shadowRadius = shadowRadius;
[self _invalidateRenderer];
@@ -1015,6 +1099,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
- (UIEdgeInsets)shadowPaddingWithRenderer:(ASTextKitRenderer *)renderer
{
std::lock_guard<std::recursive_mutex> l(_textLock);
return renderer.shadower.shadowPadding;
}
@@ -1032,6 +1118,8 @@ static NSAttributedString *DefaultTruncationAttributedString()
- (void)setTruncationAttributedText:(NSAttributedString *)truncationAttributedText
{
std::lock_guard<std::recursive_mutex> l(_textLock);
if (ASObjectIsEqual(_truncationAttributedText, truncationAttributedText)) {
return;
}
@@ -1042,6 +1130,8 @@ static NSAttributedString *DefaultTruncationAttributedString()
- (void)setAdditionalTruncationMessage:(NSAttributedString *)additionalTruncationMessage
{
std::lock_guard<std::recursive_mutex> l(_textLock);
if (ASObjectIsEqual(_additionalTruncationMessage, additionalTruncationMessage)) {
return;
}
@@ -1052,6 +1142,8 @@ static NSAttributedString *DefaultTruncationAttributedString()
- (void)setTruncationMode:(NSLineBreakMode)truncationMode
{
std::lock_guard<std::recursive_mutex> l(_textLock);
if (_truncationMode != truncationMode) {
_truncationMode = truncationMode;
[self _invalidateRenderer];
@@ -1061,12 +1153,16 @@ static NSAttributedString *DefaultTruncationAttributedString()
- (BOOL)isTruncated
{
std::lock_guard<std::recursive_mutex> l(_textLock);
ASTextKitRenderer *renderer = [self _renderer];
return renderer.firstVisibleRange.length < _attributedText.length;
}
- (void)setPointSizeScaleFactors:(NSArray *)pointSizeScaleFactors
{
std::lock_guard<std::recursive_mutex> l(_textLock);
if ([_pointSizeScaleFactors isEqualToArray:pointSizeScaleFactors] == NO) {
_pointSizeScaleFactors = pointSizeScaleFactors;
[self _invalidateRenderer];
@@ -1075,15 +1171,19 @@ static NSAttributedString *DefaultTruncationAttributedString()
- (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines
{
if (_maximumNumberOfLines != maximumNumberOfLines) {
_maximumNumberOfLines = maximumNumberOfLines;
[self _invalidateRenderer];
[self setNeedsDisplay];
}
std::lock_guard<std::recursive_mutex> l(_textLock);
if (_maximumNumberOfLines != maximumNumberOfLines) {
_maximumNumberOfLines = maximumNumberOfLines;
[self _invalidateRenderer];
[self setNeedsDisplay];
}
}
- (NSUInteger)lineCount
{
std::lock_guard<std::recursive_mutex> l(_textLock);
return [[self _renderer] lineCount];
}
@@ -1091,6 +1191,8 @@ static NSAttributedString *DefaultTruncationAttributedString()
- (void)_updateComposedTruncationText
{
std::lock_guard<std::recursive_mutex> l(_textLock);
_composedTruncationText = [self _prepareTruncationStringForDrawing:[self _composedTruncationText]];
}
@@ -1107,6 +1209,8 @@ static NSAttributedString *DefaultTruncationAttributedString()
*/
- (NSRange)_additionalTruncationMessageRangeWithVisibleRange:(NSRange)visibleRange
{
std::lock_guard<std::recursive_mutex> l(_textLock);
// Check if we even have an additional truncation message.
if (!_additionalTruncationMessage) {
return NSMakeRange(NSNotFound, 0);
@@ -1118,8 +1222,7 @@ static NSAttributedString *DefaultTruncationAttributedString()
NSUInteger additionalTruncationMessageLength = _additionalTruncationMessage.length;
// We get the location of the truncation token, then add the length of the
// truncation attributed string +1 for the space between.
NSRange range = NSMakeRange(truncationTokenIndex + _truncationAttributedText.length + 1, additionalTruncationMessageLength);
return range;
return NSMakeRange(truncationTokenIndex + _truncationAttributedText.length + 1, additionalTruncationMessageLength);
}
/**
@@ -1129,6 +1232,8 @@ static NSAttributedString *DefaultTruncationAttributedString()
*/
- (NSAttributedString *)_composedTruncationText
{
std::lock_guard<std::recursive_mutex> l(_textLock);
//If we have neither return the default
if (!_additionalTruncationMessage && !_truncationAttributedText) {
return _composedTruncationText;
@@ -1157,6 +1262,8 @@ static NSAttributedString *DefaultTruncationAttributedString()
*/
- (NSAttributedString *)_prepareTruncationStringForDrawing:(NSAttributedString *)truncationString
{
std::lock_guard<std::recursive_mutex> l(_textLock);
truncationString = ASCleanseAttributedStringOfCoreTextAttributes(truncationString);
NSMutableAttributedString *truncationMutableString = [truncationString mutableCopy];
// Grab the attributes from the full string

View File

@@ -453,7 +453,9 @@ static NSString * const kStatus = @"status";
- (void)setDelegate:(id<ASVideoNodeDelegate>)delegate
{
[super setDelegate:delegate];
_delegate = delegate;
if (_delegate == nil) {
memset(&_delegateFlags, 0, sizeof(_delegateFlags));
} else {

View File

@@ -7,3 +7,13 @@
#ifdef __OBJC__
#import <Foundation/Foundation.h>
#endif
// CocoaPods has a preproceessor macro for PIN_REMOTE_IMAGE, if already defined, okay
#ifndef PIN_REMOTE_IMAGE
// For Carthage or manual builds, this will define PIN_REMOTE_IMAGE if the header is available in the
// search path e.g. they've dragged in the framework (technically this will not be able to detect if
// a user does not include the framework in the link binary with build step).
#define PIN_REMOTE_IMAGE __has_include(<PINRemoteImage/PINRemoteImage.h>)
#endif

View File

@@ -49,8 +49,8 @@ extern BOOL ASRangeTuningParametersEqualToRangeTuningParameters(ASRangeTuningPar
.trailingBufferScreenfuls = 0.25
};
_tuningParameters[ASLayoutRangeModeMinimum][ASLayoutRangeTypeFetchData] = {
.leadingBufferScreenfuls = 0.25,
.trailingBufferScreenfuls = 0.5
.leadingBufferScreenfuls = 0.5,
.trailingBufferScreenfuls = 0.25
};
_tuningParameters[ASLayoutRangeModeVisibleOnly][ASLayoutRangeTypeDisplay] = {

View File

@@ -26,17 +26,6 @@
@implementation ASChangeSetDataController
- (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled
{
if (!(self = [super initWithAsyncDataFetching:asyncDataFetchingEnabled])) {
return nil;
}
_changeSetBatchUpdateCounter = 0;
return self;
}
#pragma mark - Batching (External API)
- (void)beginUpdates
@@ -66,6 +55,8 @@
[super deleteSections:change.indexSet withAnimationOptions:change.animationOptions];
}
// TODO: Shouldn't reloads be processed before deletes, since deletes affect
// the index space and reloads don't?
for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeReload]) {
[super reloadSections:change.indexSet withAnimationOptions:change.animationOptions];
}

View File

@@ -32,9 +32,9 @@
NSMutableDictionary<NSString *, NSMutableArray<ASIndexedNodeContext *> *> *_pendingContexts;
}
- (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled
- (instancetype)init
{
self = [super initWithAsyncDataFetching:asyncDataFetchingEnabled];
self = [super init];
if (self != nil) {
_pendingContexts = [NSMutableDictionary dictionary];
}
@@ -53,15 +53,13 @@
- (void)willReloadData
{
NSArray *keys = _pendingContexts.allKeys;
for (NSString *kind in keys) {
NSMutableArray<ASIndexedNodeContext *> *contexts = _pendingContexts[kind];
[_pendingContexts enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull kind, NSMutableArray<ASIndexedNodeContext *> * _Nonnull contexts, __unused BOOL * _Nonnull stop) {
// Remove everything that existed before the reload, now that we're ready to insert replacements
NSArray *indexPaths = [self indexPathsForEditingNodesOfKind:kind];
[self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil];
NSArray *editingNodes = [self editingNodesOfKind:kind];
NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodes.count)];
NSIndexSet *indexSet = [[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodes.count)];
[self deleteSectionsOfKind:kind atIndexSet:indexSet completion:nil];
// Insert each section
@@ -75,8 +73,8 @@
[self batchLayoutNodesFromContexts:contexts ofKind:kind completion:^(NSArray<ASCellNode *> *nodes, NSArray<NSIndexPath *> *indexPaths) {
[self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil];
}];
[_pendingContexts removeObjectForKey:kind];
}
}];
[_pendingContexts removeAllObjects];
}
- (void)prepareForInsertSections:(NSIndexSet *)sections

View File

@@ -57,17 +57,6 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind;
*/
- (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController;
/**
Lock the data source for data fetching.
*/
- (void)dataControllerLockDataSource;
/**
Unlock the data source after data fetching.
*/
- (void)dataControllerUnlockDataSource;
@end
@protocol ASDataControllerEnvironmentDelegate
@@ -135,20 +124,6 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind;
*/
@property (nonatomic, weak) id<ASDataControllerEnvironmentDelegate> environmentDelegate;
/**
* Designated initializer.
*
* @param asyncDataFetchingEnabled Enable the data fetching in async mode.
*
* @discussion If enabled, we will fetch data through `dataController:nodeAtIndexPath:` and `dataController:rowsInSection:` in background thread.
* Otherwise, the methods will be invoked synchronically in calling thread. Enabling data fetching in async mode could avoid blocking main thread
* while allocating cell on main thread, which is frequently reported issue for handling large scale data. On another hand, the application code
* will take the responsibility to avoid data inconsistency. Specifically, we will lock the data source through `dataControllerLockDataSource`,
* and unlock it by `dataControllerUnlockDataSource` after the data fetching. The application should not update the data source while
* the data source is locked.
*/
- (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled;
/** @name Data Updating */
- (void)beginUpdates;

View File

@@ -44,8 +44,6 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
NSMutableArray *_pendingEditCommandBlocks; // To be run on the main thread. Handles begin/endUpdates tracking.
NSOperationQueue *_editingTransactionQueue; // Serial background queue. Dispatches concurrent layout and manages _editingNodes.
BOOL _asyncDataFetchingEnabled;
BOOL _initialReloadDataHasBeenCalled;
BOOL _delegateDidInsertNodes;
@@ -62,7 +60,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
#pragma mark - Lifecycle
- (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled
- (instancetype)init
{
if (!(self = [super init])) {
return nil;
@@ -83,7 +81,6 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
_editingTransactionQueue.name = @"org.AsyncDisplayKit.ASDataController.editingTransactionQueue";
_batchUpdateCounter = 0;
_asyncDataFetchingEnabled = asyncDataFetchingEnabled;
return self;
}
@@ -119,6 +116,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
- (void)batchLayoutNodesFromContexts:(NSArray<ASIndexedNodeContext *> *)contexts ofKind:(NSString *)kind completion:(ASDataControllerCompletionBlock)completionBlock
{
ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"%@ must be called on the editing transaction queue", NSStringFromSelector(_cmd));
NSUInteger blockSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor;
NSUInteger count = contexts.count;
@@ -143,8 +142,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
*/
- (void)_layoutNode:(ASCellNode *)node withConstrainedSize:(ASSizeRange)constrainedSize
{
[node measureWithSizeRange:constrainedSize];
node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height);
CGSize size = [node measureWithSizeRange:constrainedSize].size;
node.frame = { .size = size };
}
/**
@@ -152,6 +151,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
*/
- (void)_batchLayoutNodesFromContexts:(NSArray<ASIndexedNodeContext *> *)contexts withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"%@ must be called on the editing transaction queue", NSStringFromSelector(_cmd));
[self batchLayoutNodesFromContexts:contexts ofKind:ASDataControllerRowNodeKind completion:^(NSArray<ASCellNode *> *nodes, NSArray<NSIndexPath *> *indexPaths) {
// Insert finished nodes into data storage
[self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
@@ -163,6 +164,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
*/
- (void)_layoutNodes:(NSArray<ASCellNode *> *)nodes fromContexts:(NSArray<ASIndexedNodeContext *> *)contexts atIndexesOfRange:(NSRange)range ofKind:(NSString *)kind
{
ASDisplayNodeAssert([NSOperationQueue currentQueue] != _editingTransactionQueue, @"%@ should not be called on the editing transaction queue", NSStringFromSelector(_cmd));
if (_dataSource == nil) {
return;
}
@@ -182,6 +185,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
- (void)_layoutNodesFromContexts:(NSArray<ASIndexedNodeContext *> *)contexts ofKind:(NSString *)kind completion:(ASDataControllerCompletionBlock)completionBlock
{
ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"%@ must be called on the editing transaction queue", NSStringFromSelector(_cmd));
if (!contexts.count || _dataSource == nil) {
return;
}
@@ -278,7 +283,6 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
NSMutableArray *editingNodes = _editingNodes[kind];
ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(editingNodes, indexPaths, nodes);
_editingNodes[kind] = editingNodes;
// Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads.
NSMutableArray *completedNodes = ASTwoDimensionalArrayDeepMutableCopy(editingNodes);
@@ -359,7 +363,11 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
*/
- (void)_insertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"%@ must be called on the editing transaction queue", NSStringFromSelector(_cmd));
[self insertNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) {
ASDisplayNodeAssertMainThread();
if (_delegateDidInsertNodes)
[_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
}];
@@ -373,7 +381,11 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
*/
- (void)_deleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"%@ must be called on the editing transaction queue", NSStringFromSelector(_cmd));
[self deleteNodesOfKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) {
ASDisplayNodeAssertMainThread();
if (_delegateDidDeleteNodes)
[_delegate dataController:self didDeleteNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
}];
@@ -387,7 +399,11 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
*/
- (void)_insertSections:(NSMutableArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"%@ must be called on the editing transaction queue", NSStringFromSelector(_cmd));
[self insertSections:sections ofKind:ASDataControllerRowNodeKind atIndexSet:indexSet completion:^(NSArray *sections, NSIndexSet *indexSet) {
ASDisplayNodeAssertMainThread();
if (_delegateDidInsertSections)
[_delegate dataController:self didInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions];
}];
@@ -401,7 +417,11 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
*/
- (void)_deleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"%@ must be called on the editing transaction queue", NSStringFromSelector(_cmd));
[self deleteSectionsOfKind:ASDataControllerRowNodeKind atIndexSet:indexSet completion:^(NSIndexSet *indexSet) {
ASDisplayNodeAssertMainThread();
if (_delegateDidDeleteSections)
[_delegate dataController:self didDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions];
}];
@@ -426,49 +446,44 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
ASDisplayNodeAssertMainThread();
[_editingTransactionQueue waitUntilAllOperationsAreFinished];
[self accessDataSourceSynchronously:synchronously withBlock:^{
NSUInteger sectionCount = [_dataSource numberOfSectionsInDataController:self];
NSIndexSet *sectionIndexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)];
NSArray<ASIndexedNodeContext *> *contexts = [self _populateFromDataSourceWithSectionIndexSet:sectionIndexSet];
NSUInteger sectionCount = [_dataSource numberOfSectionsInDataController:self];
NSIndexSet *sectionIndexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)];
NSArray<ASIndexedNodeContext *> *contexts = [self _populateFromDataSourceWithSectionIndexSet:sectionIndexSet];
// Allow subclasses to perform setup before going into the edit transaction
[self prepareForReloadData];
// Allow subclasses to perform setup before going into the edit transaction
[self prepareForReloadData];
[_editingTransactionQueue addOperationWithBlock:^{
LOG(@"Edit Transaction - reloadData");
void (^transactionBlock)() = ^{
LOG(@"Edit Transaction - reloadData");
// Remove everything that existed before the reload, now that we're ready to insert replacements
NSMutableArray *editingNodes = _editingNodes[ASDataControllerRowNodeKind];
NSUInteger editingNodesSectionCount = editingNodes.count;
if (editingNodesSectionCount) {
NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodesSectionCount)];
[self _deleteNodesAtIndexPaths:ASIndexPathsForTwoDimensionalArray(editingNodes) withAnimationOptions:animationOptions];
[self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions];
}
[self willReloadData];
// Insert empty sections
NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount];
for (int i = 0; i < sectionCount; i++) {
[sections addObject:[[NSMutableArray alloc] init]];
}
[self _insertSections:sections atIndexSet:sectionIndexSet withAnimationOptions:animationOptions];
[self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions];
if (completion) {
dispatch_async(dispatch_get_main_queue(), completion);
}
};
// Remove everything that existed before the reload, now that we're ready to insert replacements
NSMutableArray *editingNodes = _editingNodes[ASDataControllerRowNodeKind];
NSUInteger editingNodesSectionCount = editingNodes.count;
if (synchronously) {
transactionBlock();
} else {
[_editingTransactionQueue addOperationWithBlock:transactionBlock];
if (editingNodesSectionCount) {
NSIndexSet *indexSet = [[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodesSectionCount)];
[self _deleteNodesAtIndexPaths:ASIndexPathsForTwoDimensionalArray(editingNodes) withAnimationOptions:animationOptions];
[self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions];
}
[self willReloadData];
// Insert empty sections
NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount];
for (int i = 0; i < sectionCount; i++) {
[sections addObject:[[NSMutableArray alloc] init]];
}
[self _insertSections:sections atIndexSet:sectionIndexSet withAnimationOptions:animationOptions];
[self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions];
if (completion) {
dispatch_async(dispatch_get_main_queue(), completion);
}
}];
if (synchronously) {
[self waitUntilAllUpdatesAreCommitted];
}
}];
}
@@ -491,45 +506,21 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
#pragma mark - Data Source Access (Calling _dataSource)
/**
* Safely locks access to the data source and executes the given block, unlocking once complete.
*
* @discussion When `asyncDataFetching` is enabled, the block is executed on a background thread.
*/
- (void)accessDataSourceWithBlock:(dispatch_block_t)block
{
[self accessDataSourceSynchronously:NO withBlock:block];
}
- (void)accessDataSourceSynchronously:(BOOL)synchronously withBlock:(dispatch_block_t)block
{
if (!synchronously && _asyncDataFetchingEnabled) {
[_dataSource dataControllerLockDataSource];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
block();
[_dataSource dataControllerUnlockDataSource];
});
} else {
[_dataSource dataControllerLockDataSource];
block();
[_dataSource dataControllerUnlockDataSource];
}
}
/**
* Fetches row contexts for the provided sections from the data source.
*/
- (NSArray<ASIndexedNodeContext *> *)_populateFromDataSourceWithSectionIndexSet:(NSIndexSet *)indexSet
{
ASDisplayNodeAssertMainThread();
id<ASEnvironment> environment = [self.environmentDelegate dataControllerEnvironment];
ASEnvironmentTraitCollection environmentTraitCollection = environment.environmentTraitCollection;
NSMutableArray<ASIndexedNodeContext *> *contexts = [NSMutableArray array];
[indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
NSUInteger rowNum = [_dataSource dataController:self rowsInSection:idx];
NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx];
[indexSet enumerateIndexesUsingBlock:^(NSUInteger sectionIndex, BOOL *stop) {
NSUInteger rowNum = [_dataSource dataController:self rowsInSection:sectionIndex];
for (NSUInteger i = 0; i < rowNum; i++) {
NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i];
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:sectionIndex];
ASCellNodeBlock nodeBlock = [_dataSource dataController:self nodeBlockAtIndexPath:indexPath];
ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:ASDataControllerRowNodeKind atIndexPath:indexPath];
@@ -628,24 +619,22 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
LOG(@"Edit Command - insertSections: %@", sections);
[_editingTransactionQueue waitUntilAllOperationsAreFinished];
[self accessDataSourceWithBlock:^{
NSArray<ASIndexedNodeContext *> *contexts = [self _populateFromDataSourceWithSectionIndexSet:sections];
NSArray<ASIndexedNodeContext *> *contexts = [self _populateFromDataSourceWithSectionIndexSet:sections];
[self prepareForInsertSections:sections];
[self prepareForInsertSections:sections];
[_editingTransactionQueue addOperationWithBlock:^{
[self willInsertSections:sections];
LOG(@"Edit Transaction - insertSections: %@", sections);
NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:sections.count];
for (NSUInteger i = 0; i < sections.count; i++) {
[sectionArray addObject:[NSMutableArray array]];
}
[self _insertSections:sectionArray atIndexSet:sections withAnimationOptions:animationOptions];
[_editingTransactionQueue addOperationWithBlock:^{
[self willInsertSections:sections];
LOG(@"Edit Transaction - insertSections: %@", sections);
NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:sections.count];
for (NSUInteger i = 0; i < sections.count; i++) {
[sectionArray addObject:[NSMutableArray array]];
}
[self _insertSections:sectionArray atIndexSet:sections withAnimationOptions:animationOptions];
[self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions];
}];
[self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions];
}];
}];
}
@@ -678,23 +667,21 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
[_editingTransactionQueue waitUntilAllOperationsAreFinished];
[self accessDataSourceWithBlock:^{
NSArray<ASIndexedNodeContext *> *contexts= [self _populateFromDataSourceWithSectionIndexSet:sections];
NSArray<ASIndexedNodeContext *> *contexts= [self _populateFromDataSourceWithSectionIndexSet:sections];
[self prepareForReloadSections:sections];
[self prepareForReloadSections:sections];
[_editingTransactionQueue addOperationWithBlock:^{
[self willReloadSections:sections];
NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], sections);
[_editingTransactionQueue addOperationWithBlock:^{
[self willReloadSections:sections];
LOG(@"Edit Transaction - reloadSections: updatedIndexPaths: %@, indexPaths: %@, _editingNodes: %@", updatedIndexPaths, indexPaths, ASIndexPathsForTwoDimensionalArray(_editingNodes[ASDataControllerRowNodeKind]));
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], sections);
LOG(@"Edit Transaction - reloadSections: updatedIndexPaths: %@, indexPaths: %@, _editingNodes: %@", updatedIndexPaths, indexPaths, ASIndexPathsForTwoDimensionalArray(_editingNodes[ASDataControllerRowNodeKind]));
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
// reinsert the elements
[self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions];
}];
// reinsert the elements
[self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions];
}];
}];
}
@@ -818,27 +805,25 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)];
NSMutableArray<ASIndexedNodeContext *> *contexts = [[NSMutableArray alloc] initWithCapacity:indexPaths.count];
[self accessDataSourceWithBlock:^{
id<ASEnvironment> environment = [self.environmentDelegate dataControllerEnvironment];
ASEnvironmentTraitCollection environmentTraitCollection = environment.environmentTraitCollection;
for (NSIndexPath *indexPath in sortedIndexPaths) {
ASCellNodeBlock nodeBlock = [_dataSource dataController:self nodeBlockAtIndexPath:indexPath];
ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:ASDataControllerRowNodeKind atIndexPath:indexPath];
[contexts addObject:[[ASIndexedNodeContext alloc] initWithNodeBlock:nodeBlock
indexPath:indexPath
constrainedSize:constrainedSize
environmentTraitCollection:environmentTraitCollection]];
}
id<ASEnvironment> environment = [self.environmentDelegate dataControllerEnvironment];
ASEnvironmentTraitCollection environmentTraitCollection = environment.environmentTraitCollection;
for (NSIndexPath *indexPath in sortedIndexPaths) {
ASCellNodeBlock nodeBlock = [_dataSource dataController:self nodeBlockAtIndexPath:indexPath];
ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:ASDataControllerRowNodeKind atIndexPath:indexPath];
[contexts addObject:[[ASIndexedNodeContext alloc] initWithNodeBlock:nodeBlock
indexPath:indexPath
constrainedSize:constrainedSize
environmentTraitCollection:environmentTraitCollection]];
}
[self prepareForInsertRowsAtIndexPaths:indexPaths];
[self prepareForInsertRowsAtIndexPaths:indexPaths];
[_editingTransactionQueue addOperationWithBlock:^{
[self willInsertRowsAtIndexPaths:indexPaths];
[_editingTransactionQueue addOperationWithBlock:^{
[self willInsertRowsAtIndexPaths:indexPaths];
LOG(@"Edit Transaction - insertRows: %@", indexPaths);
[self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions];
}];
LOG(@"Edit Transaction - insertRows: %@", indexPaths);
[self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions];
}];
}];
}
@@ -874,35 +859,32 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
[_editingTransactionQueue waitUntilAllOperationsAreFinished];
// Reloading requires re-fetching the data. Load it on the current calling thread, locking the data source.
[self accessDataSourceWithBlock:^{
NSMutableArray<ASIndexedNodeContext *> *contexts = [[NSMutableArray alloc] initWithCapacity:indexPaths.count];
// Sort indexPath to avoid messing up the index when deleting
// FIXME: Shouldn't deletes be sorted in descending order?
NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)];
id<ASEnvironment> environment = [self.environmentDelegate dataControllerEnvironment];
ASEnvironmentTraitCollection environmentTraitCollection = environment.environmentTraitCollection;
for (NSIndexPath *indexPath in sortedIndexPaths) {
ASCellNodeBlock nodeBlock = [_dataSource dataController:self nodeBlockAtIndexPath:indexPath];
ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:ASDataControllerRowNodeKind atIndexPath:indexPath];
[contexts addObject:[[ASIndexedNodeContext alloc] initWithNodeBlock:nodeBlock
indexPath:indexPath
constrainedSize:constrainedSize
environmentTraitCollection:environmentTraitCollection]];
}
NSMutableArray<ASIndexedNodeContext *> *contexts = [[NSMutableArray alloc] initWithCapacity:indexPaths.count];
// Sort indexPath to avoid messing up the index when deleting
// FIXME: Shouldn't deletes be sorted in descending order?
NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)];
id<ASEnvironment> environment = [self.environmentDelegate dataControllerEnvironment];
ASEnvironmentTraitCollection environmentTraitCollection = environment.environmentTraitCollection;
for (NSIndexPath *indexPath in sortedIndexPaths) {
ASCellNodeBlock nodeBlock = [_dataSource dataController:self nodeBlockAtIndexPath:indexPath];
ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:ASDataControllerRowNodeKind atIndexPath:indexPath];
[contexts addObject:[[ASIndexedNodeContext alloc] initWithNodeBlock:nodeBlock
indexPath:indexPath
constrainedSize:constrainedSize
environmentTraitCollection:environmentTraitCollection]];
}
[self prepareForReloadRowsAtIndexPaths:indexPaths];
[_editingTransactionQueue addOperationWithBlock:^{
[self willReloadRowsAtIndexPaths:indexPaths];
[self prepareForReloadRowsAtIndexPaths:indexPaths];
[_editingTransactionQueue addOperationWithBlock:^{
[self willReloadRowsAtIndexPaths:indexPaths];
LOG(@"Edit Transaction - reloadRows: %@", indexPaths);
[self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions];
[self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions];
}];
LOG(@"Edit Transaction - reloadRows: %@", indexPaths);
[self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions];
[self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions];
}];
}];
}
@@ -935,16 +917,18 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
return;
}
[self accessDataSourceWithBlock:^{
[nodes enumerateObjectsUsingBlock:^(NSMutableArray *section, NSUInteger sectionIndex, BOOL *stop) {
[section enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger rowIndex, BOOL *stop) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex];
ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath];
ASLayout *layout = [node measureWithSizeRange:constrainedSize];
node.frame = CGRectMake(0.0f, 0.0f, layout.size.width, layout.size.height);
}];
}];
}];
NSUInteger sectionIndex = 0;
for (NSMutableArray *section in nodes) {
NSUInteger rowIndex = 0;
for (ASCellNode *node in section) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex];
ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath];
CGSize size = [node measureWithSizeRange:constrainedSize].size;
node.frame = { .size = size };
rowIndex += 1;
}
sectionIndex += 1;
}
}
- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
@@ -1021,17 +1005,15 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
- (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode;
{
ASDisplayNodeAssertMainThread();
NSArray *nodes = [self completedNodes];
NSUInteger numberOfNodes = nodes.count;
NSInteger section = 0;
// Loop through each section to look for the cellNode
for (NSUInteger i = 0; i < numberOfNodes; i++) {
NSArray *sectionNodes = nodes[i];
NSUInteger cellIndex = [sectionNodes indexOfObjectIdenticalTo:cellNode];
if (cellIndex != NSNotFound) {
return [NSIndexPath indexPathForRow:cellIndex inSection:i];
for (NSArray *sectionNodes in [self completedNodes]) {
NSUInteger item = [sectionNodes indexOfObjectIdenticalTo:cellNode];
if (item != NSNotFound) {
return [NSIndexPath indexPathForItem:item inSection:section];
}
section += 1;
}
return nil;

View File

@@ -147,6 +147,9 @@ ASDISPLAYNODE_EXTERN_C_END
//
// If there is any new downward propagating state, it should be added to this define.
//
// If the only change in a trait collection is that its dislplayContext has gone from non-nil to nil,
// assume that we are clearing the context as part of a ASVC dealloc and do not trigger a layout.
//
// This logic is used in both ASCollectionNode and ASTableNode
#define ASEnvironmentCollectionTableSetEnvironmentState(lock) \
- (void)setEnvironmentState:(ASEnvironmentState)environmentState\
@@ -156,12 +159,16 @@ ASDISPLAYNODE_EXTERN_C_END
[super setEnvironmentState:environmentState];\
ASEnvironmentTraitCollection currentTraits = environmentState.environmentTraitCollection;\
if (ASEnvironmentTraitCollectionIsEqualToASEnvironmentTraitCollection(currentTraits, oldTraits) == NO) {\
/* Must dispatch to main for self.view && [self.view.dataController completedNodes]*/ \
ASPerformBlockOnMainThread(^{\
BOOL needsLayout = (oldTraits.displayContext == currentTraits.displayContext) || currentTraits.displayContext != nil;\
NSArray<NSArray <ASCellNode *> *> *completedNodes = [self.view.dataController completedNodes];\
for (NSArray *sectionArray in completedNodes) {\
for (ASCellNode *cellNode in sectionArray) {\
ASEnvironmentStatePropagateDown(cellNode, currentTraits);\
[cellNode setNeedsLayout];\
if (needsLayout) {\
[cellNode setNeedsLayout];\
}\
}\
}\
});\

View File

@@ -85,7 +85,23 @@
static PINRemoteImageManager *sharedPINRemoteImageManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
#if PIN_ANIMATED_AVAILABLE
// Check that Carthage users have linked both PINRemoteImage & PINCache by testing for one file each
if (!(NSClassFromString(@"PINRemoteImageManager"))) {
NSException *e = [NSException
exceptionWithName:@"FrameworkSetupException"
reason:@"Missing the path to the PINRemoteImage framework."
userInfo:nil];
@throw e;
}
if (!(NSClassFromString(@"PINCache"))) {
NSException *e = [NSException
exceptionWithName:@"FrameworkSetupException"
reason:@"Missing the path to the PINCache framework."
userInfo:nil];
@throw e;
}
sharedPINRemoteImageManager = [[PINRemoteImageManager alloc] initWithSessionConfiguration:nil alternativeRepresentationProvider:self];
#else
sharedPINRemoteImageManager = [[PINRemoteImageManager alloc] initWithSessionConfiguration:nil];

View File

@@ -146,6 +146,13 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)rangeController:(ASRangeController * )rangeController didEndUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion;
/**
* Completed updates to cell node addition and removal.
*
* @param rangeController Sender.
*/
- (void)didCompleteUpdatesInRangeController:(ASRangeController *)rangeController;
/**
* Called for nodes insertion.
*

View File

@@ -333,6 +333,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive;
[modifiedIndexPaths sortUsingSelector:@selector(compare:)];
NSLog(@"Range update complete; modifiedIndexPaths: %@", [self descriptionWithIndexPaths:modifiedIndexPaths]);
#endif
[_delegate didCompleteUpdatesInRangeController:self];
}
#pragma mark - Notification observers

View File

@@ -327,7 +327,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance()
_callbackQueue = callbackQueue;
_completionBlock = [completionBlock copy];
_state = ASAsyncTransactionStateOpen;
__atomic_store_n(&_state, ASAsyncTransactionStateOpen, __ATOMIC_SEQ_CST);
}
return self;
}
@@ -335,7 +335,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance()
- (void)dealloc
{
// Uncommitted transactions break our guarantees about releasing completion blocks on callbackQueue.
ASDisplayNodeAssert(_state != ASAsyncTransactionStateOpen, @"Uncommitted ASAsyncTransactions are not allowed");
ASDisplayNodeAssert(__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateOpen, @"Uncommitted ASAsyncTransactions are not allowed");
if (_group) {
_group->release();
}
@@ -360,7 +360,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance()
completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion
{
ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(_state == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions");
ASDisplayNodeAssert(__atomic_load_n(&_state, __ATOMIC_SEQ_CST) == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions");
[self _ensureTransactionData];
@@ -368,7 +368,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance()
[_operations addObject:operation];
_group->schedule(priority, queue, ^{
@autoreleasepool {
if (_state != ASAsyncTransactionStateCanceled) {
if (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateCanceled) {
_group->enter();
block(^(id<NSObject> value){
operation.value = value;
@@ -395,7 +395,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance()
completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion
{
ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(_state == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions");
ASDisplayNodeAssert(__atomic_load_n(&_state, __ATOMIC_SEQ_CST) == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions");
[self _ensureTransactionData];
@@ -403,7 +403,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance()
[_operations addObject:operation];
_group->schedule(priority, queue, ^{
@autoreleasepool {
if (_state != ASAsyncTransactionStateCanceled) {
if (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateCanceled) {
operation.value = block();
}
}
@@ -422,15 +422,15 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance()
- (void)cancel
{
ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(_state != ASAsyncTransactionStateOpen, @"You can only cancel a committed or already-canceled transaction");
_state = ASAsyncTransactionStateCanceled;
ASDisplayNodeAssert(__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateOpen, @"You can only cancel a committed or already-canceled transaction");
__atomic_store_n(&_state, ASAsyncTransactionStateCanceled, __ATOMIC_SEQ_CST);
}
- (void)commit
{
ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(_state == ASAsyncTransactionStateOpen, @"You cannot double-commit a transaction");
_state = ASAsyncTransactionStateCommitted;
ASDisplayNodeAssert(__atomic_load_n(&_state, __ATOMIC_SEQ_CST) == ASAsyncTransactionStateOpen, @"You cannot double-commit a transaction");
__atomic_store_n(&_state, ASAsyncTransactionStateCommitted, __ATOMIC_SEQ_CST);
if ([_operations count] == 0) {
// Fast path: if a transaction was opened, but no operations were added, execute completion block synchronously.
@@ -451,8 +451,8 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance()
- (void)completeTransaction
{
if (_state != ASAsyncTransactionStateComplete) {
BOOL isCanceled = (_state == ASAsyncTransactionStateCanceled);
if (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateComplete) {
BOOL isCanceled = (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) == ASAsyncTransactionStateCanceled);
for (ASDisplayNodeAsyncTransactionOperation *operation in _operations) {
[operation callAndReleaseCompletionBlock:isCanceled];
}
@@ -460,7 +460,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance()
// Always set _state to Complete, even if we were cancelled, to block any extraneous
// calls to this method that may have been scheduled for the next runloop
// (e.g. if we needed to force one in this runloop with -waitUntilComplete, but another was already scheduled)
_state = ASAsyncTransactionStateComplete;
__atomic_store_n(&_state, ASAsyncTransactionStateComplete, __ATOMIC_SEQ_CST);
if (_completionBlock) {
_completionBlock(self, isCanceled);
@@ -471,7 +471,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance()
- (void)waitUntilComplete
{
ASDisplayNodeAssertMainThread();
if (_state != ASAsyncTransactionStateComplete) {
if (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateComplete) {
if (_group) {
ASDisplayNodeAssertTrue(_callbackQueue == dispatch_get_main_queue());
_group->wait();
@@ -481,9 +481,9 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance()
// commit ourselves via the group to avoid double-committing the transaction.
// This is only necessary when forcing display work to complete before allowing the runloop
// to continue, e.g. in the implementation of -[ASDisplayNode recursivelyEnsureDisplay].
if (_state == ASAsyncTransactionStateOpen) {
if (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) == ASAsyncTransactionStateOpen) {
[_ASAsyncTransactionGroup commit];
ASDisplayNodeAssert(_state != ASAsyncTransactionStateOpen, @"Transaction should not be open after committing group");
ASDisplayNodeAssert(__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateOpen, @"Transaction should not be open after committing group");
}
// If we needed to commit the group above, -completeTransaction may have already been run.
// It is designed to accommodate this by checking _state to ensure it is not complete.
@@ -508,7 +508,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance()
- (NSString *)description
{
return [NSString stringWithFormat:@"<_ASAsyncTransaction: %p - _state = %lu, _group = %p, _operations = %@>", self, (unsigned long)_state, _group, _operations];
return [NSString stringWithFormat:@"<_ASAsyncTransaction: %p - _state = %lu, _group = %p, _operations = %@>", self, (unsigned long)__atomic_load_n(&_state, __ATOMIC_SEQ_CST), _group, _operations];
}
@end

View File

@@ -14,7 +14,8 @@
#import "ASBaseDefines.h"
#import "ASLayout.h"
static NSString * const kBackgroundChildKey = @"kBackgroundChildKey";
static NSUInteger const kForegroundChildIndex = 0;
static NSUInteger const kBackgroundChildIndex = 1;
@interface ASBackgroundLayoutSpec ()
@end
@@ -28,7 +29,7 @@ static NSString * const kBackgroundChildKey = @"kBackgroundChildKey";
}
ASDisplayNodeAssertNotNil(child, @"Child cannot be nil");
[self setChild:child];
[self setChild:child forIndex:kForegroundChildIndex];
self.background = background;
return self;
}
@@ -63,12 +64,12 @@ static NSString * const kBackgroundChildKey = @"kBackgroundChildKey";
- (void)setBackground:(id<ASLayoutable>)background
{
[super setChild:background forIdentifier:kBackgroundChildKey];
[super setChild:background forIndex:kBackgroundChildIndex];
}
- (id<ASLayoutable>)background
{
return [super childForIdentifier:kBackgroundChildKey];
return [super childForIndex:kBackgroundChildIndex];
}
@end

View File

@@ -40,10 +40,9 @@ NS_ASSUME_NONNULL_BEGIN
* only require a single child.
*
* For layout specs that require a known number of children (ASBackgroundLayoutSpec, for example)
* a subclass should use this method to set the "primary" child. It can then use setChild:forIdentifier:
* to set any other required children. Ideally a subclass would hide this from the user, and use the
* setChild:forIdentifier: internally. For example, ASBackgroundLayoutSpec exposes a backgroundChild
* property that behind the scenes is calling setChild:forIdentifier:.
* a subclass should use this method to set the "primary" child. This is actually the same as calling
* setChild:forIdentifier:0. All other children should be set by defining convenience methods
* that call setChild:forIdentifier behind the scenes.
*/
- (void)setChild:(id<ASLayoutable>)child;
@@ -52,19 +51,19 @@ NS_ASSUME_NONNULL_BEGIN
*
* @param child A child to be added.
*
* @param identifier An identifier associated with the child.
* @param index An index associated with the child.
*
* @discussion Every ASLayoutSpec must act on at least one child. The ASLayoutSpec base class takes the
* responsibility of holding on to the spec children. Some layout specs, like ASInsetLayoutSpec,
* only require a single child.
*
* For layout specs that require a known number of children (ASBackgroundLayoutSpec, for example)
* a subclass should use the setChild method to set the "primary" child. It can then use this method
* a subclass can use the setChild method to set the "primary" child. It should then use this method
* to set any other required children. Ideally a subclass would hide this from the user, and use the
* setChild:forIdentifier: internally. For example, ASBackgroundLayoutSpec exposes a backgroundChild
* property that behind the scenes is calling setChild:forIdentifier:.
* setChild:forIndex: internally. For example, ASBackgroundLayoutSpec exposes a backgroundChild
* property that behind the scenes is calling setChild:forIndex:.
*/
- (void)setChild:(id<ASLayoutable>)child forIdentifier:(NSString *)identifier;
- (void)setChild:(id<ASLayoutable>)child forIndex:(NSUInteger)index;
/**
* Adds childen to this layout spec.
@@ -94,11 +93,11 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable id<ASLayoutable>)child;
/**
* Returns the child added to this layout spec using the given identifier.
* Returns the child added to this layout spec using the given index.
*
* @param identifier An identifier associated withe the child.
* @param index An identifier associated withe the child.
*/
- (nullable id<ASLayoutable>)childForIdentifier:(NSString *)identifier;
- (nullable id<ASLayoutable>)childForIndex:(NSUInteger)index;
/**
* Returns all children added to this layout spec.

View File

@@ -20,14 +20,15 @@
#import "ASTraitCollection.h"
#import <objc/runtime.h>
#import <map>
#import <vector>
typedef std::map<unsigned long, id<ASLayoutable>, std::less<unsigned long>> ASChildMap;
@interface ASLayoutSpec() {
ASEnvironmentState _environmentState;
ASDN::RecursiveMutex _propertyLock;
NSArray *_children;
NSMutableDictionary *_childrenWithIdentifier;
ASChildMap _children;
}
@end
@@ -45,7 +46,6 @@
}
_isMutable = YES;
_environmentState = ASEnvironmentStateMakeDefault();
_children = [NSArray array];
return self;
}
@@ -102,14 +102,6 @@
return child;
}
- (NSMutableDictionary *)childrenWithIdentifier
{
if (!_childrenWithIdentifier) {
_childrenWithIdentifier = [NSMutableDictionary dictionary];
}
return _childrenWithIdentifier;
}
- (void)setParent:(id<ASLayoutable>)parent
{
// FIXME: Locking should be evaluated here. _parent is not widely used yet, though.
@@ -126,34 +118,23 @@
if (child) {
id<ASLayoutable> finalLayoutable = [self layoutableToAddFromLayoutable:child];
if (finalLayoutable) {
_children = @[finalLayoutable];
_children[0] = finalLayoutable;
[self propagateUpLayoutable:finalLayoutable];
}
} else {
// remove the only child
_children = [NSArray array];
_children.erase(0);
}
}
- (void)setChild:(id<ASLayoutable>)child forIdentifier:(NSString *)identifier
- (void)setChild:(id<ASLayoutable>)child forIndex:(NSUInteger)index
{
ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable");
if (child) {
id<ASLayoutable> finalLayoutable = [self layoutableToAddFromLayoutable:child];
self.childrenWithIdentifier[identifier] = finalLayoutable;
if (finalLayoutable) {
_children = [_children arrayByAddingObject:finalLayoutable];
}
_children[index] = finalLayoutable;
} else {
id<ASLayoutable> oldChild = self.childrenWithIdentifier[identifier];
if (oldChild) {
self.childrenWithIdentifier[identifier] = nil;
NSMutableArray *mutableChildren = [_children mutableCopy];
[mutableChildren removeObject:oldChild];
_children = [mutableChildren copy];
}
_children.erase(index);
}
// TODO: Should we propagate up the layoutable at it could happen that multiple children will propagated up their
// layout options and one child will overwrite values from another child
// [self propagateUpLayoutable:finalLayoutable];
@@ -163,32 +144,33 @@
{
ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable");
std::vector<id<ASLayoutable>> finalChildren;
for (id<ASLayoutable> child in children) {
finalChildren.push_back([self layoutableToAddFromLayoutable:child]);
}
_children = nil;
if (finalChildren.size() > 0) {
_children = [NSArray arrayWithObjects:&finalChildren[0] count:finalChildren.size()];
} else {
_children = [NSArray array];
}
_children.clear();
[children enumerateObjectsUsingBlock:^(id<ASLayoutable> _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
_children[idx] = obj;
}];
}
- (id<ASLayoutable>)childForIdentifier:(NSString *)identifier
- (id<ASLayoutable>)childForIndex:(NSUInteger)index
{
return self.childrenWithIdentifier[identifier];
if (index < _children.size()) {
return _children[index];
}
return nil;
}
- (id<ASLayoutable>)child
{
return [_children firstObject];
return _children[0];
}
- (NSArray *)children
{
return _children;
std::vector<ASLayout *> children;
for (ASChildMap::iterator it = _children.begin(); it != _children.end(); ++it ) {
children.push_back(it->second);
}
return [NSArray arrayWithObjects:&children[0] count:children.size()];
}
#pragma mark - ASEnvironment

View File

@@ -14,7 +14,8 @@
#import "ASBaseDefines.h"
#import "ASLayout.h"
static NSString * const kOverlayChildKey = @"kOverlayChildKey";
static NSUInteger const kUnderlayChildIndex = 0;
static NSUInteger const kOverlayChildIndex = 1;
@implementation ASOverlayLayoutSpec
@@ -25,7 +26,7 @@ static NSString * const kOverlayChildKey = @"kOverlayChildKey";
}
ASDisplayNodeAssertNotNil(child, @"Child that will be overlayed on shouldn't be nil");
self.overlay = overlay;
[self setChild:child];
[self setChild:child forIndex:kUnderlayChildIndex];
return self;
}
@@ -36,12 +37,12 @@ static NSString * const kOverlayChildKey = @"kOverlayChildKey";
- (void)setOverlay:(id<ASLayoutable>)overlay
{
[super setChild:overlay forIdentifier:kOverlayChildKey];
[super setChild:overlay forIndex:kOverlayChildIndex];
}
- (id<ASLayoutable>)overlay
{
return [super childForIdentifier:kOverlayChildKey];
return [super childForIndex:kOverlayChildIndex];
}
/**

View File

@@ -75,6 +75,20 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo
unsigned displaySuspended:1;
unsigned shouldAnimateSizeChanges:1;
unsigned hasCustomDrawingPriority:1;
// Wrapped view handling
// The layer contents should not be cleared in case the node is wrapping a UIImageView.UIImageView is specifically
// optimized for performance and does not use the usual way to provide the contents of the CALayer via the
// CALayerDelegate method that backs the UIImageView.
unsigned canClearContentsOfLayer:1;
// Prevent calling setNeedsDisplay on a layer that backs a UIImageView. Usually calling setNeedsDisplay on a CALayer
// triggers a recreation of the contents of layer unfortunately calling it on a CALayer that backs a UIImageView
// it goes trough the normal flow to assign the contents to a layer via the CALayerDelegate methods. Unfortunately
// UIImageView does not do recreate the layer contents the usual way, it actually does not implement some of the
// methods at all instead it throws away the contents of the layer and nothing will show up.
unsigned canCallNeedsDisplayOfLayer:1;
// whether custom drawing is enabled
unsigned implementsInstanceDrawRect:1;

View File

@@ -48,9 +48,12 @@
{
ASDN::MutexLocker l(_propertyLock);
[self calculateSubnodeOperationsIfNeeded];
for (NSUInteger i = 0; i < [_insertedSubnodes count]; i++) {
NSUInteger i = 0;
for (ASDisplayNode *node in _insertedSubnodes) {
NSUInteger p = _insertedSubnodePositions[i];
[_node insertSubnode:_insertedSubnodes[i] atIndex:p];
[_node insertSubnode:node atIndex:p];
i += 1;
}
}
@@ -58,8 +61,8 @@
{
ASDN::MutexLocker l(_propertyLock);
[self calculateSubnodeOperationsIfNeeded];
for (NSUInteger i = 0; i < [_removedSubnodes count]; i++) {
[_removedSubnodes[i] removeFromSupernode];
for (ASDisplayNode *subnode in _removedSubnodes) {
[subnode removeFromSupernode];
}
}

View File

@@ -11,6 +11,10 @@
#import "ASAssert.h"
#import "ASMultidimensionalArrayUtils.h"
// Import UIKit to get [NSIndexPath indexPathForItem:inSection:] which uses
// static memory addresses rather than allocating new index path objects.
#import <UIKit/UIKit.h>
#pragma mark - Internal Methods
static void ASRecursivelyUpdateMultidimensionalArrayAtIndexPaths(NSMutableArray *mutableArray,
@@ -25,8 +29,10 @@ static void ASRecursivelyUpdateMultidimensionalArrayAtIndexPaths(NSMutableArray
}
if (curIndexPath.length < dimension - 1) {
for (int i = 0; i < mutableArray.count; i++) {
ASRecursivelyUpdateMultidimensionalArrayAtIndexPaths(mutableArray[i], indexPaths, curIdx, [curIndexPath indexPathByAddingIndex:i], dimension, updateBlock);
NSInteger i = 0;
for (NSMutableArray *subarray in mutableArray) {
ASRecursivelyUpdateMultidimensionalArrayAtIndexPaths(subarray, indexPaths, curIdx, [curIndexPath indexPathByAddingIndex:i], dimension, updateBlock);
i += 1;
}
} else {
NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] init];
@@ -41,7 +47,7 @@ static void ASRecursivelyUpdateMultidimensionalArrayAtIndexPaths(NSMutableArray
}
}
static void ASRecursivelyFindIndexPathsForMultidimensionalArray(NSObject *obj, NSIndexPath *curIndexPath, NSMutableArray *res)
static void ASRecursivelyFindIndexPathsForMultidimensionalArray(NSObject *obj, NSIndexPath *curIndexPath, NSMutableArray <NSIndexPath *>*res)
{
if (![obj isKindOfClass:[NSArray class]]) {
[res addObject:curIndexPath];
@@ -72,7 +78,12 @@ static BOOL ASElementExistsAtIndexPathForMultidimensionalArray(NSArray *array, N
NSUInteger indexesLength = indexLength - 1;
NSUInteger indexes[indexesLength];
[indexPath getIndexes:indexes range:NSMakeRange(1, indexesLength)];
NSIndexPath *indexPathByRemovingFirstIndex = [NSIndexPath indexPathWithIndexes:indexes length:indexesLength];
NSIndexPath *indexPathByRemovingFirstIndex;
if (indexesLength == 2) {
indexPathByRemovingFirstIndex = [NSIndexPath indexPathForItem:indexes[1] inSection:indexes[0]];
} else {
indexPathByRemovingFirstIndex = [NSIndexPath indexPathWithIndexes:indexes length:indexesLength];
}
return ASElementExistsAtIndexPathForMultidimensionalArray(array[firstIndex], indexPathByRemovingFirstIndex);
}
@@ -184,9 +195,8 @@ NSArray *ASIndexPathsForTwoDimensionalArray(NSArray <NSArray *>* twoDimensionalA
NSUInteger section = 0;
for (NSArray *subarray in twoDimensionalArray) {
ASDisplayNodeCAssert([subarray isKindOfClass:[NSArray class]], @"This function expects NSArray<NSArray *> *");
NSUInteger itemCount = subarray.count;
for (NSUInteger item = 0; item < itemCount; item++) {
[result addObject:[NSIndexPath indexPathWithIndexes:(const NSUInteger []){ section, item } length:2]];
for (NSUInteger item = 0; item < subarray.count; item++) {
[result addObject:[NSIndexPath indexPathForItem:item inSection:section]];
}
section++;
}

View File

@@ -442,7 +442,7 @@ typedef enum {
-(BOOL)_tryRetain { \
__typeof__(_rc_ivar) _prev; \
do { \
_prev = _rc_ivar; \
_prev = __atomic_load_n(&_rc_ivar, __ATOMIC_SEQ_CST);; \
if (_prev & 1) { \
return 0; \
} else if (_prev == -2) { \
@@ -454,12 +454,13 @@ typedef enum {
return 1; \
} \
-(BOOL)_isDeallocating { \
if (_rc_ivar == -2) { \
__typeof__(_rc_ivar) _prev = __atomic_load_n(&_rc_ivar, __ATOMIC_SEQ_CST); \
if (_prev == -2) { \
return 1; \
} else if (_rc_ivar < -2) { \
} else if (_prev < -2) { \
__builtin_trap(); /* BUG: over-release elsewhere */ \
} \
return _rc_ivar & 1; \
return _prev & 1; \
}
#define _OBJC_SUPPORTED_INLINE_REFCNT_LOGIC(_rc_ivar, _dealloc2main) \

View File

@@ -9,14 +9,14 @@
//
#import "ASTextKitContext.h"
#import "ASThread.h"
#import "ASLayoutManager.h"
#import <mutex>
@implementation ASTextKitContext
{
// All TextKit operations (even non-mutative ones) must be executed serially.
ASDN::Mutex _textKitMutex;
std::mutex _textKitMutex;
NSLayoutManager *_layoutManager;
NSTextStorage *_textStorage;
@@ -35,8 +35,8 @@
{
if (self = [super init]) {
// Concurrently initialising TextKit components crashes (rdar://18448377) so we use a global lock.
static ASDN::Mutex __staticMutex;
ASDN::MutexLocker l(__staticMutex);
static std::mutex __static_mutex;
std::lock_guard<std::mutex> l(__static_mutex);
// Create the TextKit component stack with our default configuration.
if (textStorageCreationBlock) {
_textStorage = textStorageCreationBlock(attributedString);
@@ -60,13 +60,13 @@
- (CGSize)constrainedSize
{
ASDN::MutexLocker l(_textKitMutex);
std::lock_guard<std::mutex> l(_textKitMutex);
return _textContainer.size;
}
- (void)setConstrainedSize:(CGSize)constrainedSize
{
ASDN::MutexLocker l(_textKitMutex);
std::lock_guard<std::mutex> l(_textKitMutex);
_textContainer.size = constrainedSize;
}
@@ -74,7 +74,7 @@
NSTextStorage *,
NSTextContainer *))block
{
ASDN::MutexLocker l(_textKitMutex);
std::lock_guard<std::mutex> l(_textKitMutex);
block(_layoutManager, _textStorage, _textContainer);
}

View File

@@ -227,7 +227,12 @@ static _ASDisplayLayerTestDelegateClassModes _class_modes;
// DANGER: Don't use the delegate as the parameters in real code; this is not thread-safe and just for accounting in unit tests!
+ (void)drawRect:(CGRect)bounds withParameters:(_ASDisplayLayerTestDelegate *)delegate isCancelled:(asdisplaynode_iscancelled_block_t)sentinelBlock isRasterizing:(BOOL)isRasterizing
{
delegate->_drawRectCount++;
__atomic_add_fetch(&delegate->_drawRectCount, 1, __ATOMIC_SEQ_CST);
}
- (NSUInteger)drawRectCount
{
return(__atomic_load_n(&_drawRectCount, __ATOMIC_SEQ_CST));
}
- (void)dealloc
@@ -267,9 +272,9 @@ static _ASDisplayLayerTestDelegateClassModes _class_modes;
// make sure we don't lock up the tests indefinitely; fail after 1 sec by using an async barrier
__block BOOL didHitBarrier = NO;
dispatch_barrier_async([_ASDisplayLayer displayQueue], ^{
didHitBarrier = YES;
__atomic_store_n(&didHitBarrier, YES, __ATOMIC_SEQ_CST);
});
XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ return didHitBarrier; }));
XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ return __atomic_load_n(&didHitBarrier, __ATOMIC_SEQ_CST); }));
}
- (void)waitForLayer:(_ASDisplayLayerTestLayer *)layer asyncDisplayCount:(NSUInteger)count

View File

@@ -60,11 +60,11 @@ static dispatch_block_t modifyMethodByAddingPrologueBlockAndReturnCleanupBlock(C
@end
// Conveniences for making nodes named a certain way
#define DeclareNodeNamed(n) ASDisplayNode *n = [[ASDisplayNode alloc] init]; n.name = @#n
#define DeclareNodeNamed(n) ASDisplayNode *n = [[[ASDisplayNode alloc] init] autorelease]; n.name = @#n
#define DeclareViewNamed(v) UIView *v = viewWithName(@#v)
static UIView *viewWithName(NSString *name) {
ASDisplayNode *n = [[ASDisplayNode alloc] init];
ASDisplayNode *n = [[[ASDisplayNode alloc] init] autorelease];
n.name = name;
return n.view;
}
@@ -130,7 +130,7 @@ static UIView *viewWithName(NSString *name) {
- (void)checkAppearanceMethodsCalledWithRootNodeInWindowLayerBacked:(BOOL)isLayerBacked
{
// ASDisplayNode visibility does not change if modifying a hierarchy that is not in a window. So create one and add the superview to it.
UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectZero];
UIWindow *window = [[[UIWindow alloc] initWithFrame:CGRectZero] autorelease];
DeclareNodeNamed(n);
DeclareViewNamed(superview);
@@ -162,15 +162,12 @@ static UIView *viewWithName(NSString *name) {
XCTAssertEqual([_willEnterHierarchyCounts countForObject:n], 1u, @"willEnterHierarchy not called when node's view added to hierarchy");
XCTAssertEqual([_didExitHierarchyCounts countForObject:n], 1u, @"didExitHierarchy erroneously called");
[superview release];
[window release];
}
- (void)checkManualAppearanceViewLoaded:(BOOL)isViewLoaded layerBacked:(BOOL)isLayerBacked
{
// ASDisplayNode visibility does not change if modifying a hierarchy that is not in a window. So create one and add the superview to it.
UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectZero];
UIWindow *window = [[[UIWindow alloc] initWithFrame:CGRectZero] autorelease];
DeclareNodeNamed(parent);
DeclareNodeNamed(a);
@@ -263,13 +260,13 @@ static UIView *viewWithName(NSString *name) {
- (void)testSynchronousIntermediaryView
{
// Parent is a wrapper node for a scrollview
ASDisplayNode *parentSynchronousNode = [[ASDisplayNode alloc] initWithViewClass:[UIScrollView class]];
ASDisplayNode *parentSynchronousNode = [[[ASDisplayNode alloc] initWithViewClass:[UIScrollView class]] autorelease];
DeclareNodeNamed(layerBackedNode);
DeclareNodeNamed(viewBackedNode);
layerBackedNode.layerBacked = YES;
UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectZero];
UIWindow *window = [[[UIWindow alloc] initWithFrame:CGRectZero] autorelease];
[parentSynchronousNode addSubnode:layerBackedNode];
[parentSynchronousNode addSubnode:viewBackedNode];
@@ -303,11 +300,6 @@ static UIView *viewWithName(NSString *name) {
XCTAssertFalse(parentSynchronousNode.inHierarchy, @"Should not have changed");
XCTAssertFalse(layerBackedNode.inHierarchy, @"Should have been marked invisible when synchronous superview was removed from the window");
XCTAssertFalse(viewBackedNode.inHierarchy, @"Should have been marked invisible when synchronous superview was removed from the window");
[window release];
[parentSynchronousNode release];
[layerBackedNode release];
[viewBackedNode release];
}
- (void)checkMoveAcrossHierarchyLayerBacked:(BOOL)isLayerBacked useManualCalls:(BOOL)useManualDisable useNodeAPI:(BOOL)useNodeAPI

View File

@@ -21,18 +21,18 @@
#import "ASCellNode.h"
// Conveniences for making nodes named a certain way
#define DeclareNodeNamed(n) ASDisplayNode *n = [[ASDisplayNode alloc] init]; n.name = @#n
#define DeclareNodeNamed(n) ASDisplayNode *n = [[[ASDisplayNode alloc] init] autorelease]; n.name = @#n
#define DeclareViewNamed(v) UIView *v = viewWithName(@#v)
#define DeclareLayerNamed(l) CALayer *l = layerWithName(@#l)
static UIView *viewWithName(NSString *name) {
ASDisplayNode *n = [[ASDisplayNode alloc] init];
ASDisplayNode *n = [[[ASDisplayNode alloc] init] autorelease];
n.name = name;
return n.view;
}
static CALayer *layerWithName(NSString *name) {
ASDisplayNode *n = [[ASDisplayNode alloc] init];
ASDisplayNode *n = [[[ASDisplayNode alloc] init] autorelease];
n.layerBacked = YES;
n.name = name;
return n.layer;
@@ -144,6 +144,12 @@ for (ASDisplayNode *n in @[ nodes ]) {\
{
if (_willDeallocBlock) {
_willDeallocBlock(self);
[_willDeallocBlock release];
_willDeallocBlock = nil;
}
if (_calculateSizeBlock) {
[_calculateSizeBlock release];
_calculateSizeBlock = nil;
}
[super dealloc];
}
@@ -214,19 +220,19 @@ for (ASDisplayNode *n in @[ nodes ]) {\
}
- (void)testOverriddenFirstResponderBehavior {
ASTestDisplayNode *node = [[ASTestResponderNode alloc] init];
ASTestDisplayNode *node = [[[ASTestResponderNode alloc] init] autorelease];
XCTAssertTrue([node canBecomeFirstResponder]);
XCTAssertTrue([node becomeFirstResponder]);
}
- (void)testDefaultFirstResponderBehavior {
ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init];
ASTestDisplayNode *node = [[[ASTestDisplayNode alloc] init] autorelease];
XCTAssertFalse([node canBecomeFirstResponder]);
XCTAssertFalse([node becomeFirstResponder]);
}
- (void)testLayerBackedFirstResponderBehavior {
ASTestDisplayNode *node = [[ASTestResponderNode alloc] init];
ASTestDisplayNode *node = [[[ASTestResponderNode alloc] init] autorelease];
node.layerBacked = YES;
XCTAssertTrue([node canBecomeFirstResponder]);
XCTAssertFalse([node becomeFirstResponder]);
@@ -250,6 +256,8 @@ for (ASDisplayNode *n in @[ nodes ]) {\
[self executeOffThread:^{
node = [[ASDisplayNode alloc] init];
}];
// executeOffThread: blocks until the background thread finishes executing.
node = [node autorelease]; // XXX This is very bad style.
UIView *view = node.view;
XCTAssertNotNil(view, @"Getting node's view on-thread should succeed.");
@@ -257,7 +265,7 @@ for (ASDisplayNode *n in @[ nodes ]) {\
- (void)testNodeCreatedOffThreadWithExistingView
{
UIView *view = [[UIDisplayNodeTestView alloc] init];
UIView *view = [[[UIDisplayNodeTestView alloc] init] autorelease];
__block ASDisplayNode *node = nil;
[self executeOffThread:^{
@@ -265,6 +273,8 @@ for (ASDisplayNode *n in @[ nodes ]) {\
return view;
}];
}];
// executeOffThread: blocks until the background thread finishes executing.
node = [node autorelease]; // XXX This is very bad style.
XCTAssertFalse(node.layerBacked, @"Can't be layer backed");
XCTAssertTrue(node.synchronous, @"Node with plain view should be synchronous");
@@ -283,6 +293,9 @@ for (ASDisplayNode *n in @[ nodes ]) {\
return view;
}];
}];
// executeOffThread: blocks until the background thread finishes executing.
view = [view autorelease]; // XXX This is very bad style.
node = [node autorelease]; // XXX This is very bad style.
XCTAssertNil(view, @"View block should not be invoked yet");
[node view];
@@ -293,10 +306,10 @@ for (ASDisplayNode *n in @[ nodes ]) {\
- (void)testNodeCreatedWithLazyAsyncView
{
ASDisplayNode *node = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{
ASDisplayNode *node = [[[ASDisplayNode alloc] initWithViewBlock:^UIView *{
XCTAssertTrue([NSThread isMainThread], @"View block must run on the main queue");
return [[_ASDisplayView alloc] init];
}];
return [[[_ASDisplayView alloc] init] autorelease];
}] autorelease];
XCTAssertThrows([node view], @"Externally provided views should be synchronous");
XCTAssertTrue(node.synchronous, @"Node with externally provided view should be synchronous");
@@ -631,7 +644,7 @@ for (ASDisplayNode *n in @[ nodes ]) {\
// Perform parallel updates of a standard UIView/CALayer and an ASDisplayNode and ensure they are equivalent.
- (void)testDeriveFrameFromBoundsPositionAnchorPoint
{
UIView *plainView = [[UIView alloc] initWithFrame:CGRectZero];
UIView *plainView = [[[UIView alloc] initWithFrame:CGRectZero] autorelease];
plainView.layer.anchorPoint = CGPointMake(0.25f, 0.75f);
plainView.layer.position = CGPointMake(10, 20);
plainView.layer.bounds = CGRectMake(0, 0, 60, 80);
@@ -643,6 +656,8 @@ for (ASDisplayNode *n in @[ nodes ]) {\
node.bounds = CGRectMake(0, 0, 60, 80);
node.position = CGPointMake(10, 20);
}];
// executeOffThread: blocks until the background thread finishes executing.
node = [node autorelease]; // XXX This is very bad style.
XCTAssertTrue(CGRectEqualToRect(plainView.frame, node.frame), @"Node frame should match UIView frame before realization.");
XCTAssertTrue(CGRectEqualToRect(plainView.frame, node.view.frame), @"Realized view frame should match UIView frame.");
@@ -651,7 +666,7 @@ for (ASDisplayNode *n in @[ nodes ]) {\
// Perform parallel updates of a standard UIView/CALayer and an ASDisplayNode and ensure they are equivalent.
- (void)testSetFrameSetsBoundsPosition
{
UIView *plainView = [[UIView alloc] initWithFrame:CGRectZero];
UIView *plainView = [[[UIView alloc] initWithFrame:CGRectZero] autorelease];
plainView.layer.anchorPoint = CGPointMake(0.25f, 0.75f);
plainView.layer.frame = CGRectMake(10, 20, 60, 80);
@@ -661,6 +676,8 @@ for (ASDisplayNode *n in @[ nodes ]) {\
node.anchorPoint = CGPointMake(0.25f, 0.75f);
node.frame = CGRectMake(10, 20, 60, 80);
}];
// executeOffThread: blocks until the background thread finishes executing.
node = [node autorelease]; // XXX This is very bad style.
XCTAssertTrue(CGPointEqualToPoint(plainView.layer.position, node.position), @"Node position should match UIView position before realization.");
XCTAssertTrue(CGRectEqualToRect(plainView.layer.bounds, node.bounds), @"Node bounds should match UIView bounds before realization.");
@@ -921,7 +938,7 @@ for (ASDisplayNode *n in @[ nodes ]) {\
- (void)testDisplayNodePointConversionOnDeepHierarchies
{
ASDisplayNode *node = [[ASDisplayNode alloc] init];
ASDisplayNode *node = [[[ASDisplayNode alloc] init] autorelease];
// 7 deep (six below root); each one positioned at position = (1, 1)
_addTonsOfSubnodes(node, 2, 6, ^(ASDisplayNode *createdNode) {
@@ -1108,7 +1125,7 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point
- (void)testSubnodes
{
ASDisplayNode *parent = [[ASDisplayNode alloc] init];
ASDisplayNode *parent = [[[ASDisplayNode alloc] init] autorelease];
ASDisplayNode *nilNode = nil;
XCTAssertNoThrow([parent addSubnode:nilNode], @"Don't try to add nil, but we'll deal.");
XCTAssertNoThrow([parent addSubnode:parent], @"Not good, test that we recover");
@@ -1211,11 +1228,6 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point
XCTAssertNodesHaveParent(nilParent, a,d);
//TODO: assert that things deallocate immediately and don't have latent autoreleases in here
[parent release];
[a release];
[b release];
[c release];
[d release];
}
- (void)testInsertSubnodeAtIndexView
@@ -1344,17 +1356,12 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point
XCTAssertNodesHaveParent(nilParent, d);
//TODO: assert that things deallocate immediately and don't have latent autoreleases in here
[parent release];
[a release];
[b release];
[c release];
[d release];
}
// This tests our resiliancy to having other views and layers inserted into our view or layer
- (void)testInsertSubviewAtIndexWithMeddlingViewsAndLayersViewBacked
{
ASDisplayNode *parent = [[ASDisplayNode alloc] init];
ASDisplayNode *parent = [[[ASDisplayNode alloc] init] autorelease];
DeclareNodeNamed(a);
DeclareNodeNamed(b);
@@ -1389,12 +1396,6 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point
XCTAssertEqual(4u, parent.layer.sublayers.count, @"Should have the right sublayer count");
//TODO: assert that things deallocate immediately and don't have latent autoreleases in here
[parent release];
[a release];
[b release];
[c release];
[d release];
[e release];
}
- (void)testAppleBugInsertSubview
@@ -1467,11 +1468,6 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point
XCTAssertEqual(4u, parent.layer.sublayers.count, @"Should have the right sublayer count");
//TODO: assert that things deallocate immediately and don't have latent autoreleases in here
[parent release];
[a release];
[b release];
[c release];
[d release];
}
@@ -1550,10 +1546,6 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point
XCTAssertNodesHaveParent(parent, a, c, b);
//TODO: assert that things deallocate immediately and don't have latent autoreleases in here
[parent release];
[a release];
[b release];
[c release];
}
- (void)testInsertSubnodeAboveWithView
@@ -1632,10 +1624,57 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point
XCTAssertNodesHaveParent(parent, a, c, b);
//TODO: assert that things deallocate immediately and don't have latent autoreleases in here
[parent release];
[a release];
[b release];
[c release];
}
- (void)testRemoveFromViewBackedLoadedSupernode
{
DeclareNodeNamed(a);
DeclareNodeNamed(b);
[b addSubnode:a];
[a view];
[b view];
XCTAssertNodesLoaded(a, b);
XCTAssertEqual(a.supernode, b);
XCTAssertEqual(a.view.superview, b.view);
[a removeFromSupernode];
XCTAssertNil(a.supernode);
XCTAssertNil(a.view.superview);
}
- (void)testRemoveFromLayerBackedLoadedSupernode
{
DeclareNodeNamed(a);
a.layerBacked = YES;
DeclareNodeNamed(b);
b.layerBacked = YES;
[b addSubnode:a];
[a layer];
[b layer];
XCTAssertNodesLoaded(a, b);
XCTAssertEqual(a.supernode, b);
XCTAssertEqual(a.layer.superlayer, b.layer);
[a removeFromSupernode];
XCTAssertNil(a.supernode);
XCTAssertNil(a.layer.superlayer);
}
- (void)testRemoveLayerBackedFromViewBackedLoadedSupernode
{
DeclareNodeNamed(a);
a.layerBacked = YES;
DeclareNodeNamed(b);
[b addSubnode:a];
[a layer];
[b view];
XCTAssertNodesLoaded(a, b);
XCTAssertEqual(a.supernode, b);
XCTAssertEqual(a.layer.superlayer, b.layer);
[a removeFromSupernode];
XCTAssertNil(a.supernode);
XCTAssertNil(a.layer.superlayer);
}
- (void)testSubnodeAddedBeforeLoadingExternalView
@@ -1651,6 +1690,9 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point
child = [[ASDisplayNode alloc] init];
[parent addSubnode:child];
}];
// executeOffThread: blocks until the background thread finishes executing.
parent = [parent autorelease]; // XXX This is very bad style.
child = [child autorelease]; // XXX This is very bad style.
XCTAssertEqual(1, parent.subnodes.count, @"Parent should have 1 subnode");
XCTAssertEqualObjects(parent, child.supernode, @"Child has the wrong parent");
@@ -1663,14 +1705,14 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point
- (void)testSubnodeAddedAfterLoadingExternalView
{
UIView *view = [[UIDisplayNodeTestView alloc] init];
ASDisplayNode *parent = [[ASDisplayNode alloc] initWithViewBlock:^{
UIView *view = [[[UIDisplayNodeTestView alloc] init] autorelease];
ASDisplayNode *parent = [[[ASDisplayNode alloc] initWithViewBlock:^{
return view;
}];
}] autorelease];
[parent view];
ASDisplayNode *child = [[ASDisplayNode alloc] init];
ASDisplayNode *child = [[[ASDisplayNode alloc] init] autorelease];
[parent addSubnode:child];
XCTAssertEqual(1, parent.subnodes.count, @"Parent should have 1 subnode");
@@ -1800,7 +1842,7 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point
- (void)testInitWithViewClass
{
ASDisplayNode *scrollNode = [[ASDisplayNode alloc] initWithViewClass:[UIScrollView class]];
ASDisplayNode *scrollNode = [[[ASDisplayNode alloc] initWithViewClass:[UIScrollView class]] autorelease];
XCTAssertFalse(scrollNode.isLayerBacked, @"Can't be layer backed");
XCTAssertFalse(scrollNode.nodeLoaded, @"Shouldn't have a view yet");
@@ -1815,7 +1857,7 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point
- (void)testInitWithLayerClass
{
ASDisplayNode *transformNode = [[ASDisplayNode alloc] initWithLayerClass:[CATransformLayer class]];
ASDisplayNode *transformNode = [[[ASDisplayNode alloc] initWithLayerClass:[CATransformLayer class]] autorelease];
XCTAssertTrue(transformNode.isLayerBacked, @"Created with layer class => should be layer-backed by default");
XCTAssertFalse(transformNode.nodeLoaded, @"Shouldn't have a view yet");
@@ -1898,7 +1940,7 @@ static bool stringContainsPointer(NSString *description, const void *p) {
- (void)testBounds
{
ASDisplayNode *node = [[ASDisplayNode alloc] init];
ASDisplayNode *node = [[[ASDisplayNode alloc] init] autorelease];
node.bounds = CGRectMake(1, 2, 3, 4);
node.frame = CGRectMake(5, 6, 7, 8);
@@ -1910,7 +1952,7 @@ static bool stringContainsPointer(NSString *description, const void *p) {
- (void)testDidEnterDisplayIsCalledWhenNodesEnterDisplayRange
{
ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init];
ASTestDisplayNode *node = [[[ASTestDisplayNode alloc] init] autorelease];
[node recursivelySetInterfaceState:ASInterfaceStateDisplay];
@@ -1919,7 +1961,7 @@ static bool stringContainsPointer(NSString *description, const void *p) {
- (void)testDidExitDisplayIsCalledWhenNodesExitDisplayRange
{
ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init];
ASTestDisplayNode *node = [[[ASTestDisplayNode alloc] init] autorelease];
[node recursivelySetInterfaceState:ASInterfaceStateDisplay];
[node recursivelySetInterfaceState:ASInterfaceStateFetchData];
@@ -1929,7 +1971,7 @@ static bool stringContainsPointer(NSString *description, const void *p) {
- (void)testDidEnterFetchDataIsCalledWhenNodesEnterFetchDataRange
{
ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init];
ASTestDisplayNode *node = [[[ASTestDisplayNode alloc] init] autorelease];
[node recursivelySetInterfaceState:ASInterfaceStateFetchData];
@@ -1938,7 +1980,7 @@ static bool stringContainsPointer(NSString *description, const void *p) {
- (void)testDidExitFetchDataIsCalledWhenNodesExitFetchDataRange
{
ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init];
ASTestDisplayNode *node = [[[ASTestDisplayNode alloc] init] autorelease];
[node recursivelySetInterfaceState:ASInterfaceStateFetchData];
[node recursivelySetInterfaceState:ASInterfaceStateDisplay];

View File

@@ -90,8 +90,22 @@ static BOOL ASRunRunLoopUntilBlockIsTrue(BOOL (^block)())
{
[super setUp];
_mockCache = [OCMockObject mockForProtocol:@protocol(ASImageCacheProtocol)];
_mockDownloader = [OCMockObject mockForProtocol:@protocol(ASImageDownloaderProtocol)];
_mockCache = [[OCMockObject mockForProtocol:@protocol(ASImageCacheProtocol)] retain];
_mockDownloader = [[OCMockObject mockForProtocol:@protocol(ASImageDownloaderProtocol)] retain];
}
- (void)tearDown
{
if(_mockCache) {
[_mockCache release];
_mockCache = nil;
}
if(_mockDownloader) {
[_mockDownloader release];
_mockDownloader = nil;
}
[super tearDown];
}
- (void)testDataSourceImageMethod

View File

@@ -408,8 +408,7 @@
{
CGSize tableViewSize = CGSizeMake(100, 500);
ASTestTableView *tableView = [[ASTestTableView alloc] initWithFrame:CGRectMake(0, 0, tableViewSize.width, tableViewSize.height)
style:UITableViewStylePlain
asyncDataFetching:YES];
style:UITableViewStylePlain];
ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new];
tableView.asyncDelegate = dataSource;

View File

@@ -0,0 +1,562 @@
//
// ASTableViewThrashTests.m
// AsyncDisplayKit
//
// Created by Adlai Holler on 6/21/16.
// Copyright © 2016 Facebook. All rights reserved.
//
@import XCTest;
#import <AsyncDisplayKit/AsyncDisplayKit.h>
// Set to 1 to use UITableView and see if the issue still exists.
#define USE_UIKIT_REFERENCE 0
#if USE_UIKIT_REFERENCE
#define TableView UITableView
#define kCellReuseID @"ASThrashTestCellReuseID"
#else
#define TableView ASTableView
#endif
#define kInitialSectionCount 20
#define kInitialItemCount 20
#define kMinimumItemCount 5
#define kMinimumSectionCount 3
#define kFickleness 0.1
#define kThrashingIterationCount 100
static NSString *ASThrashArrayDescription(NSArray *array) {
NSMutableString *str = [NSMutableString stringWithString:@"(\n"];
NSInteger i = 0;
for (id obj in array) {
[str appendFormat:@"\t[%ld]: \"%@\",\n", i, obj];
i += 1;
}
[str appendString:@")"];
return str;
}
static volatile int32_t ASThrashTestItemNextID = 1;
@interface ASThrashTestItem: NSObject <NSSecureCoding>
@property (nonatomic, readonly) NSInteger itemID;
- (CGFloat)rowHeight;
@end
@implementation ASThrashTestItem
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)init {
self = [super init];
if (self != nil) {
_itemID = OSAtomicIncrement32(&ASThrashTestItemNextID);
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self != nil) {
_itemID = [aDecoder decodeIntegerForKey:@"itemID"];
NSAssert(_itemID > 0, @"Failed to decode %@", self);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeInteger:_itemID forKey:@"itemID"];
}
+ (NSMutableArray <ASThrashTestItem *> *)itemsWithCount:(NSInteger)count {
NSMutableArray *result = [NSMutableArray arrayWithCapacity:count];
for (NSInteger i = 0; i < count; i += 1) {
[result addObject:[[ASThrashTestItem alloc] init]];
}
return result;
}
- (CGFloat)rowHeight {
return (self.itemID % 400) ?: 44;
}
- (NSString *)description {
return [NSString stringWithFormat:@"<Item %lu>", (unsigned long)_itemID];
}
@end
@interface ASThrashTestSection: NSObject <NSCopying, NSSecureCoding>
@property (nonatomic, strong, readonly) NSMutableArray *items;
@property (nonatomic, readonly) NSInteger sectionID;
- (CGFloat)headerHeight;
@end
static volatile int32_t ASThrashTestSectionNextID = 1;
@implementation ASThrashTestSection
/// Create an array of sections with the given count
+ (NSMutableArray <ASThrashTestSection *> *)sectionsWithCount:(NSInteger)count {
NSMutableArray *result = [NSMutableArray arrayWithCapacity:count];
for (NSInteger i = 0; i < count; i += 1) {
[result addObject:[[ASThrashTestSection alloc] initWithCount:kInitialItemCount]];
}
return result;
}
- (instancetype)initWithCount:(NSInteger)count {
self = [super init];
if (self != nil) {
_sectionID = OSAtomicIncrement32(&ASThrashTestSectionNextID);
_items = [ASThrashTestItem itemsWithCount:count];
}
return self;
}
- (instancetype)init {
return [self initWithCount:0];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self != nil) {
_items = [aDecoder decodeObjectOfClass:[NSArray class] forKey:@"items"];
_sectionID = [aDecoder decodeIntegerForKey:@"sectionID"];
NSAssert(_sectionID > 0, @"Failed to decode %@", self);
}
return self;
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:_items forKey:@"items"];
[aCoder encodeInteger:_sectionID forKey:@"sectionID"];
}
- (CGFloat)headerHeight {
return self.sectionID % 400 ?: 44;
}
- (NSString *)description {
return [NSString stringWithFormat:@"<Section %lu: itemCount=%lu>", (unsigned long)_sectionID, (unsigned long)self.items.count];
}
- (id)copyWithZone:(NSZone *)zone {
ASThrashTestSection *copy = [[ASThrashTestSection alloc] init];
copy->_sectionID = _sectionID;
copy->_items = [_items mutableCopy];
return copy;
}
- (BOOL)isEqual:(id)object {
if ([object isKindOfClass:[ASThrashTestSection class]]) {
return [(ASThrashTestSection *)object sectionID] == _sectionID;
} else {
return NO;
}
}
@end
#if !USE_UIKIT_REFERENCE
@interface ASThrashTestNode: ASCellNode
@property (nonatomic, strong) ASThrashTestItem *item;
@end
@implementation ASThrashTestNode
@end
#endif
@interface ASThrashDataSource: NSObject
#if USE_UIKIT_REFERENCE
<UITableViewDataSource, UITableViewDelegate>
#else
<ASTableDataSource, ASTableDelegate>
#endif
@property (nonatomic, strong, readonly) UIWindow *window;
@property (nonatomic, strong, readonly) TableView *tableView;
@property (nonatomic, strong) NSArray <ASThrashTestSection *> *data;
@end
@implementation ASThrashDataSource
- (instancetype)initWithData:(NSArray <ASThrashTestSection *> *)data {
self = [super init];
if (self != nil) {
_data = [[NSArray alloc] initWithArray:data copyItems:YES];
_window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
_tableView = [[TableView alloc] initWithFrame:_window.bounds style:UITableViewStylePlain];
[_window addSubview:_tableView];
#if USE_UIKIT_REFERENCE
_tableView.dataSource = self;
_tableView.delegate = self;
[_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellReuseID];
#else
_tableView.asyncDelegate = self;
_tableView.asyncDataSource = self;
[_tableView reloadDataImmediately];
#endif
[_tableView layoutIfNeeded];
}
return self;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.data[section].items.count;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.data.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return self.data[section].headerHeight;
}
#if USE_UIKIT_REFERENCE
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
return [tableView dequeueReusableCellWithIdentifier:kCellReuseID forIndexPath:indexPath];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
ASThrashTestItem *item = self.data[indexPath.section].items[indexPath.item];
return item.rowHeight;
}
#else
- (ASCellNodeBlock)tableView:(ASTableView *)tableView nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath {
ASThrashTestItem *item = self.data[indexPath.section].items[indexPath.item];
return ^{
ASThrashTestNode *node = [[ASThrashTestNode alloc] init];
node.item = item;
return node;
};
}
#endif
@end
@implementation NSIndexSet (ASThrashHelpers)
- (NSArray <NSIndexPath *> *)indexPathsInSection:(NSInteger)section {
NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count];
[self enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {
[result addObject:[NSIndexPath indexPathForItem:idx inSection:section]];
}];
return result;
}
/// `insertMode` means that for each index selected, the max goes up by one.
+ (NSMutableIndexSet *)randomIndexesLessThan:(NSInteger)max probability:(float)probability insertMode:(BOOL)insertMode {
NSMutableIndexSet *indexes = [[NSMutableIndexSet alloc] init];
u_int32_t cutoff = probability * 100;
for (NSInteger i = 0; i < max; i++) {
if (arc4random_uniform(100) < cutoff) {
[indexes addIndex:i];
if (insertMode) {
max += 1;
}
}
}
return indexes;
}
@end
static NSInteger ASThrashUpdateCurrentSerializationVersion = 1;
@interface ASThrashUpdate : NSObject <NSSecureCoding>
@property (nonatomic, strong, readonly) NSArray<ASThrashTestSection *> *oldData;
@property (nonatomic, strong, readonly) NSMutableArray<ASThrashTestSection *> *data;
@property (nonatomic, strong, readonly) NSMutableIndexSet *deletedSectionIndexes;
@property (nonatomic, strong, readonly) NSMutableIndexSet *replacedSectionIndexes;
/// The sections used to replace the replaced sections.
@property (nonatomic, strong, readonly) NSMutableArray<ASThrashTestSection *> *replacingSections;
@property (nonatomic, strong, readonly) NSMutableIndexSet *insertedSectionIndexes;
@property (nonatomic, strong, readonly) NSMutableArray<ASThrashTestSection *> *insertedSections;
@property (nonatomic, strong, readonly) NSMutableArray<NSMutableIndexSet *> *deletedItemIndexes;
@property (nonatomic, strong, readonly) NSMutableArray<NSMutableIndexSet *> *replacedItemIndexes;
/// The items used to replace the replaced items.
@property (nonatomic, strong, readonly) NSMutableArray<NSArray <ASThrashTestItem *> *> *replacingItems;
@property (nonatomic, strong, readonly) NSMutableArray<NSMutableIndexSet *> *insertedItemIndexes;
@property (nonatomic, strong, readonly) NSMutableArray<NSArray <ASThrashTestItem *> *> *insertedItems;
- (instancetype)initWithData:(NSArray<ASThrashTestSection *> *)data;
+ (ASThrashUpdate *)thrashUpdateWithBase64String:(NSString *)base64;
- (NSString *)base64Representation;
@end
@implementation ASThrashUpdate
- (instancetype)initWithData:(NSArray<ASThrashTestSection *> *)data {
self = [super init];
if (self != nil) {
_data = [[NSMutableArray alloc] initWithArray:data copyItems:YES];
_oldData = [[NSArray alloc] initWithArray:data copyItems:YES];
_deletedItemIndexes = [NSMutableArray array];
_replacedItemIndexes = [NSMutableArray array];
_insertedItemIndexes = [NSMutableArray array];
_replacingItems = [NSMutableArray array];
_insertedItems = [NSMutableArray array];
// Randomly reload some items
for (ASThrashTestSection *section in _data) {
NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:section.items.count probability:kFickleness insertMode:NO];
NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count];
[section.items replaceObjectsAtIndexes:indexes withObjects:newItems];
[_replacingItems addObject:newItems];
[_replacedItemIndexes addObject:indexes];
}
// Randomly replace some sections
_replacedSectionIndexes = [NSIndexSet randomIndexesLessThan:_data.count probability:kFickleness insertMode:NO];
_replacingSections = [ASThrashTestSection sectionsWithCount:_replacedSectionIndexes.count];
[_data replaceObjectsAtIndexes:_replacedSectionIndexes withObjects:_replacingSections];
// Randomly delete some items
[_data enumerateObjectsUsingBlock:^(ASThrashTestSection * _Nonnull section, NSUInteger idx, BOOL * _Nonnull stop) {
if (section.items.count >= kMinimumItemCount) {
NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:section.items.count probability:kFickleness insertMode:NO];
/// Cannot reload & delete the same item.
[indexes removeIndexes:_replacedItemIndexes[idx]];
[section.items removeObjectsAtIndexes:indexes];
[_deletedItemIndexes addObject:indexes];
} else {
[_deletedItemIndexes addObject:[NSMutableIndexSet indexSet]];
}
}];
// Randomly delete some sections
if (_data.count >= kMinimumSectionCount) {
_deletedSectionIndexes = [NSIndexSet randomIndexesLessThan:_data.count probability:kFickleness insertMode:NO];
} else {
_deletedSectionIndexes = [NSMutableIndexSet indexSet];
}
// Cannot replace & delete the same section.
[_deletedSectionIndexes removeIndexes:_replacedSectionIndexes];
// Cannot delete/replace item in deleted/replaced section
[_deletedSectionIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {
[_replacedItemIndexes[idx] removeAllIndexes];
[_deletedItemIndexes[idx] removeAllIndexes];
}];
[_replacedSectionIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {
[_replacedItemIndexes[idx] removeAllIndexes];
[_deletedItemIndexes[idx] removeAllIndexes];
}];
[_data removeObjectsAtIndexes:_deletedSectionIndexes];
// Randomly insert some sections
_insertedSectionIndexes = [NSIndexSet randomIndexesLessThan:(_data.count + 1) probability:kFickleness insertMode:YES];
_insertedSections = [ASThrashTestSection sectionsWithCount:_insertedSectionIndexes.count];
[_data insertObjects:_insertedSections atIndexes:_insertedSectionIndexes];
// Randomly insert some items
for (ASThrashTestSection *section in _data) {
// Only insert items into the old sections not replaced/inserted sections.
if ([_oldData containsObject:section]) {
NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:(section.items.count + 1) probability:kFickleness insertMode:YES];
NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count];
[section.items insertObjects:newItems atIndexes:indexes];
[_insertedItems addObject:newItems];
[_insertedItemIndexes addObject:indexes];
} else {
[_insertedItems addObject:@[]];
[_insertedItemIndexes addObject:[NSMutableIndexSet indexSet]];
}
}
}
return self;
}
+ (BOOL)supportsSecureCoding {
return YES;
}
+ (ASThrashUpdate *)thrashUpdateWithBase64String:(NSString *)base64 {
return [NSKeyedUnarchiver unarchiveObjectWithData:[[NSData alloc] initWithBase64EncodedString:base64 options:kNilOptions]];
}
- (NSString *)base64Representation {
return [[NSKeyedArchiver archivedDataWithRootObject:self] base64EncodedStringWithOptions:kNilOptions];
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
NSDictionary *dict = [self dictionaryWithValuesForKeys:@[
@"oldData",
@"data",
@"deletedSectionIndexes",
@"replacedSectionIndexes",
@"replacingSections",
@"insertedSectionIndexes",
@"insertedSections",
@"deletedItemIndexes",
@"replacedItemIndexes",
@"replacingItems",
@"insertedItemIndexes",
@"insertedItems"
]];
[aCoder encodeObject:dict forKey:@"_dict"];
[aCoder encodeInteger:ASThrashUpdateCurrentSerializationVersion forKey:@"_version"];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self != nil) {
NSAssert(ASThrashUpdateCurrentSerializationVersion == [aDecoder decodeIntegerForKey:@"_version"], @"This thrash update was archived from a different version and can't be read. Sorry.");
NSDictionary *dict = [aDecoder decodeObjectOfClass:[NSDictionary class] forKey:@"_dict"];
[self setValuesForKeysWithDictionary:dict];
}
return self;
}
- (NSString *)description {
return [NSString stringWithFormat:@"<ASThrashUpdate %p:\nOld data: %@\nDeleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@>", self, ASThrashArrayDescription(_oldData), ASThrashArrayDescription(_deletedItemIndexes), _deletedSectionIndexes, ASThrashArrayDescription(_replacedItemIndexes), _replacedSectionIndexes, ASThrashArrayDescription(_insertedItemIndexes), _insertedSectionIndexes, ASThrashArrayDescription(_data)];
}
- (NSString *)logFriendlyBase64Representation {
return [NSString stringWithFormat:@"\n\n**********\nBase64 Representation:\n**********\n%@\n**********\nEnd Base64 Representation\n**********", self.base64Representation];
}
@end
@interface ASTableViewThrashTests: XCTestCase
@end
@implementation ASTableViewThrashTests {
// The current update, which will be logged in case of a failure.
ASThrashUpdate *_update;
}
#pragma mark Overrides
- (void)tearDown {
_update = nil;
}
// NOTE: Despite the documentation, this is not always called if an exception is caught.
- (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *)filePath atLine:(NSUInteger)lineNumber expected:(BOOL)expected {
[self logCurrentUpdateIfNeeded];
[super recordFailureWithDescription:description inFile:filePath atLine:lineNumber expected:expected];
}
#pragma mark Test Methods
- (void)testInitialDataRead {
ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]];
[self verifyDataSource:ds];
}
/// Replays the Base64 representation of an ASThrashUpdate from "ASThrashTestRecordedCase" file
- (void)DISABLED_testRecordedThrashCase {
NSURL *caseURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"ASThrashTestRecordedCase" withExtension:nil subdirectory:@"TestResources"];
NSString *base64 = [NSString stringWithContentsOfURL:caseURL encoding:NSUTF8StringEncoding error:NULL];
_update = [ASThrashUpdate thrashUpdateWithBase64String:base64];
if (_update == nil) {
return;
}
ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:_update.oldData];
[self applyUpdate:_update toDataSource:ds];
[self verifyDataSource:ds];
}
- (void)DISABLED_testThrashingWildly {
for (NSInteger i = 0; i < kThrashingIterationCount; i++) {
[self setUp];
ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]];
_update = [[ASThrashUpdate alloc] initWithData:ds.data];
[self applyUpdate:_update toDataSource:ds];
[self verifyDataSource:ds];
[self tearDown];
}
}
#pragma mark Helpers
- (void)logCurrentUpdateIfNeeded {
if (_update != nil) {
NSLog(@"Failed update %@: %@", _update, _update.logFriendlyBase64Representation);
}
}
- (void)applyUpdate:(ASThrashUpdate *)update toDataSource:(ASThrashDataSource *)dataSource {
TableView *tableView = dataSource.tableView;
[tableView beginUpdates];
dataSource.data = update.data;
[tableView insertSections:update.insertedSectionIndexes withRowAnimation:UITableViewRowAnimationNone];
[tableView deleteSections:update.deletedSectionIndexes withRowAnimation:UITableViewRowAnimationNone];
[tableView reloadSections:update.replacedSectionIndexes withRowAnimation:UITableViewRowAnimationNone];
[update.insertedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) {
NSArray *indexPaths = [indexes indexPathsInSection:idx];
[tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
}];
[update.deletedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) {
NSArray *indexPaths = [indexes indexPathsInSection:sec];
[tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
}];
[update.replacedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) {
NSArray *indexPaths = [indexes indexPathsInSection:sec];
[tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
}];
@try {
[tableView endUpdates];
#if !USE_UIKIT_REFERENCE
[tableView waitUntilAllUpdatesAreCommitted];
#endif
} @catch (NSException *exception) {
[self logCurrentUpdateIfNeeded];
@throw exception;
}
}
- (void)verifyDataSource:(ASThrashDataSource *)ds {
TableView *tableView = ds.tableView;
NSArray <ASThrashTestSection *> *data = [ds data];
XCTAssertEqual(data.count, tableView.numberOfSections);
for (NSInteger i = 0; i < tableView.numberOfSections; i++) {
XCTAssertEqual([tableView numberOfRowsInSection:i], data[i].items.count);
XCTAssertEqual([tableView rectForHeaderInSection:i].size.height, data[i].headerHeight);
for (NSInteger j = 0; j < [tableView numberOfRowsInSection:i]; j++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];
ASThrashTestItem *item = data[i].items[j];
#if USE_UIKIT_REFERENCE
XCTAssertEqual([tableView rectForRowAtIndexPath:indexPath].size.height, item.rowHeight);
#else
ASThrashTestNode *node = (ASThrashTestNode *)[tableView nodeForRowAtIndexPath:indexPath];
XCTAssertEqual(node.item, item);
#endif
}
}
}
@end

View File

@@ -15,7 +15,7 @@
#import <AVFoundation/AVFoundation.h>
#import <AsyncDisplayKit/AsyncDisplayKit.h>
@interface ASVideoNodeTests : XCTestCase
@interface ASVideoNodeTests : XCTestCase <ASVideoNodeDelegate>
{
ASVideoNode *_videoNode;
AVURLAsset *_firstAsset;
@@ -25,10 +25,18 @@
}
@end
@interface ASNetworkImageNode () {
@public __weak id<ASNetworkImageNodeDelegate> _delegate;
}
@end
@interface ASVideoNode () {
ASDisplayNode *_playerNode;
AVPlayer *_player;
}
@property (atomic, readwrite) ASInterfaceState interfaceState;
@property (atomic, readonly) ASDisplayNode *spinner;
@property (atomic, readwrite) ASDisplayNode *playerNode;
@@ -405,4 +413,18 @@
XCTAssertNil(_videoNode.image);
}
- (void)testDelegateProperlySetForClassHierarchy
{
_videoNode.delegate = self;
XCTAssertTrue([_videoNode.delegate conformsToProtocol:@protocol(ASVideoNodeDelegate)]);
XCTAssertTrue([_videoNode.delegate conformsToProtocol:@protocol(ASNetworkImageNodeDelegate)]);
XCTAssertTrue([((ASNetworkImageNode*)_videoNode).delegate conformsToProtocol:@protocol(ASNetworkImageNodeDelegate)]);
XCTAssertTrue([((ASNetworkImageNode*)_videoNode)->_delegate conformsToProtocol:@protocol(ASNetworkImageNodeDelegate)]);
XCTAssertEqual(_videoNode.delegate, self);
XCTAssertEqual(((ASNetworkImageNode*)_videoNode).delegate, self);
XCTAssertEqual(((ASNetworkImageNode*)_videoNode)->_delegate, self);
}
@end

File diff suppressed because one or more lines are too long

1
Cartfile Normal file
View File

@@ -0,0 +1 @@
github "pinterest/PINRemoteImage" "3.0.0-beta.2"

View File

@@ -36,21 +36,102 @@ if [ "$MODE" = "tests" ]; then
exit 0
fi
if [ "$MODE" = "examples" ]; then
if [ "$MODE" = "examples-pt1" ]; then
echo "Verifying that all AsyncDisplayKit examples compile."
for example in examples/*/; do
echo "Building $example."
for example in $((find ./examples -type d -maxdepth 1 \( ! -iname ".*" \)) | head -6 | head); do
echo "Building (examples-pt1) $example."
if [ -f "${example}/Podfile" ]; then
echo "Using CocoaPods"
pod install --project-directory=$example
set -o pipefail && xcodebuild \
-workspace "${example}Sample.xcworkspace" \
-workspace "${example}/Sample.xcworkspace" \
-scheme Sample \
-sdk "$SDK" \
-destination "$PLATFORM" \
-derivedDataPath ~/ \
build | xcpretty $FORMATTER
elif [ -f "${example}/Cartfile" ]; then
echo "Using Carthage"
local_repo=`pwd`
current_branch=`git rev-parse --abbrev-ref HEAD`
cd $example
echo "git \"file://${local_repo}\" \"${current_branch}\"" > "Cartfile"
carthage update --platform iOS
set -o pipefail && xcodebuild \
-project "Sample.xcodeproj" \
-scheme Sample \
-sdk "$SDK" \
-destination "$PLATFORM" \
build | xcpretty $FORMATTER
cd ../..
fi
done
trap - EXIT
exit 0
fi
if [ "$MODE" = "examples-pt2" ]; then
echo "Verifying that all AsyncDisplayKit examples compile."
for example in $((find ./examples -type d -maxdepth 1 \( ! -iname ".*" \)) | head -12 | tail -6 | head); do
echo "Building $example (examples-pt2)."
if [ -f "${example}/Podfile" ]; then
echo "Using CocoaPods"
pod install --project-directory=$example
set -o pipefail && xcodebuild \
-workspace "${example}/Sample.xcworkspace" \
-scheme Sample \
-sdk "$SDK" \
-destination "$PLATFORM" \
-derivedDataPath ~/ \
build | xcpretty $FORMATTER
elif [ -f "${example}/Cartfile" ]; then
echo "Using Carthage"
local_repo=`pwd`
current_branch=`git rev-parse --abbrev-ref HEAD`
cd $example
echo "git \"file://${local_repo}\" \"${current_branch}\"" > "Cartfile"
carthage update --platform iOS
set -o pipefail && xcodebuild \
-project "Sample.xcodeproj" \
-scheme Sample \
-sdk "$SDK" \
-destination "$PLATFORM" \
build | xcpretty $FORMATTER
cd ../..
fi
done
trap - EXIT
exit 0
fi
if [ "$MODE" = "examples-pt3" ]; then
echo "Verifying that all AsyncDisplayKit examples compile."
for example in $((find ./examples -type d -maxdepth 1 \( ! -iname ".*" \)) | head -7 | head); do
echo "Building $example (examples-pt3)."
if [ -f "${example}/Podfile" ]; then
echo "Using CocoaPods"
pod install --project-directory=$example
set -o pipefail && xcodebuild \
-workspace "${example}/Sample.xcworkspace" \
-scheme Sample \
-sdk "$SDK" \
-destination "$PLATFORM" \
-derivedDataPath ~/ \
build | xcpretty $FORMATTER
elif [ -f "${example}/Cartfile" ]; then
echo "Using Carthage"

View File

@@ -1 +0,0 @@
git "file:///Users/scottg/code/AsyncDisplayKit" "master"

View File

@@ -0,0 +1 @@
github "facebook/AsyncDisplayKit" "master"

View File

@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="8150" systemVersion="15A204g" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8122"/>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--View Controller-->
@@ -15,7 +16,6 @@
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<animations/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>

View File

@@ -1,13 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6211" systemVersion="14A298i" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6204"/>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="" sceneMemberID="viewController">
<viewController id="BYZ-38-t0r" customClass="ViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>

View File

@@ -14,6 +14,8 @@
871BB3591C7C98B1005CF62A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 871BB3581C7C98B1005CF62A /* Assets.xcassets */; };
871BB35C1C7C98B1005CF62A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 871BB35A1C7C98B1005CF62A /* LaunchScreen.storyboard */; };
871BB3651C7C99B0005CF62A /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 871BB3641C7C99B0005CF62A /* AsyncDisplayKit.framework */; };
DEAE185D1D1A504A0083FAD0 /* PINCache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DEAE185B1D1A504A0083FAD0 /* PINCache.framework */; };
DEAE185E1D1A504A0083FAD0 /* PINRemoteImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DEAE185C1D1A504A0083FAD0 /* PINRemoteImage.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -28,6 +30,8 @@
871BB35B1C7C98B1005CF62A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
871BB35D1C7C98B1005CF62A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
871BB3641C7C99B0005CF62A /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AsyncDisplayKit.framework; path = Carthage/Build/iOS/AsyncDisplayKit.framework; sourceTree = SOURCE_ROOT; };
DEAE185B1D1A504A0083FAD0 /* PINCache.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PINCache.framework; path = ../Carthage/Build/iOS/PINCache.framework; sourceTree = "<group>"; };
DEAE185C1D1A504A0083FAD0 /* PINRemoteImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PINRemoteImage.framework; path = ../Carthage/Build/iOS/PINRemoteImage.framework; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -36,6 +40,8 @@
buildActionMask = 2147483647;
files = (
871BB3651C7C99B0005CF62A /* AsyncDisplayKit.framework in Frameworks */,
DEAE185D1D1A504A0083FAD0 /* PINCache.framework in Frameworks */,
DEAE185E1D1A504A0083FAD0 /* PINRemoteImage.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -87,6 +93,8 @@
isa = PBXGroup;
children = (
871BB3641C7C99B0005CF62A /* AsyncDisplayKit.framework */,
DEAE185B1D1A504A0083FAD0 /* PINCache.framework */,
DEAE185C1D1A504A0083FAD0 /* PINRemoteImage.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -126,7 +134,7 @@
};
};
};
buildConfigurationList = 871BB3441C7C98B1005CF62A /* Build configuration list for PBXProject "CarthageExample" */;
buildConfigurationList = 871BB3441C7C98B1005CF62A /* Build configuration list for PBXProject "Sample" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
@@ -325,7 +333,7 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
871BB3441C7C98B1005CF62A /* Build configuration list for PBXProject "CarthageExample" */ = {
871BB3441C7C98B1005CF62A /* Build configuration list for PBXProject "Sample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
871BB35E1C7C98B1005CF62A /* Debug */,