Merge branch 'master' into update-objc

Conflicts:
	AsyncDisplayKit/ASDisplayNode+Subclasses.h
	AsyncDisplayKit/ASMultiplexImageNode.h
	AsyncDisplayKit/ASViewController.h
	AsyncDisplayKit/Details/ASDataController.h
This commit is contained in:
Adlai Holler
2015-10-05 13:24:16 -07:00
68 changed files with 2337 additions and 159 deletions

View File

@@ -15,6 +15,29 @@ NS_ASSUME_NONNULL_BEGIN
*/
@interface ASCellNode : ASDisplayNode
/**
* @abstract When enabled, ensures that the cell is completely displayed before allowed onscreen.
*
* @default NO
* @discussion Normally, ASCellNodes are preloaded and have finished display before they are onscreen.
* However, if the Table or Collection's rangeTuningParameters are set to small values (or 0),
* or if the user is scrolling rapidly on a slow device, it is possible for a cell's display to
* be incomplete when it becomes visible.
*
* In this case, normally placeholder states are shown and scrolling continues uninterrupted.
* The finished, drawn content is then shown as soon as it is ready.
*
* With this property set to YES, the main thread will be blocked until display is complete for
* the cell. This is more similar to UIKit, and in fact makes AsyncDisplayKit scrolling visually
* indistinguishible from UIKit's, except being faster.
*
* Using this option does not eliminate all of the performance advantages of AsyncDisplayKit.
* Normally, a cell has been preloading and is almost done when it reaches the screen, so the
* blocking time is very short. If the rangeTuningParameters are set to 0, still this option
* outperforms UIKit: while the main thread is waiting, subnode display executes concurrently.
*/
@property (nonatomic, assign) BOOL neverShowPlaceholders;
/*
* ASTableView uses these properties when configuring UITableViewCells that host ASCellNodes.
*/

View File

