Skip to content
This repository was archived by the owner on Feb 2, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions AsyncDisplayKit/ASCellNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,29 @@
*/
@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.
*/
Expand Down
9 changes: 9 additions & 0 deletions AsyncDisplayKit/ASCollectionView.h
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,15 @@
*/
- (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.
*
Expand Down
23 changes: 18 additions & 5 deletions AsyncDisplayKit/ASCollectionView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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;


Expand Down Expand Up @@ -315,6 +318,16 @@ - (CGSize)calculatedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath
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];
Expand Down Expand Up @@ -392,11 +405,6 @@ - (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)
[_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:kASCollectionViewAnimationNone];
}

- (ASCellNode *)nodeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return [_dataController nodeAtIndexPath:indexPath];
}

#pragma mark -
#pragma mark Intercepted selectors.

Expand Down Expand Up @@ -490,6 +498,11 @@ - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICol
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
Expand Down
32 changes: 32 additions & 0 deletions AsyncDisplayKit/ASDisplayNode+Subclasses.h
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,38 @@
// 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.
- (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")
Expand Down
64 changes: 64 additions & 0 deletions AsyncDisplayKit/ASDisplayNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#import <objc/runtime.h>

#import "_ASAsyncTransaction.h"
#import "_ASAsyncTransactionContainer+Private.h"
#import "_ASPendingState.h"
#import "_ASDisplayView.h"
#import "_ASScopeTimer.h"
Expand Down Expand Up @@ -1415,6 +1416,68 @@ - (void)_tearDownPlaceholderLayer
[_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
Expand Down Expand Up @@ -1484,6 +1547,7 @@ - (CGSize)preferredFrameSize
ASDN::MutexLocker l(_propertyLock);
return _preferredFrameSize;
}

- (UIImage *)placeholderImage
{
return nil;
Expand Down
1 change: 1 addition & 0 deletions AsyncDisplayKit/ASMultiplexImageNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ - (instancetype)initWithCache:(id<ASImageCacheProtocol>)cache downloader:(id<ASI

_cache = cache;
_downloader = downloader;
self.shouldBypassEnsureDisplay = YES;

return self;
}
Expand Down
1 change: 1 addition & 0 deletions AsyncDisplayKit/ASNetworkImageNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ - (instancetype)initWithCache:(id<ASImageCacheProtocol>)cache downloader:(id<ASI
_cache = cache;
_downloader = downloader;
_shouldCacheImage = YES;
self.shouldBypassEnsureDisplay = YES;

return self;
}
Expand Down
42 changes: 21 additions & 21 deletions AsyncDisplayKit/ASTableView.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,28 @@
*/
@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.
Expand All @@ -50,26 +70,6 @@
*/
- (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.
*
Expand Down
8 changes: 8 additions & 0 deletions AsyncDisplayKit/ASTableView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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(...)

Expand Down Expand Up @@ -569,6 +572,11 @@ - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)ce
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
Expand Down
6 changes: 5 additions & 1 deletion AsyncDisplayKit/ASViewController.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@

@property (nonatomic, strong, readonly) ASDisplayNode *node;

//TODO Use nonnull annotation late on. Travis doesn't recognize it (yet).
// 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
18 changes: 17 additions & 1 deletion AsyncDisplayKit/ASViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -33,15 +39,25 @@ - (void)loadView

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

Expand Down
8 changes: 8 additions & 0 deletions AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ typedef NS_ENUM(NSUInteger, ASAsyncTransactionState) {
ASAsyncTransactionStateOpen = 0,
ASAsyncTransactionStateCommitted,
ASAsyncTransactionStateCanceled,
ASAsyncTransactionStateComplete
};

/**
Expand Down Expand Up @@ -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.
*/
Expand Down
Loading