@@ -213,6 +213,15 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (ASCellNode *)nodeForItemAtIndexPath:(NSIndexPath *)indexPath;
/**
* Similar to -indexPathForCell:.
*
* @param cellNode a cellNode part of the table view
*
* @returns an indexPath for this cellNode
*/
- (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode;
/**
* Similar to -visibleCells.
*

View File

@@ -17,6 +17,9 @@
#import "UICollectionViewLayout+ASConvenience.h"
#import "ASInternalHelpers.h"
// FIXME: Temporary nonsense import until method names are finalized and exposed
#import "ASDisplayNode+Subclasses.h"
const static NSUInteger kASCollectionViewAnimationNone = UITableViewRowAnimationNone;
@@ -234,7 +237,8 @@ static BOOL _isInterceptedSelector(SEL sel)
- (void)setDataSource:(id<UICollectionViewDataSource>)dataSource
{
ASDisplayNodeAssert(NO, @"ASCollectionView uses asyncDataSource, not UICollectionView's dataSource property.");
// UIKit can internally generate a call to this method upon changing the asyncDataSource; only assert for non-nil.
ASDisplayNodeAssert(dataSource == nil, @"ASCollectionView uses asyncDataSource, not UICollectionView's dataSource property.");
}
- (void)setDelegate:(id<UICollectionViewDelegate>)delegate
@@ -314,6 +318,16 @@ static BOOL _isInterceptedSelector(SEL sel)
return [[_dataController nodeAtIndexPath:indexPath] calculatedSize];
}
- (ASCellNode *)nodeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return [_dataController nodeAtIndexPath:indexPath];
}
- (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode
{
return [_dataController indexPathForNode:cellNode];
}
- (NSArray *)visibleNodes
{
NSArray *indexPaths = [self indexPathsForVisibleItems];
@@ -391,11 +405,6 @@ static BOOL _isInterceptedSelector(SEL sel)
[_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:kASCollectionViewAnimationNone];
}
- (ASCellNode *)nodeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return [_dataController nodeAtIndexPath:indexPath];
}
#pragma mark -
#pragma mark Intercepted selectors.
@@ -489,6 +498,11 @@ static BOOL _isInterceptedSelector(SEL sel)
if ([_asyncDelegate respondsToSelector:@selector(collectionView:willDisplayNodeForItemAtIndexPath:)]) {
[_asyncDelegate collectionView:self willDisplayNodeForItemAtIndexPath:indexPath];
}
ASCellNode *cellNode = [self nodeForItemAtIndexPath:indexPath];
if (cellNode.neverShowPlaceholders) {
[cellNode recursivelyEnsureDisplay];
}
}
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath

View File

@@ -431,9 +431,41 @@ NS_ASSUME_NONNULL_BEGIN
// This method has proven helpful in a few rare scenarios, similar to a category extension on UIView,
// but it's considered private API for now and its use should not be encouraged.
- (nullable ASDisplayNode *)_supernodeWithClass:(Class)supernodeClass;
// The two methods below will eventually be exposed, but their names are subject to change.
/**
* @abstract Ensure that all rendering is complete for this node and its descendents.
*
* @discussion Calling this method on the main thread after a node is added to the view heirarchy will ensure that
* placeholder states are never visible to the user. It is used by ASTableView, ASCollectionView, and ASViewController
* to implement their respective ".neverShowPlaceholders" option.
*
* If all nodes have layer.contents set and/or their layer does not have -needsDisplay set, the method will return immediately.
*
* This method is capable of handling a mixed set of nodes, with some not having started display, some in progress on an
* asynchronous display operation, and some already finished.
*
* In order to guarantee against deadlocks, this method should only be called on the main thread.
* It may block on the private queue, [_ASDisplayLayer displayQueue]
*/
- (void)recursivelyEnsureDisplay;
/**
* @abstract Allows a node to bypass all ensureDisplay passes. Defaults to NO.
*
* @discussion Nodes that are expensive to draw and expected to have placeholder even with
* .neverShowPlaceholders enabled should set this to YES.
*
* ASImageNode uses the default of NO, as it is often used for UI images that are expected to synchronize with ensureDisplay.
*
* ASNetworkImageNode and ASMultiplexImageNode set this to YES, because they load data from a database or server,
* and are expected to support a placeholder state given that display is often blocked on slow data fetching.
*/
@property (nonatomic, assign) BOOL shouldBypassEnsureDisplay;
@end
#define ASDisplayNodeAssertThreadAffinity(viewNode) ASDisplayNodeAssert(!viewNode || ASDisplayNodeThreadIsMain() || !(viewNode).nodeLoaded, @"Incorrect display node thread affinity")
#define ASDisplayNodeCAssertThreadAffinity(viewNode) ASDisplayNodeCAssert(!viewNode || ASDisplayNodeThreadIsMain() || !(viewNode).nodeLoaded, @"Incorrect display node thread affinity")
#define ASDisplayNodeAssertThreadAffinity(viewNode) ASDisplayNodeAssert(!viewNode || ASDisplayNodeThreadIsMain() || !(viewNode).nodeLoaded, @"Incorrect display node thread affinity - this method should not be called off the main thread after the ASDisplayNode's view or layer have been created")
#define ASDisplayNodeCAssertThreadAffinity(viewNode) ASDisplayNodeCAssert(!viewNode || ASDisplayNodeThreadIsMain() || !(viewNode).nodeLoaded, @"Incorrect display node thread affinity - this method should not be called off the main thread after the ASDisplayNode's view or layer have been created")
NS_ASSUME_NONNULL_END

View File

@@ -14,6 +14,7 @@
#import <objc/runtime.h>
#import "_ASAsyncTransaction.h"
#import "_ASAsyncTransactionContainer+Private.h"
#import "_ASPendingState.h"
#import "_ASDisplayView.h"
#import "_ASScopeTimer.h"
@@ -671,25 +672,20 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
// This is the root node. Trigger a full measurement pass on *current* thread. Old constrained size is re-used.
[self measureWithSizeRange:oldConstrainedSize];
CGSize oldSize = self.bounds.size;
CGRect oldBounds = self.bounds;
CGSize oldSize = oldBounds.size;
CGSize newSize = _layout.size;
if (! CGSizeEqualToSize(oldSize, newSize)) {
CGRect bounds = self.bounds;
bounds.size = newSize;
self.bounds = bounds;
self.bounds = (CGRect){ oldBounds.origin, newSize };
// Frame's origin must be preserved. Since it is computed from bounds size, anchorPoint
// and position (see frame setter in ASDisplayNode+UIViewBridge), position needs to be adjusted.
BOOL useLayer = (_layer && ASDisplayNodeThreadIsMain());
CGPoint anchorPoint = (useLayer ? _layer.anchorPoint : self.anchorPoint);
CGPoint oldPosition = (useLayer ? _layer.position : self.position);
CGPoint anchorPoint = self.anchorPoint;
CGPoint oldPosition = self.position;
CGFloat xDelta = (newSize.width - oldSize.width) * anchorPoint.x;
CGFloat yDelta = (newSize.height - oldSize.height) * anchorPoint.y;
CGPoint newPosition = CGPointMake(oldPosition.x + xDelta, oldPosition.y + yDelta);
useLayer ? _layer.position = newPosition : self.position = newPosition;
self.position = CGPointMake(oldPosition.x + xDelta, oldPosition.y + yDelta);
}
}
}
@@ -1415,6 +1411,68 @@ static NSInteger incrementIfFound(NSInteger i) {
[_placeholderLayer removeFromSuperlayer];
}
void recursivelyEnsureDisplayForLayer(CALayer *layer)
{
// This recursion must handle layers in various states:
// 1. Just added to hierarchy, CA hasn't yet called -display
// 2. Previously in a hierarchy (such as a working window owned by an Intelligent Preloading class, like ASTableView / ASCollectionView / ASViewController)
// 3. Has no content to display at all
// Specifically for case 1), we need to explicitly trigger a -display call now.
// Otherwise, there is no opportunity to block the main thread after CoreAnimation's transaction commit
// (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 (!layer.contents && [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.
[layer displayIfNeeded];
}
// Kick off the recursion first, so that all necessary display calls are sent and the displayQueue is full of parallelizable work.
for (CALayer *sublayer in layer.sublayers) {
recursivelyEnsureDisplayForLayer(sublayer);
}
// As the recursion unwinds, verify each transaction is complete and block if it is not.
// While blocking on one transaction, others may be completing concurrently, so it doesn't matter which blocks first.
BOOL waitUntilComplete = (!node.shouldBypassEnsureDisplay);
if (waitUntilComplete) {
for (_ASAsyncTransaction *transaction in [layer.asyncdisplaykit_asyncLayerTransactions copy]) {
// Even if none of the layers have had a chance to start display earlier, they will still be allowed to saturate a multicore CPU while blocking main.
// This significantly reduces time on the main thread relative to UIKit.
[transaction waitUntilComplete];
}
}
}
- (void)recursivelyEnsureDisplay
{
ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(self.isNodeLoaded, @"Node must have layer or view loaded to use -recursivelyEnsureDisplay");
ASDisplayNodeAssert(self.inHierarchy && (self.isLayerBacked || self.view.window != nil), @"Node must be in a hierarchy to use -recursivelyEnsureDisplay");
CALayer *layer = self.layer;
// -layoutIfNeeded is recursive, and even walks up to superlayers to check if they need layout,
// so we should call it outside of starting the recursion below. If our own layer is not marked
// as dirty, we can assume layout has run on this subtree before.
if ([layer needsLayout]) {
[layer layoutIfNeeded];
}
recursivelyEnsureDisplayForLayer(layer);
}
- (void)setShouldBypassEnsureDisplay:(BOOL)shouldBypassEnsureDisplay
{
ASDN::MutexLocker l(_propertyLock);
_flags.shouldBypassEnsureDisplay = shouldBypassEnsureDisplay;
}
- (BOOL)shouldBypassEnsureDisplay
{
ASDN::MutexLocker l(_propertyLock);
return _flags.shouldBypassEnsureDisplay;
}
#pragma mark - For Subclasses
- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
@@ -1484,6 +1542,7 @@ static NSInteger incrementIfFound(NSInteger i) {
ASDN::MutexLocker l(_propertyLock);
return _preferredFrameSize;
}
- (UIImage *)placeholderImage
{
return nil;

View File

@@ -12,7 +12,10 @@ NS_ASSUME_NONNULL_BEGIN
@protocol ASEditableTextNodeDelegate;
/// @abstract ASEditableTextNode implements a node that supports text editing.
/**
@abstract Implements a node that supports text editing.
@discussion Does not support layer backing.
*/
@interface ASEditableTextNode : ASDisplayNode
// @abstract The text node's delegate, which must conform to the <ASEditableTextNodeDelegate> protocol.

View File

@@ -143,6 +143,7 @@
_textKitComponents.textView.accessibilityHint = _placeholderTextKitComponents.textStorage.string;
configureTextView(_textKitComponents.textView);
[self.view addSubview:_textKitComponents.textView];
[self _updateDisplayingPlaceholder];
}
- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize
@@ -188,6 +189,12 @@
_placeholderTextKitComponents.textView.opaque = opaque;
}
- (void)setLayerBacked:(BOOL)layerBacked
{
ASDisplayNodeAssert(!layerBacked, @"Cannot set layerBacked to YES on ASEditableTextNode instances must be view-backed in order to ensure touch events can be passed to the internal UITextView during editing.");
[super setLayerBacked:layerBacked];
}
#pragma mark - Configuration
@synthesize delegate = _delegate;

View File

@@ -9,6 +9,8 @@
#import <AsyncDisplayKit/ASImageNode.h>
#import <AsyncDisplayKit/ASImageProtocols.h>
#import <Photos/Photos.h>
NS_ASSUME_NONNULL_BEGIN
@protocol ASMultiplexImageNodeDelegate;
@@ -100,6 +102,13 @@ typedef NS_ENUM(NSUInteger, ASMultiplexImageNodeErrorCode) {
*/
@property (nullable, nonatomic, readonly) ASImageIdentifier displayedImageIdentifier;
/**
* @abstract The image manager that this image node should use when requesting images from the Photos framework. If this is `nil` (the default), then `PHImageManager.defaultManager` is used.
* @see `+[NSURL URLWithAssetLocalIdentifier:targetSize:contentMode:options:]` below.
*/
@property (nonatomic, strong) PHImageManager *imageManager;
@end
@@ -197,13 +206,34 @@ didFinishDownloadingImageWithIdentifier:(ASImageIdentifier)imageIdentifier
* @abstract An image URL for the specified identifier.
* @param imageNode The sender.
* @param imageIdentifier The identifier for the image that will be downloaded.
* @discussion Supported URLs include assets-library, Photo framework URLs (ph://), HTTP, HTTPS, and FTP URLs. If the
* image is already available to the data source, it should be provided via <[ASMultiplexImageNodeDataSource
* @discussion Supported URLs include HTTP, HTTPS, AssetsLibrary, and FTP URLs as well as Photos framework URLs (see note).
*
* If the image is already available to the data source, it should be provided via <[ASMultiplexImageNodeDataSource
* multiplexImageNode:imageForImageIdentifier:]> instead.
* @returns An NSURL for the image identified by `imageIdentifier`, or nil if none is available.
* @return An NSURL for the image identified by `imageIdentifier`, or nil if none is available.
* @see `+[NSURL URLWithAssetLocalIdentifier:targetSize:contentMode:options:]` below.
*/
- (nullable NSURL *)multiplexImageNode:(ASMultiplexImageNode *)imageNode URLForImageIdentifier:(ASImageIdentifier)imageIdentifier;
@end
NS_ASSUME_NONNULL_END
#pragma mark -
@interface NSURL (ASPhotosFrameworkURLs)
/**
* @abstract Create an NSURL that specifies an image from the Photos framework.
*
* @discussion When implementing `-multiplexImageNode:URLForImageIdentifier:`, you can return a URL
* created by this method and the image node will attempt to load the image from the Photos framework.
* @note The `synchronous` flag in `options` is ignored.
* @note The `Opportunistic` delivery mode is not supported and will be treated as `HighQualityFormat`.
*/
+ (NSURL *)URLWithAssetLocalIdentifier:(NSString *)assetLocalIdentifier
targetSize:(CGSize)targetSize
contentMode:(PHImageContentMode)contentMode
options:(PHImageRequestOptions *)options;
@end
NS_ASSUME_NONNULL_END

View File

@@ -18,6 +18,7 @@
#import "ASBaseDefines.h"
#import "ASDisplayNode+Subclasses.h"
#import "ASLog.h"
#import "ASPhotosFrameworkImageRequest.h"
#if !AS_IOS8_SDK_OR_LATER
#error ASMultiplexImageNode can be used on iOS 7, but must be linked against the iOS 8 SDK.
@@ -26,8 +27,6 @@
NSString *const ASMultiplexImageNodeErrorDomain = @"ASMultiplexImageNodeErrorDomain";
static NSString *const kAssetsLibraryURLScheme = @"assets-library";
static NSString *const kPHAssetURLScheme = @"ph";
static NSString *const kPHAssetURLPrefix = @"ph://";
/**
@abstract Signature for the block to be performed after an image has loaded.
@@ -120,14 +119,14 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent
- (void)_loadALAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL completion:(void (^)(UIImage *image, NSError *error))completionBlock;
/**
@abstract Loads the image corresponding to the given assetURL from the Photos framework.
@abstract Loads the image corresponding to the given image request from the Photos framework.
@param imageIdentifier The identifier for the image to be loaded. May not be nil.
@param assetURL The photos framework URL (e.g., "ph://identifier") of the image to load, from PHAsset. May not be nil.
@param request The photos image request to load. May not be nil.
@param completionBlock The block to be performed when the image has been loaded, if possible. May not be nil.
@param image The image that was loaded. May be nil if no image could be downloaded.
@param error An error describing why the load failed, if it failed; nil otherwise.
*/
- (void)_loadPHAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL completion:(void (^)(UIImage *image, NSError *error))completionBlock;
- (void)_loadPHAssetWithRequest:(ASPhotosFrameworkImageRequest *)request identifier:(id)imageIdentifier completion:(void (^)(UIImage *image, NSError *error))completionBlock;
/**
@abstract Downloads the image corresponding to the given imageIdentifier from the given URL.
@@ -158,6 +157,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent
_cache = cache;
_downloader = downloader;
self.shouldBypassEnsureDisplay = YES;
return self;
}
@@ -456,8 +456,8 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent
}];
}
// Likewise, if it's a iOS 8 Photo asset, we need to fetch it accordingly.
else if (AS_AT_LEAST_IOS8 && [[nextImageURL scheme] isEqualToString:kPHAssetURLScheme]) {
[self _loadPHAssetWithIdentifier:nextImageIdentifier URL:nextImageURL completion:^(UIImage *image, NSError *error) {
else if (ASPhotosFrameworkImageRequest *request = [ASPhotosFrameworkImageRequest requestWithURL:nextImageURL]) {
[self _loadPHAssetWithRequest:request identifier:nextImageIdentifier completion:^(UIImage *image, NSError *error) {
ASMultiplexImageNodeCLogDebug(@"[%p] Acquired next image (%@) from Photos Framework", weakSelf, nextImageIdentifier);
finishedLoadingBlock(image, nextImageIdentifier, error);
}];
@@ -511,38 +511,48 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent
}];
}
- (void)_loadPHAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL completion:(void (^)(UIImage *image, NSError *error))completionBlock
- (void)_loadPHAssetWithRequest:(ASPhotosFrameworkImageRequest *)request identifier:(id)imageIdentifier completion:(void (^)(UIImage *image, NSError *error))completionBlock
{
ASDisplayNodeAssert(AS_AT_LEAST_IOS8, @"PhotosKit is unavailable on iOS 7.");
ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required");
ASDisplayNodeAssertNotNil(assetURL, @"assetURL is required");
ASDisplayNodeAssertNotNil(request, @"request is required");
ASDisplayNodeAssertNotNil(completionBlock, @"completionBlock is required");
// Get the PHAsset itself.
ASDisplayNodeAssertTrue([[assetURL absoluteString] hasPrefix:kPHAssetURLPrefix]);
NSString *assetIdentifier = [[assetURL absoluteString] substringFromIndex:[kPHAssetURLPrefix length]];
PHFetchResult *assetFetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetIdentifier] options:nil];
if ([assetFetchResult count] == 0) {
// Error.
completionBlock(nil, nil);
return;
}
// Get the best image we can.
PHAsset *imageAsset = [assetFetchResult firstObject];
PHImageRequestOptions *requestOptions = [[PHImageRequestOptions alloc] init];
requestOptions.version = PHImageRequestOptionsVersionCurrent;
requestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
requestOptions.resizeMode = PHImageRequestOptionsResizeModeNone;
[[PHImageManager defaultManager] requestImageForAsset:imageAsset
targetSize:CGSizeMake(2048.0, 2048.0) // Ideally we would use PHImageManagerMaximumSize and kill the options, but we get back nil when requesting images of video assets. rdar://18447788
contentMode:PHImageContentModeDefault
options:requestOptions
resultHandler:^(UIImage *image, NSDictionary *info) {
completionBlock(image, info[PHImageErrorKey]);
}];
// This is sometimes called on main but there's no reason to stay there
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
// Get the PHAsset itself.
PHFetchResult *assetFetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[request.assetIdentifier] options:nil];
if ([assetFetchResult count] == 0) {
// Error.
completionBlock(nil, nil);
return;
}
PHAsset *imageAsset = [assetFetchResult firstObject];
PHImageRequestOptions *options = [request.options copy];
// We don't support opportunistic delivery one request, one image.
if (options.deliveryMode == PHImageRequestOptionsDeliveryModeOpportunistic) {
options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
}
if (options.deliveryMode == PHImageRequestOptionsDeliveryModeHighQualityFormat) {
// Without this flag the result will be delivered on the main queue, which is pointless
// But synchronous -> HighQualityFormat so we only use it if high quality format is specified
options.synchronous = YES;
}
PHImageManager *imageManager = self.imageManager ?: PHImageManager.defaultManager;
[imageManager requestImageForAsset:imageAsset targetSize:request.targetSize contentMode:request.contentMode options:options resultHandler:^(UIImage *image, NSDictionary *info) {
if (NSThread.isMainThread) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
completionBlock(image, info[PHImageErrorKey]);
});
} else {
completionBlock(image, info[PHImageErrorKey]);
}
}];
});
}
- (void)_fetchImageWithIdentifierFromCache:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image))completionBlock
@@ -642,3 +652,16 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent
}
@end
@implementation NSURL (ASPhotosFrameworkURLs)
+ (NSURL *)URLWithAssetLocalIdentifier:(NSString *)assetLocalIdentifier targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(PHImageRequestOptions *)options
{
ASPhotosFrameworkImageRequest *request = [[ASPhotosFrameworkImageRequest alloc] initWithAssetIdentifier:assetLocalIdentifier];
request.options = options;
request.contentMode = contentMode;
request.targetSize = targetSize;
return request.url;
}
@end

View File

@@ -44,6 +44,7 @@
_cache = cache;
_downloader = downloader;
_shouldCacheImage = YES;
self.shouldBypassEnsureDisplay = YES;
return self;
}

View File

@@ -28,8 +28,28 @@ NS_ASSUME_NONNULL_BEGIN
*/
@interface ASTableView : UITableView
@property (nonatomic, weak) id<ASTableViewDataSource> asyncDataSource;
@property (nonatomic, weak) id<ASTableViewDelegate> asyncDelegate; // must not be nil
@property (nonatomic, weak) id<ASTableViewDataSource> asyncDataSource;
/**
* Initializer.
*
* @param frame A rectangle specifying the initial location and size of the table view in its superview€™s coordinates.
* 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;
/**
* Tuning parameters for a range.
@@ -51,26 +71,6 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType;
/**
* Initializer.
*
* @param frame A rectangle specifying the initial location and size of the table view in its superview€™s coordinates.
* 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 Enable the data fetching in async mode.
*
* @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;
/**
* The number of screens left to scroll before the delegate -tableView:beginBatchFetchingWithContext: is called.
*
@@ -228,6 +228,15 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (ASCellNode *)nodeForRowAtIndexPath:(NSIndexPath *)indexPath;
/**
* Similar to -indexPathForCell:.
*
* @param cellNode a cellNode part of the table view
*
* @returns an indexPath for this cellNode
*/
- (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode;
/**
* Similar to -visibleCells.
*

View File

@@ -18,6 +18,9 @@
#import "ASInternalHelpers.h"
#import "ASLayout.h"
// FIXME: Temporary nonsense import until method names are finalized and exposed
#import "ASDisplayNode+Subclasses.h"
//#define LOG(...) NSLog(__VA_ARGS__)
#define LOG(...)
@@ -259,7 +262,8 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
- (void)setDataSource:(id<UITableViewDataSource>)dataSource
{
ASDisplayNodeAssert(NO, @"ASTableView uses asyncDataSource, not UITableView's dataSource property.");
// UIKit can internally generate a call to this method upon changing the asyncDataSource; only assert for non-nil.
ASDisplayNodeAssert(dataSource == nil, @"ASTableView uses asyncDataSource, not UITableView's dataSource property.");
}
- (void)setDelegate:(id<UITableViewDelegate>)delegate
@@ -345,6 +349,11 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
return [_dataController nodeAtIndexPath:indexPath];
}
- (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode
{
return [_dataController indexPathForNode:cellNode];
}
- (NSArray *)visibleNodes
{
NSArray *indexPaths = [self indexPathsForVisibleRows];
@@ -563,6 +572,11 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
if ([_asyncDelegate respondsToSelector:@selector(tableView:willDisplayNodeForRowAtIndexPath:)]) {
[_asyncDelegate tableView:self willDisplayNodeForRowAtIndexPath:indexPath];
}
ASCellNode *cellNode = [self nodeForRowAtIndexPath:indexPath];
if (cellNode.neverShowPlaceholders) {
[cellNode recursivelyEnsureDisplay];
}
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath

View File

@@ -15,6 +15,11 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, strong, readonly) ASDisplayNode *node;
// AsyncDisplayKit 2.0 BETA: This property is still being tested, but it allows
// blocking as a view controller becomes visible to ensure no placeholders flash onscreen.
// Refer to examples/SynchronousConcurrency, AsyncViewController.m
@property (nonatomic, assign) BOOL neverShowPlaceholders;
- (instancetype)initWithNode:(ASDisplayNode *)node;
@end

View File

@@ -10,7 +10,13 @@
#import "ASAssert.h"
#import "ASDimension.h"
// FIXME: Temporary nonsense import until method names are finalized and exposed
#import "ASDisplayNode+Subclasses.h"
@implementation ASViewController
{
BOOL _ensureDisplayed;
}
- (instancetype)initWithNode:(ASDisplayNode *)node
{
@@ -33,15 +39,25 @@
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
CGSize viewSize = self.view.bounds.size;
ASSizeRange constrainedSize = ASSizeRangeMake(viewSize, viewSize);
[_node measureWithSizeRange:constrainedSize];
[super viewWillLayoutSubviews];
}
- (void)viewDidLayoutSubviews
{
if (_ensureDisplayed && self.neverShowPlaceholders) {
_ensureDisplayed = NO;
[self.node recursivelyEnsureDisplay];
}
[super viewDidLayoutSubviews];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
_ensureDisplayed = YES;
[_node recursivelyFetchData];
}

View File

@@ -18,6 +18,7 @@
#import <AsyncDisplayKit/ASBasicImageDownloader.h>
#import <AsyncDisplayKit/ASMultiplexImageNode.h>
#import <AsyncDisplayKit/ASNetworkImageNode.h>
#import <AsyncDisplayKit/ASPhotosFrameworkImageRequest.h>
#import <AsyncDisplayKit/ASTableView.h>
#import <AsyncDisplayKit/ASCollectionView.h>

View File

@@ -175,6 +175,8 @@ typedef NSUInteger ASDataControllerAnimationOptions;
- (nullable ASCellNode *)nodeAtIndexPath:(NSIndexPath *)indexPath;
- (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode;
- (NSArray<ASCellNode *> *)nodesAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
- (NSArray<NSArray <ASCellNode *> *> *)completedNodes; // This provides efficient access to the entire _completedNodes multidimensional array.

View File

@@ -24,7 +24,8 @@ const static NSUInteger kASDataControllerSizingCountPerProcessor = 5;
static void *kASSizingQueueContext = &kASSizingQueueContext;
@interface ASDataController () {
NSMutableArray *_completedNodes; // Main thread only. External data access can immediately query this.
NSMutableArray *_externalCompletedNodes; // Main thread only. External data access can immediately query this if available.
NSMutableArray *_completedNodes; // Main thread only. External data access can immediately query this if _externalCompletedNodes is unavailable.
NSMutableArray *_editingNodes; // Modified on _editingTransactionQueue only. Updates propogated to _completedNodes.
NSMutableArray *_pendingEditCommandBlocks; // To be run on the main thread. Handles begin/endUpdates tracking.
@@ -104,31 +105,27 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
}
dispatch_group_t layoutGroup = dispatch_group_create();
ASSizeRange *nodeBoundSizes = (ASSizeRange *)malloc(sizeof(ASSizeRange) * nodes.count);
for (NSUInteger j = 0; j < nodes.count && j < indexPaths.count; j += kASDataControllerSizingCountPerProcessor) {
NSArray *subIndexPaths = [indexPaths subarrayWithRange:NSMakeRange(j, MIN(kASDataControllerSizingCountPerProcessor, indexPaths.count - j))];
NSInteger batchCount = MIN(kASDataControllerSizingCountPerProcessor, indexPaths.count - j);
//TODO: There should be a fast-path that avoids all of this object creation.
NSMutableArray *nodeBoundSizes = [[NSMutableArray alloc] initWithCapacity:kASDataControllerSizingCountPerProcessor];
[subIndexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
ASSizeRange constrainedSize = [_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPath];
[nodeBoundSizes addObject:[NSValue valueWithBytes:&constrainedSize objCType:@encode(ASSizeRange)]];
}];
for (NSUInteger k = j; k < j + batchCount; k++) {
nodeBoundSizes[k] = [_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPaths[k]];
}
dispatch_group_async(layoutGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[subIndexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
ASCellNode *node = nodes[j + idx];
ASSizeRange constrainedSize;
[nodeBoundSizes[idx] getValue:&constrainedSize];
for (NSUInteger k = j; k < j + batchCount; k++) {
ASCellNode *node = nodes[k];
ASSizeRange constrainedSize = nodeBoundSizes[k];
[node measureWithSizeRange:constrainedSize];
node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height);
}];
node.frame = CGRectMake(0, 0, node.calculatedSize.width, node.calculatedSize.height);
}
});
}
// Block the _editingTransactionQueue from executing a new edit transaction until layout is done & _editingNodes array is updated.
dispatch_group_wait(layoutGroup, DISPATCH_TIME_FOREVER);
free(nodeBoundSizes);
// Insert finished nodes into data storage
[self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
}
@@ -347,6 +344,10 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
[_editingTransactionQueue addOperationWithBlock:^{
ASDisplayNodePerformBlockOnMainThread(^{
// Deep copy _completedNodes to _externalCompletedNodes.
// Any external queries from now on will be done on _externalCompletedNodes, to guarantee data consistency with the delegate.
_externalCompletedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_completedNodes);
LOG(@"endUpdatesWithCompletion - begin updates call to delegate");
[_delegate dataControllerBeginUpdates:self];
});
@@ -363,6 +364,9 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
[_editingTransactionQueue addOperationWithBlock:^{
ASDisplayNodePerformBlockOnMainThread(^{
// Now that the transaction is done, _completedNodes can be accessed externally again.
_externalCompletedNodes = nil;
LOG(@"endUpdatesWithCompletion - calling delegate end");
[_delegate dataController:self endUpdatesAnimated:animated completion:completion];
});
@@ -617,31 +621,51 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
- (NSUInteger)numberOfSections
{
ASDisplayNodeAssertMainThread();
return [_completedNodes count];
return [[self completedNodes] count];
}
- (NSUInteger)numberOfRowsInSection:(NSUInteger)section
{
ASDisplayNodeAssertMainThread();
return [_completedNodes[section] count];
return [[self completedNodes][section] count];
}
- (ASCellNode *)nodeAtIndexPath:(NSIndexPath *)indexPath
{
ASDisplayNodeAssertMainThread();
return _completedNodes[indexPath.section][indexPath.row];
return [self completedNodes][indexPath.section][indexPath.row];
}
- (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode;
{
ASDisplayNodeAssertMainThread();
NSArray *nodes = [self completedNodes];
NSUInteger numberOfNodes = nodes.count;
// 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];
}
}
return nil;
}
- (NSArray *)nodesAtIndexPaths:(NSArray *)indexPaths
{
ASDisplayNodeAssertMainThread();
return ASFindElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, [indexPaths sortedArrayUsingSelector:@selector(compare:)]);
return ASFindElementsInMultidimensionalArrayAtIndexPaths((NSMutableArray *)[self completedNodes], [indexPaths sortedArrayUsingSelector:@selector(compare:)]);
}
/// Returns nodes that can be queried externally. _externalCompletedNodes is used if available, _completedNodes otherwise.
- (NSArray *)completedNodes
{
ASDisplayNodeAssertMainThread();
return _completedNodes;
return _externalCompletedNodes != nil ? _externalCompletedNodes : _completedNodes;
}
#pragma mark - Dealloc

View File

@@ -0,0 +1,66 @@
//
// ASPhotosFrameworkImageRequest.h
// AsyncDisplayKit
//
// Created by Adlai Holler on 9/25/15.
// Copyright © 2015 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <Photos/Photos.h>
// NS_ASSUME_NONNULL_BEGIN
extern NSString *const ASPhotosURLScheme;
/**
@abstract Use ASPhotosFrameworkImageRequest to encapsulate all the information needed to request an image from
the Photos framework and store it in a URL.
*/
@interface ASPhotosFrameworkImageRequest : NSObject <NSCopying>
- (instancetype)initWithAssetIdentifier:(NSString *)assetIdentifier NS_DESIGNATED_INITIALIZER;
/**
@return A new image request deserialized from `url`, or nil if `url` is not a valid photos URL.
*/
+ (/*nullable*/ ASPhotosFrameworkImageRequest *)requestWithURL:(NSURL *)url;
/**
@abstract The asset identifier for this image request provided during initialization.
*/
@property (nonatomic, readonly) NSString *assetIdentifier;
/**
@abstract The target size for this image request. Defaults to `PHImageManagerMaximumSize`.
*/
@property (nonatomic) CGSize targetSize;
/**
@abstract The content mode for this image request. Defaults to `PHImageContentModeDefault`.
@see `PHImageManager`
*/
@property (nonatomic) PHImageContentMode contentMode;
/**
@abstract The options specified for this request. Default value is the result of `[PHImageRequestOptions new]`.
@discussion Some properties of this object are ignored when converting this request into a URL.
As of iOS SDK 9.0, these properties are `progressHandler` and `synchronous`.
*/
@property (nonatomic, strong) PHImageRequestOptions *options;
/**
@return A new URL converted from this request.
*/
@property (nonatomic, readonly) NSURL *url;
/**
@return `YES` if `object` is an equivalent image request, `NO` otherwise.
*/
- (BOOL)isEqual:(id)object;
@end
// NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,161 @@
//
// ASPhotosFrameworkImageRequest.m
// AsyncDisplayKit
//
// Created by Adlai Holler on 9/25/15.
// Copyright © 2015 Facebook. All rights reserved.
//
#import "ASPhotosFrameworkImageRequest.h"
#import "ASBaseDefines.h"
#import "ASAvailability.h"
NSString *const ASPhotosURLScheme = @"ph";
static NSString *const _ASPhotosURLQueryKeyWidth = @"width";
static NSString *const _ASPhotosURLQueryKeyHeight = @"height";
// value is PHImageContentMode value
static NSString *const _ASPhotosURLQueryKeyContentMode = @"contentmode";
// value is PHImageRequestOptionsResizeMode value
static NSString *const _ASPhotosURLQueryKeyResizeMode = @"resizemode";
// value is PHImageRequestOptionsDeliveryMode value
static NSString *const _ASPhotosURLQueryKeyDeliveryMode = @"deliverymode";
// value is PHImageRequestOptionsVersion value
static NSString *const _ASPhotosURLQueryKeyVersion = @"version";
// value is 0 or 1
static NSString *const _ASPhotosURLQueryKeyAllowNetworkAccess = @"network";
static NSString *const _ASPhotosURLQueryKeyCropOriginX = @"crop_x";
static NSString *const _ASPhotosURLQueryKeyCropOriginY = @"crop_y";
static NSString *const _ASPhotosURLQueryKeyCropWidth = @"crop_w";
static NSString *const _ASPhotosURLQueryKeyCropHeight = @"crop_h";
@implementation ASPhotosFrameworkImageRequest
- (instancetype)init
{
ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER();
self = [self initWithAssetIdentifier:@""];
return nil;
}
- (instancetype)initWithAssetIdentifier:(NSString *)assetIdentifier
{
self = [super init];
if (self) {
_assetIdentifier = assetIdentifier;
_options = [PHImageRequestOptions new];
_contentMode = PHImageContentModeDefault;
_targetSize = PHImageManagerMaximumSize;
}
return self;
}
#pragma mark NSCopying
- (id)copyWithZone:(NSZone *)zone
{
ASPhotosFrameworkImageRequest *copy = [[ASPhotosFrameworkImageRequest alloc] initWithAssetIdentifier:self.assetIdentifier];
copy.options = [self.options copy];
copy.targetSize = self.targetSize;
copy.contentMode = self.contentMode;
return copy;
}
#pragma mark Converting to URL
- (NSURL *)url
{
NSURLComponents *comp = [NSURLComponents new];
comp.scheme = ASPhotosURLScheme;
comp.host = _assetIdentifier;
NSMutableArray *queryItems = [NSMutableArray arrayWithObjects:
[NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyWidth value:@(_targetSize.width).stringValue],
[NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyHeight value:@(_targetSize.height).stringValue],
[NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyVersion value:@(_options.version).stringValue],
[NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyContentMode value:@(_contentMode).stringValue],
[NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyAllowNetworkAccess value:@(_options.networkAccessAllowed).stringValue],
[NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyResizeMode value:@(_options.resizeMode).stringValue],
[NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyDeliveryMode value:@(_options.deliveryMode).stringValue]
, nil];
CGRect cropRect = _options.normalizedCropRect;
if (!CGRectIsEmpty(cropRect)) {
[queryItems addObjectsFromArray:@[
[NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropOriginX value:@(cropRect.origin.x).stringValue],
[NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropOriginY value:@(cropRect.origin.y).stringValue],
[NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropWidth value:@(cropRect.size.width).stringValue],
[NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropHeight value:@(cropRect.size.height).stringValue]
]];
}
comp.queryItems = queryItems;
return comp.URL;
}
#pragma mark Converting from URL
+ (ASPhotosFrameworkImageRequest *)requestWithURL:(NSURL *)url
{
// not a photos URL or iOS < 8
if (![url.scheme isEqualToString:ASPhotosURLScheme] || !AS_AT_LEAST_IOS8) {
return nil;
}
NSURLComponents *comp = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
ASPhotosFrameworkImageRequest *request = [[ASPhotosFrameworkImageRequest alloc] initWithAssetIdentifier:url.host];
CGRect cropRect = CGRectZero;
CGSize targetSize = PHImageManagerMaximumSize;
for (NSURLQueryItem *item in comp.queryItems) {
if ([_ASPhotosURLQueryKeyAllowNetworkAccess isEqualToString:item.name]) {
request.options.networkAccessAllowed = item.value.boolValue;
} else if ([_ASPhotosURLQueryKeyWidth isEqualToString:item.name]) {
targetSize.width = item.value.doubleValue;
} else if ([_ASPhotosURLQueryKeyHeight isEqualToString:item.name]) {
targetSize.height = item.value.doubleValue;
} else if ([_ASPhotosURLQueryKeyContentMode isEqualToString:item.name]) {
request.contentMode = (PHImageContentMode)item.value.integerValue;
} else if ([_ASPhotosURLQueryKeyVersion isEqualToString:item.name]) {
request.options.version = (PHImageRequestOptionsVersion)item.value.integerValue;
} else if ([_ASPhotosURLQueryKeyCropOriginX isEqualToString:item.name]) {
cropRect.origin.x = item.value.doubleValue;
} else if ([_ASPhotosURLQueryKeyCropOriginY isEqualToString:item.name]) {
cropRect.origin.y = item.value.doubleValue;
} else if ([_ASPhotosURLQueryKeyCropWidth isEqualToString:item.name]) {
cropRect.size.width = item.value.doubleValue;
} else if ([_ASPhotosURLQueryKeyCropHeight isEqualToString:item.name]) {
cropRect.size.height = item.value.doubleValue;
} else if ([_ASPhotosURLQueryKeyResizeMode isEqualToString:item.name]) {
request.options.resizeMode = (PHImageRequestOptionsResizeMode)item.value.integerValue;
} else if ([_ASPhotosURLQueryKeyDeliveryMode isEqualToString:item.name]) {
request.options.deliveryMode = (PHImageRequestOptionsDeliveryMode)item.value.integerValue;
}
}
request.targetSize = targetSize;
request.options.normalizedCropRect = cropRect;
return request;
}
#pragma mark NSObject
- (BOOL)isEqual:(id)object
{
if (![object isKindOfClass:ASPhotosFrameworkImageRequest.class]) {
return NO;
}
ASPhotosFrameworkImageRequest *other = object;
return [other.assetIdentifier isEqualToString:self.assetIdentifier] &&
other.contentMode == self.contentMode &&
CGSizeEqualToSize(other.targetSize, self.targetSize) &&
CGRectEqualToRect(other.options.normalizedCropRect, self.options.normalizedCropRect) &&
other.options.resizeMode == self.options.resizeMode &&
other.options.version == self.options.version;
}
@end

View File

@@ -26,6 +26,7 @@ typedef NS_ENUM(NSUInteger, ASAsyncTransactionState) {
ASAsyncTransactionStateOpen = 0,
ASAsyncTransactionStateCommitted,
ASAsyncTransactionStateCanceled,
ASAsyncTransactionStateComplete
};
/**
@@ -54,6 +55,13 @@ typedef NS_ENUM(NSUInteger, ASAsyncTransactionState) {
- (id)initWithCallbackQueue:(dispatch_queue_t)callbackQueue
completionBlock:(asyncdisplaykit_async_transaction_completion_block_t)completionBlock;
/**
@summary Block the main thread until the transaction is complete, including callbacks.
@desc This must be called on the main thread.
*/
- (void)waitUntilComplete;
/**
The dispatch queue that the completion blocks will be called on.
*/

View File

@@ -7,7 +7,7 @@
*/
#import "_ASAsyncTransaction.h"
#import "_ASAsyncTransactionGroup.h"
#import "ASAssert.h"
@interface ASDisplayNodeAsyncTransactionOperation : NSObject
@@ -40,6 +40,11 @@
}
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<ASDisplayNodeAsyncTransactionOperation: %p - value = %@", self, self.value];
}
@end
@implementation _ASAsyncTransaction
@@ -140,7 +145,7 @@
ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(_state == ASAsyncTransactionStateOpen, @"You cannot double-commit a transaction");
_state = ASAsyncTransactionStateCommitted;
if ([_operations count] == 0) {
// Fast path: if a transaction was opened, but no operations were added, execute completion block synchronously.
if (_completionBlock) {
@@ -148,18 +153,59 @@
}
} else {
ASDisplayNodeAssert(_group != NULL, @"If there are operations, dispatch group should have been created");
dispatch_group_notify(_group, _callbackQueue, ^{
BOOL isCanceled = (_state == ASAsyncTransactionStateCanceled);
for (ASDisplayNodeAsyncTransactionOperation *operation in _operations) {
[operation callAndReleaseCompletionBlock:isCanceled];
}
if (_completionBlock) {
_completionBlock(self, isCanceled);
}
// _callbackQueue is the main queue in current practice (also asserted in -waitUntilComplete).
// This code should be reviewed before taking on significantly different use cases.
ASDisplayNodeAssertMainThread();
[self completeTransaction];
});
}
}
- (void)completeTransaction
{
if (_state != ASAsyncTransactionStateComplete) {
BOOL isCanceled = (_state == ASAsyncTransactionStateCanceled);
for (ASDisplayNodeAsyncTransactionOperation *operation in _operations) {
[operation callAndReleaseCompletionBlock:isCanceled];
}
// 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;
if (_completionBlock) {
_completionBlock(self, isCanceled);
}
}
}
- (void)waitUntilComplete
{
ASDisplayNodeAssertMainThread();
if (_state != ASAsyncTransactionStateComplete) {
if (_group) {
ASDisplayNodeAssertTrue(_callbackQueue == dispatch_get_main_queue());
dispatch_group_wait(_group, DISPATCH_TIME_FOREVER);
// At this point, the asynchronous operation may have completed, but the runloop
// observer has not committed the batch of transactions we belong to. It's important to
// 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) {
[_ASAsyncTransactionGroup commit];
ASDisplayNodeAssert(_state != 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 accomodate this by checking _state to ensure it is not complete.
[self completeTransaction];
}
}
}
#pragma mark -
#pragma mark Helper Methods
@@ -174,4 +220,9 @@
}
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<_ASAsyncTransaction: %p - _state = %lu, _group = %@, _operations = %@>", self, (unsigned long)_state, _group, _operations];
}
@end

View File

@@ -15,6 +15,7 @@
@interface _ASAsyncTransactionGroup : NSObject
/// The main transaction group is scheduled to commit on every tick of the main runloop.
+ (instancetype)mainTransactionGroup;
+ (void)commit;
/// Add a transaction container to be committed.
/// @param containerLayer A layer containing a transaction to be commited. May or may not be a container layer.

View File

@@ -95,6 +95,11 @@ static void _transactionGroupRunLoopObserverCallback(CFRunLoopObserverRef observ
}
}
+ (void)commit
{
[[_ASAsyncTransactionGroup mainTransactionGroup] commit];
}
@end
static void _transactionGroupRunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)

View File

@@ -98,6 +98,7 @@ static Class gDefaultLayoutOptionsClass = nil;
self.spacingBefore = layoutOptions.spacingBefore;
self.flexGrow = layoutOptions.flexGrow;
self.flexShrink = layoutOptions.flexShrink;
self.alignSelf = layoutOptions.alignSelf;
self.ascender = layoutOptions.ascender;
self.descender = layoutOptions.descender;

View File

@@ -58,18 +58,14 @@
[sublayouts addObject:sublayout];
}
if (isnan(size.width) || size.width >= FLT_MAX - FLT_EPSILON) {
size.width = constrainedSize.min.width;
for (ASLayout *sublayout in sublayouts) {
size.width = MAX(size.width, sublayout.position.x + sublayout.size.width);
}
size.width = constrainedSize.min.width;
for (ASLayout *sublayout in sublayouts) {
size.width = MAX(size.width, sublayout.position.x + sublayout.size.width);
}
if (isnan(size.height) || size.height >= FLT_MAX - FLT_EPSILON) {
size.height = constrainedSize.min.height;
for (ASLayout *sublayout in sublayouts) {
size.height = MAX(size.height, sublayout.position.y + sublayout.size.height);
}
size.height = constrainedSize.min.height;
for (ASLayout *sublayout in sublayouts) {
size.height = MAX(size.height, sublayout.position.y + sublayout.size.height);
}
return [ASLayout layoutWithLayoutableObject:self

View File

@@ -94,6 +94,8 @@ static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync,
// Capture these outside the display block so they are retained.
UIColor *backgroundColor = self.backgroundColor;
CGRect bounds = self.bounds;
CGFloat cornerRadius = self.cornerRadius;
BOOL clipsToBounds = self.clipsToBounds;
CGRect frame;
@@ -129,7 +131,7 @@ static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync,
CGContextTranslateCTM(context, frame.origin.x, frame.origin.y);
//support cornerRadius
if (rasterizingFromAscendent && self.cornerRadius && self.clipsToBounds) {
if (rasterizingFromAscendent && cornerRadius && clipsToBounds) {
[[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:self.cornerRadius] addClip];
}
@@ -308,16 +310,14 @@ static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync,
return BOOL(displaySentinelValue != displaySentinel.value);
};
// If we're participating in an ancestor's asyncTransaction, find it here
ASDisplayNodeAssert(_layer, @"Expect _layer to be not nil");
CALayer *containerLayer = _layer.asyncdisplaykit_parentTransactionContainer ?: _layer;
_ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction;
// Set up displayBlock to call either display or draw on the delegate and return a UIImage contents
asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:asynchronously isCancelledBlock:isCancelledBlock rasterizing:NO];
if (!displayBlock) {
return;
}
ASDisplayNodeAssert(_layer, @"Expect _layer to be not nil");
// This block is called back on the main thread after rendering at the completion of the current async transaction, or immediately if !asynchronously
asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id<NSObject> value, BOOL canceled){
@@ -335,16 +335,27 @@ static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync,
}
};
if (displayBlock != NULL) {
// Call willDisplay immediately in either case
[self willDisplayAsyncLayer:self.asyncLayer];
// Call willDisplay immediately in either case
[self willDisplayAsyncLayer:self.asyncLayer];
if (asynchronously) {
[transaction addOperationWithBlock:displayBlock queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
} else {
UIImage *contents = (UIImage *)displayBlock();
completionBlock(contents, NO);
}
if (asynchronously) {
// Async rendering operations are contained by a transaction, which allows them to proceed and concurrently
// while synchronizing the final application of the results to the layer's contents property (completionBlock).
// First, look to see if we are expected to join a parent's transaction container.
CALayer *containerLayer = _layer.asyncdisplaykit_parentTransactionContainer ?: _layer;
// In the case that a transaction does not yet exist (such as for an individual node outside of a container),
// this call will allocate the transaction and add it to _ASAsyncTransactionGroup.
// It will automatically commit the transaction at the end of the runloop.
_ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction;
// Adding this displayBlock operation to the transaction will start it IMMEDIATELY.
// The only function of the transaction commit is to gate the calling of the completionBlock.
[transaction addOperationWithBlock:displayBlock queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
} else {
UIImage *contents = (UIImage *)displayBlock();
completionBlock(contents, NO);
}
}

View File

@@ -60,7 +60,7 @@
#define _messageToLayer(layerSelector) __loaded ? [_layer layerSelector] : [self.pendingViewState layerSelector]
/**
* This category implements certainly frequently-used properties and methods of UIView and CALayer so that ASDisplayNode clients can just call the view/layer methods on the node,
* This category implements certain frequently-used properties and methods of UIView and CALayer so that ASDisplayNode clients can just call the view/layer methods on the node,
* with minimal loss in performance. Unlike UIView and CALayer methods, these can be called from a non-main thread until the view or layer is created.
* This allows text sizing in -calculateSizeThatFits: (essentially a simplified layout) to happen off the main thread
* without any CALayer or UIView actually existing while still being able to set and read properties from ASDisplayNode instances.
@@ -122,7 +122,7 @@
// Frame is only defined when transform is identity.
#if DEBUG
// Checking if the transform is identity is expensive, so disable when unnecessary. We have assertions on in Release, so DEBUG is the only way I know of.
ASDisplayNodeAssert(CATransform3DIsIdentity(self.transform), @"Must be an identity transform");
ASDisplayNodeAssert(CATransform3DIsIdentity(self.transform), @"-[ASDisplayNode frame] - self.transform must be identity in order to use the frame property. (From Apple's UIView documentation: If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.)");
#endif
CGPoint position = self.position;
@@ -140,7 +140,7 @@
// Frame is only defined when transform is identity because we explicitly diverge from CALayer behavior and define frame without transform
#if DEBUG
// Checking if the transform is identity is expensive, so disable when unnecessary. We have assertions on in Release, so DEBUG is the only way I know of.
ASDisplayNodeAssert(CATransform3DIsIdentity(self.transform), @"Must be an identity transform");
ASDisplayNodeAssert(CATransform3DIsIdentity(self.transform), @"-[ASDisplayNode setFrame:] - self.transform must be identity in order to set the frame property. (From Apple's UIView documentation: If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.)");
#endif
BOOL useLayer = (_layer && ASDisplayNodeThreadIsMain());

View File

@@ -80,6 +80,7 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) {
unsigned layerBacked:1;
unsigned displaysAsynchronously:1;
unsigned shouldRasterizeDescendants:1;
unsigned shouldBypassEnsureDisplay:1;
unsigned displaySuspended:1;
// whether custom drawing is enabled