diff --git a/AsyncDisplayKit/ASButtonNode.mm b/AsyncDisplayKit/ASButtonNode.mm index f1f40a33..6341f137 100644 --- a/AsyncDisplayKit/ASButtonNode.mm +++ b/AsyncDisplayKit/ASButtonNode.mm @@ -14,7 +14,6 @@ #import "ASDisplayNode+Subclasses.h" #import "ASBackgroundLayoutSpec.h" #import "ASInsetLayoutSpec.h" -#import "ASDisplayNode+Beta.h" #import "ASStaticLayoutSpec.h" @interface ASButtonNode () @@ -56,7 +55,7 @@ - (instancetype)init { if (self = [super init]) { - self.usesImplicitHierarchyManagement = YES; + self.automaticallyManagesSubnodes = YES; _contentSpacing = 8.0; _laysOutHorizontally = YES; diff --git a/AsyncDisplayKit/ASCollectionNode.mm b/AsyncDisplayKit/ASCollectionNode.mm index c8d7aa25..510ad4e5 100644 --- a/AsyncDisplayKit/ASCollectionNode.mm +++ b/AsyncDisplayKit/ASCollectionNode.mm @@ -17,6 +17,7 @@ #import "ASEnvironmentInternal.h" #import "ASInternalHelpers.h" #import "ASCellNode+Internal.h" +#import "AsyncDisplayKit+Debug.h" #pragma mark - _ASCollectionPendingState @@ -171,6 +172,12 @@ [self.view clearFetchedData]; } +- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState +{ + [super interfaceStateDidChange:newState fromState:oldState]; + [ASRangeController layoutDebugOverlayIfNeeded]; +} + #if ASRangeControllerLoggingEnabled - (void)visibleStateDidChange:(BOOL)isVisible { diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 7d222eae..dd121126 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -18,7 +18,6 @@ #import "ASCollectionViewFlowLayoutInspector.h" #import "ASDisplayNodeExtras.h" #import "ASDisplayNode+FrameworkPrivate.h" -#import "ASDisplayNode+Beta.h" #import "ASInternalHelpers.h" #import "UICollectionViewLayout+ASConvenience.h" #import "ASRangeController.h" @@ -1076,6 +1075,11 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; return [_dataController nodeAtIndexPath:indexPath]; } +- (NSString *)nameForRangeControllerDataSource +{ + return self.asyncDataSource ? NSStringFromClass([self.asyncDataSource class]) : NSStringFromClass([self class]); +} + #pragma mark - ASRangeControllerDelegate - (void)didBeginUpdatesInRangeController:(ASRangeController *)rangeController diff --git a/AsyncDisplayKit/ASDisplayNode+Beta.h b/AsyncDisplayKit/ASDisplayNode+Beta.h index aa3b3d96..4d028f19 100644 --- a/AsyncDisplayKit/ASDisplayNode+Beta.h +++ b/AsyncDisplayKit/ASDisplayNode+Beta.h @@ -20,9 +20,6 @@ ASDISPLAYNODE_EXTERN_C_END @interface ASDisplayNode (Beta) -+ (BOOL)usesImplicitHierarchyManagement; -+ (void)setUsesImplicitHierarchyManagement:(BOOL)enabled; - /** * ASTableView and ASCollectionView now throw exceptions on invalid updates * like their UIKit counterparts. If YES, these classes will log messages @@ -63,7 +60,6 @@ ASDISPLAYNODE_EXTERN_C_END /** @name Layout Transitioning */ -@property (nonatomic) BOOL usesImplicitHierarchyManagement; /** * @abstract Currently used by ASNetworkImageNode and ASMultiplexImageNode to allow their placeholders to stay if they are loading an image from the network. diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index 08b2bfe4..cd4f5508 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -749,6 +749,23 @@ NS_ASSUME_NONNULL_BEGIN @interface ASDisplayNode (LayoutTransitioning) +/** + * @abstract The amount of time it takes to complete the default transition animation. Default is 0.2. + */ +@property (nonatomic, assign) NSTimeInterval defaultLayoutTransitionDuration; + +/** + * @abstract The amount of time (measured in seconds) to wait before beginning the default transition animation. + * Default is 0.0. + */ +@property (nonatomic, assign) NSTimeInterval defaultLayoutTransitionDelay; + +/** + * @abstract A mask of options indicating how you want to perform the default transition animations. + * For a list of valid constants, see UIViewAnimationOptions. + */ +@property (nonatomic, assign) UIViewAnimationOptions defaultLayoutTransitionOptions; + /** * @discussion A place to perform your animation. New nodes have been inserted here. You can also use this time to re-order the hierarchy. */ @@ -796,8 +813,24 @@ NS_ASSUME_NONNULL_BEGIN @end /* - ASDisplayNode participates in ASAsyncTransactions, so you can determine when your subnodes are done rendering. - See: -(void)asyncdisplaykit_asyncTransactionContainerStateDidChange in ASDisplayNodeSubclass.h + * ASDisplayNode support for automatic subnode management. + */ +@interface ASDisplayNode (AutomaticSubnodeManagement) + +/** + * @abstract A boolean that shows whether the node automatically inserts and removes nodes based on the presence or + * absence of the node and its subnodes is completely determined in its layoutSpecThatFits: method. + * + * @discussion If flag is YES the node no longer require addSubnode: or removeFromSupernode method calls. The presence + * or absence of subnodes is completely determined in its layoutSpecThatFits: method. + */ +@property (nonatomic, assign) BOOL automaticallyManagesSubnodes; + +@end + +/* + * ASDisplayNode participates in ASAsyncTransactions, so you can determine when your subnodes are done rendering. + * See: -(void)asyncdisplaykit_asyncTransactionContainerStateDidChange in ASDisplayNodeSubclass.h */ @interface ASDisplayNode (ASDisplayNodeAsyncTransactionContainer) @end @@ -812,7 +845,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)addSubnode:(nonnull ASDisplayNode *)node; @end -/** CALayer(AsyncDisplayKit) defines convenience method for adding sub-ASDisplayNode to a CALayer. */ +/* + * CALayer(AsyncDisplayKit) defines convenience method for adding sub-ASDisplayNode to a CALayer. + */ @interface CALayer (AsyncDisplayKit) /** * Convenience method, equivalent to [layer addSublayer:node.layer]. @@ -873,6 +908,17 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)cancelLayoutTransitionsInProgress ASDISPLAYNODE_DEPRECATED; +/** + * @abstract A boolean that shows whether the node automatically inserts and removes nodes based on the presence or + * absence of the node and its subnodes is completely determined in its layoutSpecThatFits: method. + * + * @discussion If flag is YES the node no longer require addSubnode: or removeFromSupernode method calls. The presence + * or absence of subnodes is completely determined in its layoutSpecThatFits: method. + * + * @deprecated Deprecated in version 2.0: Use automaticallyManagesSubnodes + */ +@property (nonatomic, assign) BOOL usesImplicitHierarchyManagement ASDISPLAYNODE_DEPRECATED; + - (void)reclaimMemory ASDISPLAYNODE_DEPRECATED; - (void)recursivelyReclaimMemory ASDISPLAYNODE_DEPRECATED; @property (nonatomic, assign) BOOL placeholderFadesOut ASDISPLAYNODE_DEPRECATED; diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index f4b804d6..fbcf0396 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -77,18 +77,6 @@ NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp = @"AS @synthesize isFinalLayoutable = _isFinalLayoutable; @synthesize threadSafeBounds = _threadSafeBounds; -static BOOL usesImplicitHierarchyManagement = NO; - -+ (BOOL)usesImplicitHierarchyManagement -{ - return usesImplicitHierarchyManagement; -} - -+ (void)setUsesImplicitHierarchyManagement:(BOOL)enabled -{ - usesImplicitHierarchyManagement = enabled; -} - static BOOL suppressesInvalidCollectionUpdateExceptions = YES; + (BOOL)suppressesInvalidCollectionUpdateExceptions @@ -291,6 +279,10 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) _environmentState = ASEnvironmentStateMakeDefault(); + _defaultLayoutTransitionDuration = 0.2; + _defaultLayoutTransitionDelay = 0.0; + _defaultLayoutTransitionOptions = UIViewAnimationOptionBeginFromCurrentState; + _flags.canClearContentsOfLayer = YES; _flags.canCallNeedsDisplayOfLayer = NO; } @@ -699,6 +691,20 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) return !self.isNodeLoaded; } +#pragma mark - Automatic Hierarchy + +- (BOOL)automaticallyManagesSubnodes +{ + ASDN::MutexLocker l(__instanceLock__); + return _automaticallyManagesSubnodes; +} + +- (void)setAutomaticallyManagesSubnodes:(BOOL)automaticallyManagesSubnodes +{ + ASDN::MutexLocker l(__instanceLock__); + _automaticallyManagesSubnodes = automaticallyManagesSubnodes; +} + #pragma mark - Layout Transition - (void)transitionLayoutAnimated:(BOOL)animated measurementCompletion:(void (^)())completion @@ -747,11 +753,11 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) ASLayoutableSetCurrentContext(ASLayoutableContextMake(transitionID, NO)); ASDN::MutexLocker l(__instanceLock__); - BOOL disableImplicitHierarchyManagement = self.usesImplicitHierarchyManagement == NO; - self.usesImplicitHierarchyManagement = YES; // Temporary flag for 1.9.x + BOOL automaticallyManagesSubnodesDisabled = (self.automaticallyManagesSubnodes == NO); + self.automaticallyManagesSubnodes = YES; // Temporary flag for 1.9.x newLayout = [self calculateLayoutThatFits:constrainedSize]; - if (disableImplicitHierarchyManagement) { - self.usesImplicitHierarchyManagement = NO; // Temporary flag for 1.9.x + if (automaticallyManagesSubnodesDisabled) { + self.automaticallyManagesSubnodes = NO; // Temporary flag for 1.9.x } ASLayoutableClearCurrentContext(); @@ -816,18 +822,6 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) } } -- (BOOL)usesImplicitHierarchyManagement -{ - ASDN::MutexLocker l(__instanceLock__); - return _usesImplicitHierarchyManagement ? : [[self class] usesImplicitHierarchyManagement]; -} - -- (void)setUsesImplicitHierarchyManagement:(BOOL)value -{ - ASDN::MutexLocker l(__instanceLock__); - _usesImplicitHierarchyManagement = value; -} - - (BOOL)_isTransitionInProgress { ASDN::MutexLocker l(__instanceLock__); @@ -857,14 +851,118 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) #pragma mark Layout Transition API +- (void)setDefaultLayoutTransitionDuration:(NSTimeInterval)defaultLayoutTransitionDuration +{ + ASDN::MutexLocker l(__instanceLock__); + _defaultLayoutTransitionDuration = defaultLayoutTransitionDuration; +} + +- (NSTimeInterval)defaultLayoutTransitionDuration +{ + ASDN::MutexLocker l(__instanceLock__); + return _defaultLayoutTransitionDuration; +} + +- (void)setDefaultLayoutTransitionDelay:(NSTimeInterval)defaultLayoutTransitionDelay +{ + ASDN::MutexLocker l(__instanceLock__); + _defaultLayoutTransitionDelay = defaultLayoutTransitionDelay; +} + +- (NSTimeInterval)defaultLayoutTransitionDelay +{ + ASDN::MutexLocker l(__instanceLock__); + return _defaultLayoutTransitionDelay; +} + +- (void)setDefaultLayoutTransitionOptions:(UIViewAnimationOptions)defaultLayoutTransitionOptions +{ + ASDN::MutexLocker l(__instanceLock__); + _defaultLayoutTransitionOptions = defaultLayoutTransitionOptions; +} + +- (UIViewAnimationOptions)defaultLayoutTransitionOptions +{ + ASDN::MutexLocker l(__instanceLock__); + return _defaultLayoutTransitionOptions; +} + /* - * Hook for subclasse to perform an animation based on the given ASContextTransitioning. By default this just layouts - * applies all subnodes without animation and calls completes the transition on the context. + * Hook for subclasse to perform an animation based on the given ASContextTransitioning. By default a fade in and out + * animation is provided. */ - (void)animateLayoutTransition:(id)context { - [self __layoutSublayouts]; - [context completeTransition:YES]; + ASDisplayNode *node = self; + + NSAssert(node.isNodeLoaded == YES, @"Invalid node state"); + NSAssert([context isAnimated] == YES, @"Can't animate a non-animatable context"); + + NSArray *removedSubnodes = [context removedSubnodes]; + NSMutableArray *removedViews = [NSMutableArray array]; + NSMutableArray *insertedSubnodes = [[context insertedSubnodes] mutableCopy]; + NSMutableArray *movedSubnodes = [NSMutableArray array]; + + for (ASDisplayNode *subnode in [context subnodesForKey:ASTransitionContextToLayoutKey]) { + if ([insertedSubnodes containsObject:subnode] == NO) { + // This is an existing subnode, check if it is resized, moved or both + CGRect fromFrame = [context initialFrameForNode:subnode]; + CGRect toFrame = [context finalFrameForNode:subnode]; + if (CGSizeEqualToSize(fromFrame.size, toFrame.size) == NO) { + // To crossfade resized subnodes, show a snapshot of it on top. + // The node itself can then be treated as a newly-inserted one. + UIView *snapshotView = [subnode.view snapshotViewAfterScreenUpdates:YES]; + snapshotView.frame = [context initialFrameForNode:subnode]; + snapshotView.alpha = 1; + + [node.view insertSubview:snapshotView aboveSubview:subnode.view]; + [removedViews addObject:snapshotView]; + + [insertedSubnodes addObject:subnode]; + } + if (CGPointEqualToPoint(fromFrame.origin, toFrame.origin) == NO) { + [movedSubnodes addObject:subnode]; + } + } + } + + for (ASDisplayNode *insertedSubnode in insertedSubnodes) { + insertedSubnode.frame = [context finalFrameForNode:insertedSubnode]; + insertedSubnode.alpha = 0; + } + + [UIView animateWithDuration:self.defaultLayoutTransitionDuration delay:self.defaultLayoutTransitionDelay options:self.defaultLayoutTransitionOptions animations:^{ + // Fade removed subnodes and views out + for (ASDisplayNode *removedSubnode in removedSubnodes) { + removedSubnode.alpha = 0; + } + for (UIView *removedView in removedViews) { + removedView.alpha = 0; + } + + // Fade inserted subnodes in + for (ASDisplayNode *insertedSubnode in insertedSubnodes) { + insertedSubnode.alpha = 1; + } + + // Update frame of self and moved subnodes + CGSize fromSize = [context layoutForKey:ASTransitionContextFromLayoutKey].size; + CGSize toSize = [context layoutForKey:ASTransitionContextToLayoutKey].size; + BOOL isResized = (CGSizeEqualToSize(fromSize, toSize) == NO); + if (isResized == YES) { + CGPoint position = node.frame.origin; + node.frame = CGRectMake(position.x, position.y, toSize.width, toSize.height); + } + for (ASDisplayNode *movedSubnode in movedSubnodes) { + movedSubnode.frame = [context finalFrameForNode:movedSubnode]; + } + } completion:^(BOOL finished) { + for (UIView *removedView in removedViews) { + [removedView removeFromSuperview]; + } + // Subnode removals are automatically performed + [context completeTransition:finished]; + }]; } /* @@ -911,8 +1009,8 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) */ - (void)_completeLayoutTransition:(ASLayoutTransition *)layoutTransition { - // Layout transition is not supported for non implicit hierarchy managed nodes yet - if (layoutTransition == nil || self.usesImplicitHierarchyManagement == NO) { + // Layout transition is not supported for nodes that are not have automatic subnode management enabled + if (layoutTransition == nil || self.automaticallyManagesSubnodes == NO) { return; } @@ -1078,7 +1176,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) ASDN::MutexLocker l(__instanceLock__); // FIXME: Ideally we'd call this as soon as the node receives -setNeedsLayout - // but implicit hierarchy management would require us to modify the node tree + // but automatic subnode management would require us to modify the node tree // in the background on a loaded node, which isn't currently supported. if (_pendingViewState.hasSetNeedsLayout) { [self __setNeedsLayout]; @@ -1874,7 +1972,7 @@ static NSInteger incrementIfFound(NSInteger i) { // If a node was added to a supernode, the supernode could be in a layout pending state. All of the hierarchy state // properties related to the transition need to be copied over as well as propagated down the subtree. - // This is especially important as with Implicit Hierarchy Management adding subnodes can happen while a transition + // This is especially important as with automatic subnode management, adding subnodes can happen while a transition // is in fly if (ASHierarchyStateIncludesLayoutPending(stateToEnterOrExit)) { int32_t pendingTransitionId = newSupernode.pendingTransitionID; @@ -2593,7 +2691,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) ASDisplayNodeAssertTrue(layout.size.width >= 0.0); ASDisplayNodeAssertTrue(layout.size.height >= 0.0); - if (layoutTransition == nil || self.usesImplicitHierarchyManagement == NO) { + if (layoutTransition == nil || self.automaticallyManagesSubnodes == NO) { return; } @@ -3186,6 +3284,16 @@ static const char *ASDisplayNodeAssociatedNodeKey = "ASAssociatedNode"; [self cancelLayoutTransition]; } +- (BOOL)usesImplicitHierarchyManagement +{ + return self.automaticallyManagesSubnodes; +} + +- (void)setUsesImplicitHierarchyManagement:(BOOL)enabled +{ + self.automaticallyManagesSubnodes = enabled; +} + - (void)setPlaceholderFadesOut:(BOOL)placeholderFadesOut { self.placeholderFadeDuration = placeholderFadesOut ? 0.1 : 0.0; diff --git a/AsyncDisplayKit/ASTableNode.mm b/AsyncDisplayKit/ASTableNode.mm index 2f4de2d0..f8af1ae0 100644 --- a/AsyncDisplayKit/ASTableNode.mm +++ b/AsyncDisplayKit/ASTableNode.mm @@ -16,6 +16,7 @@ #import "ASDisplayNode+Subclasses.h" #import "ASInternalHelpers.h" #import "ASCellNode+Internal.h" +#import "AsyncDisplayKit+Debug.h" #pragma mark - _ASTablePendingState @@ -125,6 +126,12 @@ [self.view clearFetchedData]; } +- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState +{ + [super interfaceStateDidChange:newState fromState:oldState]; + [ASRangeController layoutDebugOverlayIfNeeded]; +} + #if ASRangeControllerLoggingEnabled - (void)visibleStateDidChange:(BOOL)isVisible { diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 958530cc..079ca9e2 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -17,7 +17,6 @@ #import "ASChangeSetDataController.h" #import "ASDelegateProxy.h" #import "ASDisplayNodeExtras.h" -#import "ASDisplayNode+Beta.h" #import "ASDisplayNode+FrameworkPrivate.h" #import "ASInternalHelpers.h" #import "ASLayout.h" @@ -937,6 +936,11 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; return ASInterfaceStateForDisplayNode(self.tableNode, self.window); } +- (NSString *)nameForRangeControllerDataSource +{ + return self.asyncDataSource ? NSStringFromClass([self.asyncDataSource class]) : NSStringFromClass([self class]); +} + #pragma mark - ASRangeControllerDelegate - (void)didBeginUpdatesInRangeController:(ASRangeController *)rangeController diff --git a/AsyncDisplayKit/ASTextNode+Beta.h b/AsyncDisplayKit/ASTextNode+Beta.h index 899b7d21..1e6dc387 100644 --- a/AsyncDisplayKit/ASTextNode+Beta.h +++ b/AsyncDisplayKit/ASTextNode+Beta.h @@ -31,6 +31,14 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nullable, nonatomic, copy) NSTextStorage * (^textStorageCreationBlock)(NSAttributedString *_Nullable attributedString); +/** + @abstract Text margins for text laid out in the text node. + @discussion defaults to UIEdgeInsetsZero. + This property can be useful for handling text which does not fit within the view by default. An example: like UILabel, + ASTextNode will clip the left and right of the string "judar" if it's rendered in an italicised font. + */ +@property (nonatomic, assign) UIEdgeInsets textContainerInset; + @end NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index e4edbbd9..b658be8a 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -48,6 +48,8 @@ struct ASTextNodeDrawParameter { UIColor *_cachedShadowUIColor; CGFloat _shadowOpacity; CGFloat _shadowRadius; + + UIEdgeInsets _textContainerInset; NSArray *_exclusionPaths; @@ -213,7 +215,15 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; ASDN::MutexLocker l(__instanceLock__); if (_renderer == nil) { - CGSize constrainedSize = _constrainedSize.width != -INFINITY ? _constrainedSize : bounds.size; + CGSize constrainedSize; + if (_constrainedSize.width != -INFINITY) { + constrainedSize = _constrainedSize; + } else { + constrainedSize = bounds.size; + constrainedSize.width -= (_textContainerInset.left + _textContainerInset.right); + constrainedSize.height -= (_textContainerInset.top + _textContainerInset.bottom); + } + _renderer = [[ASTextKitRenderer alloc] initWithTextKitAttributes:[self _rendererAttributes] constrainedSize:constrainedSize]; } @@ -279,6 +289,24 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; #pragma mark - Layout and Sizing +- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset +{ + ASDN::MutexLocker l(__instanceLock__); + + BOOL needsUpdate = !UIEdgeInsetsEqualToEdgeInsets(textContainerInset, _textContainerInset); + if (needsUpdate) { + _textContainerInset = textContainerInset; + [self invalidateCalculatedLayout]; + [self setNeedsLayout]; + } +} + +- (UIEdgeInsets)textContainerInset +{ + ASDN::MutexLocker l(__instanceLock__); + return _textContainerInset; +} + - (BOOL)_needInvalidateRendererForBoundsSize:(CGSize)boundsSize { ASDN::MutexLocker l(__instanceLock__); @@ -291,6 +319,10 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // a new one. However, there are common cases where the constrained size doesn't need to be the same as calculated. CGSize rendererConstrainedSize = _renderer.constrainedSize; + //inset bounds + boundsSize.width -= _textContainerInset.left + _textContainerInset.right; + boundsSize.height -= _textContainerInset.top + _textContainerInset.bottom; + if (CGSizeEqualToSize(boundsSize, rendererConstrainedSize)) { return NO; } else { @@ -321,9 +353,14 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; if (layout != nil) { ASDN::MutexLocker l(__instanceLock__); - if (CGSizeEqualToSize(_constrainedSize, layout.size) == NO) { - _constrainedSize = layout.size; - _renderer.constrainedSize = layout.size; + CGSize layoutSize = layout.size; + //Apply textContainerInset + layoutSize.width -= (_textContainerInset.left + _textContainerInset.right); + layoutSize.height -= (_textContainerInset.top + _textContainerInset.bottom); + + if (CGSizeEqualToSize(_constrainedSize, layoutSize) == NO) { + _constrainedSize = layoutSize; + _renderer.constrainedSize = layoutSize; } } } @@ -335,6 +372,10 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; ASDN::MutexLocker l(__instanceLock__); + //remove textContainerInset + constrainedSize.width -= (_textContainerInset.left + _textContainerInset.right); + constrainedSize.height -= (_textContainerInset.top + _textContainerInset.bottom); + _constrainedSize = constrainedSize; // Instead of invalidating the renderer, in case this is a new call with a different constrained size, @@ -353,6 +394,11 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; self.descender *= _renderer.currentScaleFactor; } } + + //add textContainerInset + size.width += (_textContainerInset.left + _textContainerInset.right); + size.height += (_textContainerInset.top + _textContainerInset.bottom); + return size; } @@ -466,6 +512,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; CGContextSaveGState(context); + CGContextTranslateCTM(context, _textContainerInset.left, _textContainerInset.top); + ASTextKitRenderer *renderer = [self _rendererWithBounds:drawParameterBounds]; UIEdgeInsets shadowPadding = [self shadowPaddingWithRenderer:renderer]; CGPoint boundsOrigin = drawParameterBounds.origin; diff --git a/AsyncDisplayKit/ASVideoNode.h b/AsyncDisplayKit/ASVideoNode.h index 039c2126..9223da3f 100644 --- a/AsyncDisplayKit/ASVideoNode.h +++ b/AsyncDisplayKit/ASVideoNode.h @@ -12,7 +12,7 @@ #import #import -@class AVAsset, AVPlayer, AVPlayerItem, AVVideoComposition, AVAudioMix; +@class AVAsset, AVPlayer, AVPlayerLayer, AVPlayerItem, AVVideoComposition, AVAudioMix; @protocol ASVideoNodeDelegate; typedef enum { @@ -51,6 +51,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nullable, nonatomic, strong, readwrite) AVAudioMix *audioMix; @property (nullable, nonatomic, strong, readonly) AVPlayer *player; +@property (nullable, nonatomic, strong, readonly) AVPlayerLayer *playerLayer; @property (nullable, nonatomic, strong, readonly) AVPlayerItem *currentItem; diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index e6f44993..008c4b94 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -37,6 +37,7 @@ static void *ASVideoNodeContext = &ASVideoNodeContext; static NSString * const kPlaybackLikelyToKeepUpKey = @"playbackLikelyToKeepUp"; static NSString * const kplaybackBufferEmpty = @"playbackBufferEmpty"; static NSString * const kStatus = @"status"; +static NSString * const kRate = @"rate"; @interface ASVideoNode () { @@ -208,6 +209,25 @@ static NSString * const kStatus = @"status"; [notificationCenter removeObserver:self name:AVPlayerItemNewErrorLogEntryNotification object:playerItem]; } +- (void)addPlayerObservers:(AVPlayer *)player +{ + if (player == nil) { + return; + } + + [player addObserver:self forKeyPath:kRate options:NSKeyValueObservingOptionNew context:ASVideoNodeContext]; +} + +- (void) removePlayerObservers:(AVPlayer *)player +{ + @try { + [player removeObserver:self forKeyPath:kRate context:ASVideoNodeContext]; + } + @catch (NSException * __unused exception) { + NSLog(@"Unnecessary KVO removal"); + } +} + - (void)layout { [super layout]; @@ -267,6 +287,7 @@ static NSString * const kStatus = @"status"; AVAssetImageGenerator *previewImageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset]; previewImageGenerator.appliesPreferredTrackTransform = YES; + previewImageGenerator.videoComposition = _videoComposition; [previewImageGenerator generateCGImagesAsynchronouslyForTimes:@[[NSValue valueWithCMTime:imageTime]] completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError *error) { @@ -291,29 +312,47 @@ static NSString * const kStatus = @"status"; { ASDN::MutexLocker l(__instanceLock__); - if (object != _currentPlayerItem) { - return; - } - - if ([keyPath isEqualToString:kStatus]) { - if ([change[NSKeyValueChangeNewKey] integerValue] == AVPlayerItemStatusReadyToPlay) { - self.playerState = ASVideoNodePlayerStateReadyToPlay; - // If we don't yet have a placeholder image update it now that we should have data available for it - if (self.image == nil && self.URL == nil) { - [self generatePlaceholderImage]; + if (object == _currentPlayerItem) { + if ([keyPath isEqualToString:kStatus]) { + if ([change[NSKeyValueChangeNewKey] integerValue] == AVPlayerItemStatusReadyToPlay) { + if (self.playerState != ASVideoNodePlayerStatePlaying) { + self.playerState = ASVideoNodePlayerStateReadyToPlay; + } + // If we don't yet have a placeholder image update it now that we should have data available for it + if (self.image == nil && self.URL == nil) { + [self generatePlaceholderImage]; + } + } + } else if ([keyPath isEqualToString:kPlaybackLikelyToKeepUpKey]) { + BOOL likelyToKeepUp = [change[NSKeyValueChangeNewKey] boolValue]; + if (likelyToKeepUp && self.playerState == ASVideoNodePlayerStatePlaying) { + return; + } + if (!likelyToKeepUp) { + self.playerState = ASVideoNodePlayerStateLoading; + } else if (self.playerState != ASVideoNodePlayerStateFinished) { + self.playerState = ASVideoNodePlayerStatePlaybackLikelyToKeepUpButNotPlaying; + } + if (_shouldBePlaying && (_shouldAggressivelyRecoverFromStall || likelyToKeepUp) && ASInterfaceStateIncludesVisible(self.interfaceState)) { + if (self.playerState == ASVideoNodePlayerStateLoading && _delegateFlags.delegateVideoNodeDidRecoverFromStall) { + [_delegate videoNodeDidRecoverFromStall:self]; + } + [self play]; // autoresume after buffer catches up + } + } else if ([keyPath isEqualToString:kplaybackBufferEmpty]) { + if (_shouldBePlaying && [change[NSKeyValueChangeNewKey] boolValue] == true && ASInterfaceStateIncludesVisible(self.interfaceState)) { + self.playerState = ASVideoNodePlayerStateLoading; } } - } else if ([keyPath isEqualToString:kPlaybackLikelyToKeepUpKey]) { - self.playerState = ASVideoNodePlayerStatePlaybackLikelyToKeepUpButNotPlaying; - if (_shouldBePlaying && (_shouldAggressivelyRecoverFromStall || [change[NSKeyValueChangeNewKey] boolValue]) && ASInterfaceStateIncludesVisible(self.interfaceState)) { - if (self.playerState == ASVideoNodePlayerStateLoading && _delegateFlags.delegateVideoNodeDidRecoverFromStall) { - [_delegate videoNodeDidRecoverFromStall:self]; + } else if (object == _player) { + if ([keyPath isEqualToString:kRate]) { + if ([change[NSKeyValueChangeNewKey] floatValue] == 0.0) { + if (self.playerState == ASVideoNodePlayerStatePlaying) { + self.playerState = ASVideoNodePlayerStatePaused; + } + } else { + self.playerState = ASVideoNodePlayerStatePlaying; } - [self play]; // autoresume after buffer catches up - } - } else if ([keyPath isEqualToString:kplaybackBufferEmpty]) { - if (_shouldBePlaying && [change[NSKeyValueChangeNewKey] boolValue] == true && ASInterfaceStateIncludesVisible(self.interfaceState)) { - self.playerState = ASVideoNodePlayerStateLoading; } } } @@ -345,14 +384,11 @@ static NSString * const kStatus = @"status"; ASDN::MutexLocker l(__instanceLock__); AVAsset *asset = self.asset; // Return immediately if the asset is nil; - if (asset == nil || self.playerState == ASVideoNodePlayerStateInitialLoading) { + if (asset == nil || self.playerState != ASVideoNodePlayerStateUnknown) { return; } - // FIXME: Nothing appears to prevent this method from sending the delegate notification / calling load on the asset - // multiple times, even after the asset is fully loaded and ready to play. There should probably be a playerState - // for NotLoaded or such, besides Unknown, so this can be easily checked before proceeding. - self.playerState = ASVideoNodePlayerStateInitialLoading; + self.playerState = ASVideoNodePlayerStateLoading; if (_delegateFlags.delegateVideoNodeDidStartInitialLoading) { [_delegate videoNodeDidStartInitialLoading:self]; } @@ -396,6 +432,7 @@ static NSString * const kStatus = @"status"; self.player = nil; self.currentItem = nil; + self.playerState = ASVideoNodePlayerStateUnknown; } } @@ -514,6 +551,12 @@ static NSString * const kStatus = @"status"; return _player; } +- (AVPlayerLayer *)playerLayer +{ + ASDN::MutexLocker l(__instanceLock__); + return (AVPlayerLayer *)_playerNode.layer; +} + - (id)delegate{ return _delegate; } @@ -603,12 +646,6 @@ static NSString * const kStatus = @"status"; [_player play]; _shouldBePlaying = YES; - - if (![self ready]) { - self.playerState = ASVideoNodePlayerStateLoading; - } else { - self.playerState = ASVideoNodePlayerStatePlaying; - } } - (BOOL)ready @@ -622,7 +659,6 @@ static NSString * const kStatus = @"status"; if (![self isStateChangeValid:ASVideoNodePlayerStatePaused]) { return; } - self.playerState = ASVideoNodePlayerStatePaused; [_player pause]; _shouldBePlaying = NO; } @@ -731,9 +767,16 @@ static NSString * const kStatus = @"status"; - (void)setPlayer:(AVPlayer *)player { ASDN::MutexLocker l(__instanceLock__); + + [self removePlayerObservers:_player]; + _player = player; player.muted = _muted; ((AVPlayerLayer *)_playerNode.layer).player = player; + + if (player != nil) { + [self addPlayerObservers:player]; + } } - (BOOL)shouldBePlaying @@ -755,6 +798,7 @@ static NSString * const kStatus = @"status"; [_player removeTimeObserver:_timeObserver]; _timeObserver = nil; [self removePlayerItemObservers:_currentPlayerItem]; + [self removePlayerObservers:_player]; } @end diff --git a/AsyncDisplayKit/ASViewController.h b/AsyncDisplayKit/ASViewController.h index 31ff3ed9..84bd14d6 100644 --- a/AsyncDisplayKit/ASViewController.h +++ b/AsyncDisplayKit/ASViewController.h @@ -78,7 +78,7 @@ typedef ASTraitCollection * _Nonnull (^ASDisplayTraitsForTraitWindowSizeBlock)(C - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil AS_UNAVAILABLE("ASViewController requires using -initWithNode:"); -- (instancetype)initWithCoder:(NSCoder *)aDecoder AS_UNAVAILABLE("ASViewController requires using -initWithNode:"); +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder AS_UNAVAILABLE("ASViewController requires using -initWithNode:"); @end diff --git a/AsyncDisplayKit/AsyncDisplayKit+Debug.h b/AsyncDisplayKit/AsyncDisplayKit+Debug.h index 09c20f2b..a85dda03 100644 --- a/AsyncDisplayKit/AsyncDisplayKit+Debug.h +++ b/AsyncDisplayKit/AsyncDisplayKit+Debug.h @@ -12,6 +12,7 @@ #import "ASControlNode.h" #import "ASImageNode.h" +#import "ASRangeController.h" @interface ASImageNode (Debugging) @@ -29,16 +30,40 @@ @interface ASControlNode (Debugging) /** - Class method to enable a visualization overlay of the tappable area on the ASControlNode. For app debugging purposes only. - NOTE: GESTURE RECOGNIZERS, (including tap gesture recognizers on a control node) WILL NOT BE VISUALIZED!!! - Overlay = translucent GREEN color, - edges that are clipped by the tappable area of any parent (their bounds + hitTestSlop) in the hierarchy = DARK GREEN BORDERED EDGE, - edges that are clipped by clipToBounds = YES of any parent in the hierarchy = ORANGE BORDERED EDGE (may still receive touches beyond - overlay rect, but can't be visualized). - @param enable Specify YES to make this debug feature enabled when messaging the ASControlNode class. + * Class method to enable a visualization overlay of the tappable area on the ASControlNode. For app debugging purposes only. + * NOTE: GESTURE RECOGNIZERS, (including tap gesture recognizers on a control node) WILL NOT BE VISUALIZED!!! + * Overlay = translucent GREEN color, + * edges that are clipped by the tappable area of any parent (their bounds + hitTestSlop) in the hierarchy = DARK GREEN BORDERED EDGE, + * edges that are clipped by clipToBounds = YES of any parent in the hierarchy = ORANGE BORDERED EDGE (may still receive touches beyond + * overlay rect, but can't be visualized). + * @param enable Specify YES to make this debug feature enabled when messaging the ASControlNode class. */ + (void)setEnableHitTestDebug:(BOOL)enable; + (BOOL)enableHitTestDebug; @end +@interface ASRangeController (Debugging) + +/** + * Class method to enable a visualization overlay of the all ASRangeController's tuning parameters. For dev purposes only. + * To use, message ASRangeController in the AppDelegate --> [ASRangeController setShouldShowRangeDebugOverlay:YES]; + * @param enable Specify YES to make this debug feature enabled when messaging the ASRangeController class. + */ ++ (void)setShouldShowRangeDebugOverlay:(BOOL)show; ++ (BOOL)shouldShowRangeDebugOverlay; + ++ (void)layoutDebugOverlayIfNeeded; + +- (void)addRangeControllerToRangeDebugOverlay; + +- (void)updateRangeController:(ASRangeController *)controller + withScrollableDirections:(ASScrollDirection)scrollableDirections + scrollDirection:(ASScrollDirection)direction + rangeMode:(ASLayoutRangeMode)mode + displayTuningParameters:(ASRangeTuningParameters)displayTuningParameters + fetchDataTuningParameters:(ASRangeTuningParameters)fetchDataTuningParameters + interfaceState:(ASInterfaceState)interfaceState; + +@end + diff --git a/AsyncDisplayKit/AsyncDisplayKit+Debug.m b/AsyncDisplayKit/AsyncDisplayKit+Debug.m index 86d77e0f..e456ce72 100644 --- a/AsyncDisplayKit/AsyncDisplayKit+Debug.m +++ b/AsyncDisplayKit/AsyncDisplayKit+Debug.m @@ -13,6 +13,16 @@ #import "AsyncDisplayKit+Debug.h" #import "ASDisplayNode+Subclasses.h" #import "ASDisplayNodeExtras.h" +#import "ASWeakSet.h" +#import "UIImage+ASConvenience.h" +#import +#import +#import +#import +#import + + +#pragma mark - ASImageNode (Debugging) static BOOL __shouldShowImageScalingOverlay = NO; @@ -30,6 +40,8 @@ static BOOL __shouldShowImageScalingOverlay = NO; @end +#pragma mark - ASControlNode (DebuggingInternal) + static BOOL __enableHitTestDebug = NO; @interface ASControlNode (DebuggingInternal) @@ -191,3 +203,551 @@ static BOOL __enableHitTestDebug = NO; } @end + +#pragma mark - ASRangeController (Debugging) + +@interface _ASRangeDebugOverlayView : UIView + ++ (instancetype)sharedInstance; + +- (void)addRangeController:(ASRangeController *)rangeController; + +- (void)updateRangeController:(ASRangeController *)controller + withScrollableDirections:(ASScrollDirection)scrollableDirections + scrollDirection:(ASScrollDirection)direction + rangeMode:(ASLayoutRangeMode)mode + displayTuningParameters:(ASRangeTuningParameters)displayTuningParameters + fetchDataTuningParameters:(ASRangeTuningParameters)fetchDataTuningParameters + interfaceState:(ASInterfaceState)interfaceState; + +@end + +@interface _ASRangeDebugBarView : UIView + +@property (nonatomic, weak) ASRangeController *rangeController; +@property (nonatomic, assign) BOOL destroyOnLayout; +@property (nonatomic, strong) NSString *debugString; + +- (instancetype)initWithRangeController:(ASRangeController *)rangeController; + +- (void)updateWithVisibleRatio:(CGFloat)visibleRatio + displayRatio:(CGFloat)displayRatio + leadingDisplayRatio:(CGFloat)leadingDisplayRatio + fetchDataRatio:(CGFloat)fetchDataRatio + leadingFetchDataRatio:(CGFloat)leadingFetchDataRatio + direction:(ASScrollDirection)direction; + +@end + +static BOOL __shouldShowRangeDebugOverlay = NO; + +@implementation ASRangeController (Debugging) + ++ (void)setShouldShowRangeDebugOverlay:(BOOL)show +{ + __shouldShowRangeDebugOverlay = show; +} + ++ (BOOL)shouldShowRangeDebugOverlay +{ + return __shouldShowRangeDebugOverlay; +} + ++ (void)layoutDebugOverlayIfNeeded +{ + [[_ASRangeDebugOverlayView sharedInstance] setNeedsLayout]; +} + +- (void)addRangeControllerToRangeDebugOverlay +{ + [[_ASRangeDebugOverlayView sharedInstance] addRangeController:self]; +} + +- (void)updateRangeController:(ASRangeController *)controller + withScrollableDirections:(ASScrollDirection)scrollableDirections + scrollDirection:(ASScrollDirection)direction + rangeMode:(ASLayoutRangeMode)mode + displayTuningParameters:(ASRangeTuningParameters)displayTuningParameters + fetchDataTuningParameters:(ASRangeTuningParameters)fetchDataTuningParameters + interfaceState:(ASInterfaceState)interfaceState +{ + [[_ASRangeDebugOverlayView sharedInstance] updateRangeController:controller + withScrollableDirections:scrollableDirections + scrollDirection:direction + rangeMode:mode + displayTuningParameters:displayTuningParameters + fetchDataTuningParameters:fetchDataTuningParameters + interfaceState:interfaceState]; +} + +@end + + +#pragma mark _ASRangeDebugOverlayView + +@interface _ASRangeDebugOverlayView () +@end + +@implementation _ASRangeDebugOverlayView +{ + NSMutableArray *_rangeControllerViews; + NSInteger _newControllerCount; + NSInteger _removeControllerCount; + BOOL _animating; +} + ++ (UIWindow *)keyWindow +{ + // hack to work around app extensions not having UIApplication...not sure of a better way to do this? + return [[NSClassFromString(@"UIApplication") sharedApplication] keyWindow]; +} + ++ (instancetype)sharedInstance +{ + static _ASRangeDebugOverlayView *__rangeDebugOverlay = nil; + + if (!__rangeDebugOverlay && [ASRangeController shouldShowRangeDebugOverlay]) { + __rangeDebugOverlay = [[self alloc] initWithFrame:CGRectZero]; + [[self keyWindow] addSubview:__rangeDebugOverlay]; + } + + return __rangeDebugOverlay; +} + +#define OVERLAY_INSET 10 +#define OVERLAY_SCALE 3 +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + + if (self) { + _rangeControllerViews = [[NSMutableArray alloc] init]; + self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.4]; + self.layer.zPosition = 1000; + self.clipsToBounds = YES; + + CGSize windowSize = [[[self class] keyWindow] bounds].size; + self.frame = CGRectMake(windowSize.width - (windowSize.width / OVERLAY_SCALE) - OVERLAY_INSET, windowSize.height - OVERLAY_INSET, + windowSize.width / OVERLAY_SCALE, 0.0); + + UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(rangeDebugOverlayWasPanned:)]; + [self addGestureRecognizer:panGR]; + } + + return self; +} + +#define BAR_THICKNESS 24 + +- (void)layoutSubviews +{ + [super layoutSubviews]; + [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ + [self layoutToFitAllBarsExcept:0]; + } completion:^(BOOL finished) { + + }]; +} + +- (void)layoutToFitAllBarsExcept:(NSInteger)barsToClip +{ + CGSize boundsSize = self.bounds.size; + CGFloat totalHeight = 0.0; + + CGRect barRect = CGRectMake(0, boundsSize.height - BAR_THICKNESS, self.bounds.size.width, BAR_THICKNESS); + NSMutableArray *displayedBars = [NSMutableArray array]; + + for (_ASRangeDebugBarView *barView in [_rangeControllerViews copy]) { + barView.frame = barRect; + + ASInterfaceState interfaceState = [barView.rangeController.dataSource interfaceStateForRangeController:barView.rangeController]; + + if (!(interfaceState & (ASInterfaceStateVisible))) { + if (barView.destroyOnLayout && barView.alpha == 0.0) { + [_rangeControllerViews removeObjectIdenticalTo:barView]; + [barView removeFromSuperview]; + } else { + barView.alpha = 0.0; + } + } else { + assert(!barView.destroyOnLayout); // In this case we should not have a visible interfaceState + barView.alpha = 1.0; + totalHeight += BAR_THICKNESS; + barRect.origin.y -= BAR_THICKNESS; + [displayedBars addObject:barView]; + } + } + + if (totalHeight > 0) { + totalHeight -= (BAR_THICKNESS * barsToClip); + } + + if (barsToClip == 0) { + CGRect overlayFrame = self.frame; + CGFloat heightChange = (overlayFrame.size.height - totalHeight); + + overlayFrame.origin.y += heightChange; + overlayFrame.size.height = totalHeight; + self.frame = overlayFrame; + + for (_ASRangeDebugBarView *barView in displayedBars) { + [self offsetYOrigin:-heightChange forView:barView]; + } + } +} + +- (void)setOrigin:(CGPoint)origin forView:(UIView *)view +{ + CGRect newFrame = view.frame; + newFrame.origin = origin; + view.frame = newFrame; +} + +- (void)offsetYOrigin:(CGFloat)offset forView:(UIView *)view +{ + CGRect newFrame = view.frame; + newFrame.origin = CGPointMake(newFrame.origin.x, newFrame.origin.y + offset); + view.frame = newFrame; +} + +- (void)addRangeController:(ASRangeController *)rangeController +{ + for (_ASRangeDebugBarView *rangeView in _rangeControllerViews) { + if (rangeView.rangeController == rangeController) { + return; + } + } + _ASRangeDebugBarView *rangeView = [[_ASRangeDebugBarView alloc] initWithRangeController:rangeController]; + [_rangeControllerViews addObject:rangeView]; + [self addSubview:rangeView]; + + if (!_animating) { + [self layoutToFitAllBarsExcept:1]; + } + + [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ + _animating = YES; + [self layoutToFitAllBarsExcept:0]; + } completion:^(BOOL finished) { + _animating = NO; + }]; +} + +- (void)updateRangeController:(ASRangeController *)controller + withScrollableDirections:(ASScrollDirection)scrollableDirections + scrollDirection:(ASScrollDirection)scrollDirection + rangeMode:(ASLayoutRangeMode)rangeMode + displayTuningParameters:(ASRangeTuningParameters)displayTuningParameters + fetchDataTuningParameters:(ASRangeTuningParameters)fetchDataTuningParameters + interfaceState:(ASInterfaceState)interfaceState; +{ + _ASRangeDebugBarView *viewToUpdate = [self barViewForRangeController:controller]; + + CGRect boundsRect = self.bounds; + CGRect visibleRect = CGRectExpandToRangeWithScrollableDirections(boundsRect, ASRangeTuningParametersZero, scrollableDirections, scrollDirection); + CGRect displayRect = CGRectExpandToRangeWithScrollableDirections(boundsRect, displayTuningParameters, scrollableDirections, scrollDirection); + CGRect fetchDataRect = CGRectExpandToRangeWithScrollableDirections(boundsRect, fetchDataTuningParameters, scrollableDirections, scrollDirection); + + // figure out which is biggest and assume that is full bounds + BOOL displayRangeLargerThanFetch = NO; + CGFloat visibleRatio = 0; + CGFloat displayRatio = 0; + CGFloat fetchDataRatio = 0; + CGFloat leadingDisplayTuningRatio = 0; + CGFloat leadingFetchDataTuningRatio = 0; + + if (!((displayTuningParameters.leadingBufferScreenfuls + displayTuningParameters.trailingBufferScreenfuls) == 0)) { + leadingDisplayTuningRatio = displayTuningParameters.leadingBufferScreenfuls / (displayTuningParameters.leadingBufferScreenfuls + displayTuningParameters.trailingBufferScreenfuls); + } + if (!((fetchDataTuningParameters.leadingBufferScreenfuls + fetchDataTuningParameters.trailingBufferScreenfuls) == 0)) { + leadingFetchDataTuningRatio = fetchDataTuningParameters.leadingBufferScreenfuls / (fetchDataTuningParameters.leadingBufferScreenfuls + fetchDataTuningParameters.trailingBufferScreenfuls); + } + + if (ASScrollDirectionContainsVerticalDirection(scrollDirection)) { + + if (displayRect.size.height >= fetchDataRect.size.height) { + displayRangeLargerThanFetch = YES; + } else { + displayRangeLargerThanFetch = NO; + } + + if (displayRangeLargerThanFetch) { + visibleRatio = visibleRect.size.height / displayRect.size.height; + displayRatio = 1.0; + fetchDataRatio = fetchDataRect.size.height / displayRect.size.height; + } else { + visibleRatio = visibleRect.size.height / fetchDataRect.size.height; + displayRatio = displayRect.size.height / fetchDataRect.size.height; + fetchDataRatio = 1.0; + } + + } else { + + if (displayRect.size.width >= fetchDataRect.size.width) { + displayRangeLargerThanFetch = YES; + } else { + displayRangeLargerThanFetch = NO; + } + + if (displayRangeLargerThanFetch) { + visibleRatio = visibleRect.size.width / displayRect.size.width; + displayRatio = 1.0; + fetchDataRatio = fetchDataRect.size.width / displayRect.size.width; + } else { + visibleRatio = visibleRect.size.width / fetchDataRect.size.width; + displayRatio = displayRect.size.width / fetchDataRect.size.width; + fetchDataRatio = 1.0; + } + } + + [viewToUpdate updateWithVisibleRatio:visibleRatio + displayRatio:displayRatio + leadingDisplayRatio:leadingDisplayTuningRatio + fetchDataRatio:fetchDataRatio + leadingFetchDataRatio:leadingFetchDataTuningRatio + direction:scrollDirection]; + + [self setNeedsLayout]; +} + +- (_ASRangeDebugBarView *)barViewForRangeController:(ASRangeController *)controller +{ + _ASRangeDebugBarView *rangeControllerBarView = nil; + + for (_ASRangeDebugBarView *rangeView in [[_rangeControllerViews reverseObjectEnumerator] allObjects]) { + // remove barView if its rangeController has been deleted + if (!rangeView.rangeController) { + rangeView.destroyOnLayout = YES; + [self setNeedsLayout]; + } + ASInterfaceState interfaceState = [rangeView.rangeController.dataSource interfaceStateForRangeController:rangeView.rangeController]; + if (!(interfaceState & (ASInterfaceStateVisible | ASInterfaceStateDisplay))) { + [self setNeedsLayout]; + } + + if ([rangeView.rangeController isEqual:controller]) { + rangeControllerBarView = rangeView; + } + } + + return rangeControllerBarView; +} + +#define MIN_VISIBLE_INSET 40 +- (void)rangeDebugOverlayWasPanned:(UIPanGestureRecognizer *)recognizer +{ + CGPoint translation = [recognizer translationInView:recognizer.view]; + CGFloat newCenterX = recognizer.view.center.x + translation.x; + CGFloat newCenterY = recognizer.view.center.y + translation.y; + CGSize boundsSize = recognizer.view.bounds.size; + CGSize superBoundsSize = recognizer.view.superview.bounds.size; + CGFloat minAllowableX = -boundsSize.width / 2.0 + MIN_VISIBLE_INSET; + CGFloat maxAllowableX = superBoundsSize.width + boundsSize.width / 2.0 - MIN_VISIBLE_INSET; + + if (newCenterX > maxAllowableX) { + newCenterX = maxAllowableX; + } else if (newCenterX < minAllowableX) { + newCenterX = minAllowableX; + } + + CGFloat minAllowableY = -boundsSize.height / 2.0 + MIN_VISIBLE_INSET; + CGFloat maxAllowableY = superBoundsSize.height + boundsSize.height / 2.0 - MIN_VISIBLE_INSET; + + if (newCenterY > maxAllowableY) { + newCenterY = maxAllowableY; + } else if (newCenterY < minAllowableY) { + newCenterY = minAllowableY; + } + + recognizer.view.center = CGPointMake(newCenterX, newCenterY); + [recognizer setTranslation:CGPointMake(0, 0) inView:recognizer.view]; +} + +@end + +#pragma mark _ASRangeDebugBarView + +@implementation _ASRangeDebugBarView +{ + ASTextNode *_debugText; + ASTextNode *_leftDebugText; + ASTextNode *_rightDebugText; + ASImageNode *_visibleRect; + ASImageNode *_displayRect; + ASImageNode *_fetchDataRect; + CGFloat _visibleRatio; + CGFloat _displayRatio; + CGFloat _fetchDataRatio; + CGFloat _leadingDisplayRatio; + CGFloat _leadingFetchDataRatio; + ASScrollDirection _scrollDirection; + BOOL _firstLayoutOfRects; +} + +- (instancetype)initWithRangeController:(ASRangeController *)rangeController +{ + self = [super initWithFrame:CGRectZero]; + if (self) { + _firstLayoutOfRects = YES; + _rangeController = rangeController; + _debugText = [self createDebugTextNode]; + _leftDebugText = [self createDebugTextNode]; + _rightDebugText = [self createDebugTextNode]; + _fetchDataRect = [self createRangeNodeWithColor:[UIColor orangeColor]]; + _displayRect = [self createRangeNodeWithColor:[UIColor yellowColor]]; + _visibleRect = [self createRangeNodeWithColor:[UIColor greenColor]]; + } + + return self; +} + +#define HORIZONTAL_INSET 10 +- (void)layoutSubviews +{ + [super layoutSubviews]; + + CGSize boundsSize = self.bounds.size; + CGFloat subCellHeight = 9.0; + [self setBarDebugLabelsWithSize:subCellHeight]; + [self setBarSubviewOrder]; + + CGRect rect = CGRectIntegral(CGRectMake(0, 0, boundsSize.width, floorf(boundsSize.height / 2.0))); + rect.size = [_debugText measure:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)]; + rect.origin.x = (boundsSize.width - rect.size.width) / 2.0; + _debugText.frame = rect; + rect.origin.y += rect.size.height; + + rect.origin.x = 0; + rect.size = CGSizeMake(HORIZONTAL_INSET, boundsSize.height / 2.0); + _leftDebugText.frame = rect; + + rect.origin.x = boundsSize.width - HORIZONTAL_INSET; + _rightDebugText.frame = rect; + + CGFloat visibleDimension = (boundsSize.width - 2 * HORIZONTAL_INSET) * _visibleRatio; + CGFloat displayDimension = (boundsSize.width - 2 * HORIZONTAL_INSET) * _displayRatio; + CGFloat fetchDataDimension = (boundsSize.width - 2 * HORIZONTAL_INSET) * _fetchDataRatio; + CGFloat visiblePoint = 0; + CGFloat displayPoint = 0; + CGFloat fetchDataPoint = 0; + + BOOL displayLargerThanFetchData = (_displayRatio == 1.0) ? YES : NO; + + if (ASScrollDirectionContainsLeft(_scrollDirection) || ASScrollDirectionContainsUp(_scrollDirection)) { + + if (displayLargerThanFetchData) { + visiblePoint = (displayDimension - visibleDimension) * _leadingDisplayRatio; + fetchDataPoint = visiblePoint - (fetchDataDimension - visibleDimension) * _leadingFetchDataRatio; + } else { + visiblePoint = (fetchDataDimension - visibleDimension) * _leadingFetchDataRatio; + displayPoint = visiblePoint - (displayDimension - visibleDimension) * _leadingDisplayRatio; + } + } else if (ASScrollDirectionContainsRight(_scrollDirection) || ASScrollDirectionContainsDown(_scrollDirection)) { + + if (displayLargerThanFetchData) { + visiblePoint = (displayDimension - visibleDimension) * (1 - _leadingDisplayRatio); + fetchDataPoint = visiblePoint - (fetchDataDimension - visibleDimension) * (1 - _leadingFetchDataRatio); + } else { + visiblePoint = (fetchDataDimension - visibleDimension) * (1 - _leadingFetchDataRatio); + displayPoint = visiblePoint - (displayDimension - visibleDimension) * (1 - _leadingDisplayRatio); + } + } + + BOOL animate = !_firstLayoutOfRects; + [UIView animateWithDuration:animate ? 0.3 : 0.0 delay:0.0 options:UIViewAnimationOptionLayoutSubviews animations:^{ + _visibleRect.frame = CGRectMake(HORIZONTAL_INSET + visiblePoint, rect.origin.y, visibleDimension, subCellHeight); + _displayRect.frame = CGRectMake(HORIZONTAL_INSET + displayPoint, rect.origin.y, displayDimension, subCellHeight); + _fetchDataRect.frame = CGRectMake(HORIZONTAL_INSET + fetchDataPoint, rect.origin.y, fetchDataDimension, subCellHeight); + } completion:^(BOOL finished) {}]; + + if (!animate) { + _visibleRect.alpha = _displayRect.alpha = _fetchDataRect.alpha = 0; + [UIView animateWithDuration:0.3 animations:^{ + _visibleRect.alpha = _displayRect.alpha = _fetchDataRect.alpha = 1; + }]; + } + + _firstLayoutOfRects = NO; +} + +- (void)updateWithVisibleRatio:(CGFloat)visibleRatio + displayRatio:(CGFloat)displayRatio + leadingDisplayRatio:(CGFloat)leadingDisplayRatio + fetchDataRatio:(CGFloat)fetchDataRatio + leadingFetchDataRatio:(CGFloat)leadingFetchDataRatio + direction:(ASScrollDirection)scrollDirection +{ + _visibleRatio = visibleRatio; + _displayRatio = displayRatio; + _leadingDisplayRatio = leadingDisplayRatio; + _fetchDataRatio = fetchDataRatio; + _leadingFetchDataRatio = leadingFetchDataRatio; + _scrollDirection = scrollDirection; + + [self setNeedsLayout]; +} + +- (void)setBarSubviewOrder +{ + if (_fetchDataRatio == 1.0) { + [self sendSubviewToBack:_fetchDataRect.view]; + } else { + [self sendSubviewToBack:_displayRect.view]; + } + + [self bringSubviewToFront:_visibleRect.view]; +} + +- (void)setBarDebugLabelsWithSize:(CGFloat)size +{ + if (!_debugString) { + _debugString = [[_rangeController dataSource] nameForRangeControllerDataSource]; + } + if (_debugString) { + _debugText.attributedString = [_ASRangeDebugBarView whiteAttributedStringFromString:_debugString withSize:size]; + } + + if (ASScrollDirectionContainsVerticalDirection(_scrollDirection)) { + _leftDebugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:@"▲" withSize:size]; + _rightDebugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:@"▼" withSize:size]; + } else if (ASScrollDirectionContainsHorizontalDirection(_scrollDirection)) { + _leftDebugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:@"◀︎" withSize:size]; + _rightDebugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:@"▶︎" withSize:size]; + } + + _leftDebugText.hidden = (_scrollDirection != ASScrollDirectionLeft && _scrollDirection != ASScrollDirectionUp); + _rightDebugText.hidden = (_scrollDirection != ASScrollDirectionRight && _scrollDirection != ASScrollDirectionDown); +} + +- (ASTextNode *)createDebugTextNode +{ + ASTextNode *label = [[ASTextNode alloc] init]; + [self addSubnode:label]; + return label; +} + +#define RANGE_BAR_CORNER_RADIUS 3 +#define RANGE_BAR_BORDER_WIDTH 1 +- (ASImageNode *)createRangeNodeWithColor:(UIColor *)color +{ + ASImageNode *rangeBarImageNode = [[ASImageNode alloc] init]; + rangeBarImageNode.image = [UIImage as_resizableRoundedImageWithCornerRadius:RANGE_BAR_CORNER_RADIUS + cornerColor:[UIColor clearColor] + fillColor:[color colorWithAlphaComponent:0.5] + borderColor:[[UIColor blackColor] colorWithAlphaComponent:0.9] + borderWidth:RANGE_BAR_BORDER_WIDTH + roundedCorners:UIRectCornerAllCorners + scale:[[UIScreen mainScreen] scale]]; + [self addSubnode:rangeBarImageNode]; + + return rangeBarImageNode; +} + ++ (NSAttributedString *)whiteAttributedStringFromString:(NSString *)string withSize:(CGFloat)size +{ + NSDictionary *attributes = @{NSForegroundColorAttributeName : [UIColor whiteColor], + NSFontAttributeName : [UIFont systemFontOfSize:size]}; + return [[NSAttributedString alloc] initWithString:string attributes:attributes]; +} + +@end diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index f738b55e..e079f74b 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -49,8 +49,7 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; std::vector _itemCountsFromDataSource; // Main thread only. ASMainSerialQueue *_mainSerialQueue; - - NSMutableArray *_pendingEditCommandBlocks; // To be run on the main thread. Handles begin/endUpdates tracking. + dispatch_queue_t _editingTransactionQueue; // Serial background queue. Dispatches concurrent layout and manages _editingNodes. dispatch_group_t _editingTransactionGroup; // Group of all edit transaction blocks. Useful for waiting. @@ -62,8 +61,6 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; BOOL _delegateDidDeleteSections; } -@property (nonatomic, assign) NSUInteger batchUpdateCounter; - @end @implementation ASDataController @@ -87,15 +84,11 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; _mainSerialQueue = [[ASMainSerialQueue alloc] init]; - _pendingEditCommandBlocks = [NSMutableArray array]; - const char *queueName = [[NSString stringWithFormat:@"org.AsyncDisplayKit.ASDataController.editingTransactionQueue:%p", self] cStringUsingEncoding:NSASCIIStringEncoding]; _editingTransactionQueue = dispatch_queue_create(queueName, DISPATCH_QUEUE_SERIAL); dispatch_queue_set_specific(_editingTransactionQueue, &kASDataControllerEditingQueueKey, &kASDataControllerEditingQueueContext, NULL); _editingTransactionGroup = dispatch_group_create(); - _batchUpdateCounter = 0; - return self; } @@ -395,60 +388,55 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; - (void)_reloadDataWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions synchronously:(BOOL)synchronously completion:(void (^)())completion { + ASDisplayNodeAssertMainThread(); + _initialReloadDataHasBeenCalled = YES; - [self performEditCommandWithBlock:^{ - ASDisplayNodeAssertMainThread(); - dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); - [self invalidateDataSourceItemCounts]; - NSUInteger sectionCount = [self itemCountsFromDataSource].size(); - NSIndexSet *sectionIndexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)]; - NSArray *contexts = [self _populateFromDataSourceWithSectionIndexSet:sectionIndexSet]; + [self invalidateDataSourceItemCounts]; + NSUInteger sectionCount = [self itemCountsFromDataSource].size(); + NSIndexSet *sectionIndexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)]; + NSArray *contexts = [self _populateFromDataSourceWithSectionIndexSet:sectionIndexSet]; + + // Allow subclasses to perform setup before going into the edit transaction + [self prepareForReloadData]; + + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + LOG(@"Edit Transaction - reloadData"); - // Allow subclasses to perform setup before going into the edit transaction - [self prepareForReloadData]; + // Remove everything that existed before the reload, now that we're ready to insert replacements + NSMutableArray *editingNodes = _editingNodes[ASDataControllerRowNodeKind]; + NSUInteger editingNodesSectionCount = editingNodes.count; - dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ - 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) { - 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 _batchLayoutAndInsertNodesFromContexts:contexts withAnimationOptions:animationOptions]; - - if (completion) { - [_mainSerialQueue performBlockOnMainThread:completion]; - } - }); - if (synchronously) { - [self waitUntilAllUpdatesAreCommitted]; + 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 _batchLayoutAndInsertNodesFromContexts:contexts withAnimationOptions:animationOptions]; + + if (completion) { + [_mainSerialQueue performBlockOnMainThread:completion]; + } + }); + if (synchronously) { + [self waitUntilAllUpdatesAreCommitted]; + } } - (void)waitUntilAllUpdatesAreCommitted { ASDisplayNodeAssertMainThread(); - ASDisplayNodeAssert(_batchUpdateCounter == 0, @"Should not be called between beginUpdate or endUpdate"); - - // This should never be called in a batch update, return immediately therefore - if (_batchUpdateCounter > 0) { return; } dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); @@ -516,10 +504,20 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; - (void)beginUpdates { + ASDisplayNodeAssertMainThread(); + // TODO: make this -waitUntilAllUpdatesAreCommitted? dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); - // Begin queuing up edit calls that happen on the main thread. - // This will prevent further operations from being scheduled on _editingTransactionQueue. - _batchUpdateCounter++; + + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + [_mainSerialQueue performBlockOnMainThread:^{ + // Deep copy _completedNodes to _externalCompletedNodes. + // Any external queries from now on will be done on _externalCompletedNodes, to guarantee data consistency with the delegate. + _externalCompletedNodes = ASTwoDimensionalArrayDeepMutableCopy(_completedNodes[ASDataControllerRowNodeKind]); + + LOG(@"beginUpdates - begin updates call to delegate"); + [_delegate dataControllerBeginUpdates:self]; + }]; + }); } - (void)endUpdates @@ -529,118 +527,72 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; - (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { - _batchUpdateCounter--; + LOG(@"endUpdatesWithCompletion - beginning"); + ASDisplayNodeAssertMainThread(); - if (_batchUpdateCounter == 0) { - LOG(@"endUpdatesWithCompletion - beginning"); - - dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ - [_mainSerialQueue performBlockOnMainThread:^{ - // Deep copy _completedNodes to _externalCompletedNodes. - // Any external queries from now on will be done on _externalCompletedNodes, to guarantee data consistency with the delegate. - _externalCompletedNodes = ASTwoDimensionalArrayDeepMutableCopy(_completedNodes[ASDataControllerRowNodeKind]); - - LOG(@"endUpdatesWithCompletion - begin updates call to delegate"); - [_delegate dataControllerBeginUpdates:self]; - }]; - }); - - // Running these commands may result in blocking on an _editingTransactionQueue operation that started even before -beginUpdates. - // Each subsequent command in the queue will also wait on the full asynchronous completion of the prior command's edit transaction. - LOG(@"endUpdatesWithCompletion - %zd blocks to run", _pendingEditCommandBlocks.count); - NSUInteger i = 0; - for (dispatch_block_t block in _pendingEditCommandBlocks) { - LOG(@"endUpdatesWithCompletion - running block #%zd", i); - block(); - i += 1; - } - [_pendingEditCommandBlocks removeAllObjects]; - dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ - [_mainSerialQueue performBlockOnMainThread:^{ - // 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]; - }]; - }); - } -} - -/** - * Queues the given operation until an `endUpdates` synchronize update is completed. - * - * If this method is called outside of a begin/endUpdates batch update, the block is - * executed immediately. - */ -- (void)performEditCommandWithBlock:(void (^)(void))block -{ - // This method needs to block the thread and synchronously perform the operation if we are not - // queuing commands for begin/endUpdates. If we are queuing, it needs to return immediately. - if (!_initialReloadDataHasBeenCalled) { - return; - } - - if (block == nil) { - return; - } - - // If we have never performed a reload, there is no value in executing edit operations as the initial - // reload will directly re-query the latest state of the datasource - so completely skip the block in this case. - if (_batchUpdateCounter == 0) { - block(); - } else { - [_pendingEditCommandBlocks addObject:block]; - } + // Running these commands may result in blocking on an _editingTransactionQueue operation that started even before -beginUpdates. + // Each subsequent command in the queue will also wait on the full asynchronous completion of the prior command's edit transaction. + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + [_mainSerialQueue performBlockOnMainThread:^{ + // 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]; + }]; + }); } #pragma mark - Section Editing (External API) - (void)insertSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - [self performEditCommandWithBlock:^{ - ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssertMainThread(); + LOG(@"Edit Command - insertSections: %@", sections); + if (!_initialReloadDataHasBeenCalled) { + return; + } - LOG(@"Edit Command - insertSections: %@", sections); - dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + + NSArray *contexts = [self _populateFromDataSourceWithSectionIndexSet:sections]; + + [self prepareForInsertSections:sections]; + + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + [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]; - NSArray *contexts = [self _populateFromDataSourceWithSectionIndexSet:sections]; - - [self prepareForInsertSections:sections]; - - dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ - [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 _batchLayoutAndInsertNodesFromContexts:contexts withAnimationOptions:animationOptions]; - }); - }]; + [self _batchLayoutAndInsertNodesFromContexts:contexts withAnimationOptions:animationOptions]; + }); } - (void)deleteSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - [self performEditCommandWithBlock:^{ - ASDisplayNodeAssertMainThread(); - LOG(@"Edit Command - deleteSections: %@", sections); - dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); - dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ - [self willDeleteSections:sections]; + ASDisplayNodeAssertMainThread(); + LOG(@"Edit Command - deleteSections: %@", sections); + if (!_initialReloadDataHasBeenCalled) { + return; + } - // remove elements - LOG(@"Edit Transaction - deleteSections: %@", sections); - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], sections); - - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - [self _deleteSectionsAtIndexSet:sections withAnimationOptions:animationOptions]; - }); - }]; + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + [self willDeleteSections:sections]; + + // remove elements + LOG(@"Edit Transaction - deleteSections: %@", sections); + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], sections); + + [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [self _deleteSectionsAtIndexSet:sections withAnimationOptions:animationOptions]; + }); } - (void)reloadSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions @@ -650,33 +602,35 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - [self performEditCommandWithBlock:^{ - ASDisplayNodeAssertMainThread(); - LOG(@"Edit Command - moveSection"); + ASDisplayNodeAssertMainThread(); + LOG(@"Edit Command - moveSection"); - dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); - dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ - [self willMoveSection:section toSection:newSection]; + if (!_initialReloadDataHasBeenCalled) { + return; + } - // remove elements - - LOG(@"Edit Transaction - moveSection"); - NSMutableArray *editingRows = _editingNodes[ASDataControllerRowNodeKind]; - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(editingRows, [NSIndexSet indexSetWithIndex:section]); - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(editingRows, indexPaths); - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + [self willMoveSection:section toSection:newSection]; - // update the section of indexpaths - NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; - for (NSIndexPath *indexPath in indexPaths) { - NSIndexPath *updatedIndexPath = [NSIndexPath indexPathForItem:indexPath.item inSection:newSection]; - [updatedIndexPaths addObject:updatedIndexPath]; - } + // remove elements + + LOG(@"Edit Transaction - moveSection"); + NSMutableArray *editingRows = _editingNodes[ASDataControllerRowNodeKind]; + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(editingRows, [NSIndexSet indexSetWithIndex:section]); + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(editingRows, indexPaths); + [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - // Don't re-calculate size for moving - [self _insertNodes:nodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; - }); - }]; + // update the section of indexpaths + NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; + for (NSIndexPath *indexPath in indexPaths) { + NSIndexPath *updatedIndexPath = [NSIndexPath indexPathForItem:indexPath.item inSection:newSection]; + [updatedIndexPaths addObject:updatedIndexPath]; + } + + // Don't re-calculate size for moving + [self _insertNodes:nodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; + }); } @@ -736,59 +690,64 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - [self performEditCommandWithBlock:^{ - ASDisplayNodeAssertMainThread(); - LOG(@"Edit Command - insertRows: %@", indexPaths); - dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + ASDisplayNodeAssertMainThread(); + if (!_initialReloadDataHasBeenCalled) { + return; + } - // Sort indexPath to avoid messing up the index when inserting in several batches - NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; - NSMutableArray *contexts = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; + LOG(@"Edit Command - insertRows: %@", indexPaths); + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); - id 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]]; - } + // Sort indexPath to avoid messing up the index when inserting in several batches + NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; + NSMutableArray *contexts = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; - [self prepareForInsertRowsAtIndexPaths:indexPaths]; + id 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]]; + } - dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ - [self willInsertRowsAtIndexPaths:indexPaths]; + [self prepareForInsertRowsAtIndexPaths:indexPaths]; - LOG(@"Edit Transaction - insertRows: %@", indexPaths); - [self _batchLayoutAndInsertNodesFromContexts:contexts withAnimationOptions:animationOptions]; - }); - }]; + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + [self willInsertRowsAtIndexPaths:indexPaths]; + + LOG(@"Edit Transaction - insertRows: %@", indexPaths); + [self _batchLayoutAndInsertNodesFromContexts:contexts withAnimationOptions:animationOptions]; + }); } - (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - [self performEditCommandWithBlock:^{ - ASDisplayNodeAssertMainThread(); - LOG(@"Edit Command - deleteRows: %@", indexPaths); + ASDisplayNodeAssertMainThread(); - dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); - - // Sort indexPath in order to avoid messing up the index when deleting in several batches. - // FIXME: Shouldn't deletes be sorted in descending order? - NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; + if (!_initialReloadDataHasBeenCalled) { + return; + } - [self prepareForDeleteRowsAtIndexPaths:sortedIndexPaths]; + LOG(@"Edit Command - deleteRows: %@", indexPaths); - dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ - [self willDeleteRowsAtIndexPaths:sortedIndexPaths]; + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + + // Sort indexPath in order to avoid messing up the index when deleting in several batches. + // FIXME: Shouldn't deletes be sorted in descending order? + NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; - LOG(@"Edit Transaction - deleteRows: %@", indexPaths); - [self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions]; - }); - }]; + [self prepareForDeleteRowsAtIndexPaths:sortedIndexPaths]; + + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + [self willDeleteRowsAtIndexPaths:sortedIndexPaths]; + + LOG(@"Edit Transaction - deleteRows: %@", indexPaths); + [self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions]; + }); } - (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions @@ -798,22 +757,24 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; - (void)relayoutAllNodes { - [self performEditCommandWithBlock:^{ - ASDisplayNodeAssertMainThread(); - LOG(@"Edit Command - relayoutRows"); - dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + ASDisplayNodeAssertMainThread(); + if (!_initialReloadDataHasBeenCalled) { + return; + } - // Can't relayout right away because _completedNodes may not be up-to-date, - // i.e there might be some nodes that were measured using the old constrained size but haven't been added to _completedNodes - // (see _layoutNodes:atIndexPaths:withAnimationOptions:). - dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ - [_mainSerialQueue performBlockOnMainThread:^{ - for (NSString *kind in _completedNodes) { - [self _relayoutNodesOfKind:kind]; - } - }]; - }); - }]; + LOG(@"Edit Command - relayoutRows"); + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + + // Can't relayout right away because _completedNodes may not be up-to-date, + // i.e there might be some nodes that were measured using the old constrained size but haven't been added to _completedNodes + // (see _layoutNodes:atIndexPaths:withAnimationOptions:). + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + [_mainSerialQueue performBlockOnMainThread:^{ + for (NSString *kind in _completedNodes) { + [self _relayoutNodesOfKind:kind]; + } + }]; + }); } - (void)_relayoutNodesOfKind:(NSString *)kind @@ -840,22 +801,24 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - [self performEditCommandWithBlock:^{ - ASDisplayNodeAssertMainThread(); - LOG(@"Edit Command - moveRow: %@ > %@", indexPath, newIndexPath); - dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); - - dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ - LOG(@"Edit Transaction - moveRow: %@ > %@", indexPath, newIndexPath); - NSArray *indexPaths = @[indexPath]; - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes[ASDataControllerRowNodeKind], indexPaths); - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + ASDisplayNodeAssertMainThread(); + if (!_initialReloadDataHasBeenCalled) { + return; + } - // Don't re-calculate size for moving - NSArray *newIndexPaths = @[newIndexPath]; - [self _insertNodes:nodes atIndexPaths:newIndexPaths withAnimationOptions:animationOptions]; - }); - }]; + LOG(@"Edit Command - moveRow: %@ > %@", indexPath, newIndexPath); + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + LOG(@"Edit Transaction - moveRow: %@ > %@", indexPath, newIndexPath); + NSArray *indexPaths = @[indexPath]; + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes[ASDataControllerRowNodeKind], indexPaths); + [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + + // Don't re-calculate size for moving + NSArray *newIndexPaths = @[newIndexPath]; + [self _insertNodes:nodes atIndexPaths:newIndexPaths withAnimationOptions:animationOptions]; + }); } #pragma mark - Data Querying (Subclass API) diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index d682e07b..c46b1d99 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -134,6 +134,8 @@ NS_ASSUME_NONNULL_BEGIN - (NSArray *> *)completedNodes; +- (NSString *)nameForRangeControllerDataSource; + @end /** diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index d8207377..e71e9901 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -11,13 +11,16 @@ #import "ASRangeController.h" #import "ASAssert.h" -#import "ASWeakSet.h" +#import "ASCellNode.h" #import "ASDisplayNodeExtras.h" #import "ASDisplayNodeInternal.h" #import "ASMultidimensionalArrayUtils.h" #import "ASInternalHelpers.h" +#import "ASMultiDimensionalArrayUtils.h" +#import "ASWeakSet.h" + #import "ASDisplayNode+FrameworkPrivate.h" -#import "ASCellNode.h" +#import "AsyncDisplayKit+Debug.h" #define AS_RANGECONTROLLER_LOG_UPDATE_FREQ 0 @@ -64,6 +67,10 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; #endif + if ([ASRangeController shouldShowRangeDebugOverlay]) { + [self addRangeControllerToRangeDebugOverlay]; + } + return self; } @@ -335,6 +342,25 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; } } + // TODO: This code is for debugging only, but would be great to clean up with a delegate method implementation. + if ([ASRangeController shouldShowRangeDebugOverlay]) { + ASScrollDirection scrollableDirections = ASScrollDirectionUp | ASScrollDirectionDown; + if ([_dataSource isKindOfClass:NSClassFromString(@"ASCollectionView")]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wundeclared-selector" + scrollableDirections = (ASScrollDirection)[_dataSource performSelector:@selector(scrollableDirections)]; +#pragma clang diagnostic pop + } + + [self updateRangeController:self + withScrollableDirections:scrollableDirections + scrollDirection:scrollDirection + rangeMode:rangeMode + displayTuningParameters:parametersDisplay + fetchDataTuningParameters:parametersFetchData + interfaceState:selfInterfaceState]; + } + _rangeIsValid = YES; #if ASRangeControllerLoggingEnabled diff --git a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm index 38901bce..324fb875 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm @@ -344,7 +344,7 @@ if (shouldApply) { _layer.layerProperty = (layerValueExpr); } else { ASDisplayNo // The node is loaded but we're not on main. // We will call [self __setNeedsLayout] when we apply // the pending state. We need to call it on main if the node is loaded - // to support implicit hierarchy management. + // to support automatic subnode management. [ASDisplayNodeGetPendingState(self) setNeedsLayout]; } else { // The node is not loaded and we're not on main. diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index f6337ba0..f3528bbf 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -124,7 +124,10 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo // Main thread only _ASTransitionContext *_pendingLayoutTransitionContext; - BOOL _usesImplicitHierarchyManagement; + BOOL _automaticallyManagesSubnodes; + NSTimeInterval _defaultLayoutTransitionDuration; + NSTimeInterval _defaultLayoutTransitionDelay; + UIViewAnimationOptions _defaultLayoutTransitionOptions; int32_t _pendingTransitionID; ASLayoutTransition *_pendingLayoutTransition; diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.mm b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.mm index 2b080cec..649f39e8 100644 --- a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.mm +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.mm @@ -15,6 +15,7 @@ #import "NSIndexSet+ASHelpers.h" #import "ASAssert.h" #import "ASDisplayNode+Beta.h" + #import #define ASFailUpdateValidation(...)\ diff --git a/AsyncDisplayKitTests/ASBasicImageDownloaderTests.m b/AsyncDisplayKitTests/ASBasicImageDownloaderTests.m index 3bfad702..bc897255 100644 --- a/AsyncDisplayKitTests/ASBasicImageDownloaderTests.m +++ b/AsyncDisplayKitTests/ASBasicImageDownloaderTests.m @@ -45,7 +45,7 @@ }]; #pragma clang diagnostic pop - [self waitForExpectationsWithTimeout:3 handler:nil]; + [self waitForExpectationsWithTimeout:30 handler:nil]; } @end diff --git a/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m b/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m index 95adca7a..6ce6312a 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m @@ -47,28 +47,13 @@ @implementation ASDisplayNodeImplicitHierarchyTests -- (void)setUp { - [super setUp]; - [ASDisplayNode setUsesImplicitHierarchyManagement:YES]; -} - -- (void)tearDown { - [ASDisplayNode setUsesImplicitHierarchyManagement:NO]; - [super tearDown]; -} - - (void)testFeatureFlag { - XCTAssert([ASDisplayNode usesImplicitHierarchyManagement]); ASDisplayNode *node = [[ASDisplayNode alloc] init]; - XCTAssert(node.usesImplicitHierarchyManagement); - - [ASDisplayNode setUsesImplicitHierarchyManagement:NO]; - XCTAssertFalse([ASDisplayNode usesImplicitHierarchyManagement]); - XCTAssertFalse(node.usesImplicitHierarchyManagement); - - node.usesImplicitHierarchyManagement = YES; - XCTAssert(node.usesImplicitHierarchyManagement); + XCTAssertFalse(node.automaticallyManagesSubnodes); + + node.automaticallyManagesSubnodes = YES; + XCTAssertTrue(node.automaticallyManagesSubnodes); } - (void)testInitialNodeInsertionWithOrdering @@ -80,6 +65,7 @@ ASDisplayNode *node5 = [[ASDisplayNode alloc] init]; ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize) { ASStaticLayoutSpec *staticLayout = [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[node4]]; @@ -106,6 +92,7 @@ ASDisplayNode *node3 = [[ASDisplayNode alloc] init]; ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize){ ASSpecTestDisplayNode *strongNode = (ASSpecTestDisplayNode *)weakNode; if ([strongNode.layoutState isEqualToNumber:@1]) { @@ -136,6 +123,7 @@ ASDisplayNode *node2 = [[ASDisplayNode alloc] init]; ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize) { ASSpecTestDisplayNode *strongNode = (ASSpecTestDisplayNode *)weakNode; if ([strongNode.layoutState isEqualToNumber:@1]) { @@ -179,6 +167,7 @@ ASDisplayNode *node2 = [[ASDisplayNode alloc] init]; ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize) { ASSpecTestDisplayNode *strongNode = (ASSpecTestDisplayNode *)weakNode; @@ -190,6 +179,8 @@ }; // Intentionally trigger view creation + [node view]; + [node1 view]; [node2 view]; XCTestExpectation *expectation = [self expectationWithDescription:@"Fix IHM layout transition also if one node is already loaded"]; diff --git a/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m b/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m new file mode 100644 index 00000000..78b420bf --- /dev/null +++ b/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m @@ -0,0 +1,35 @@ +// +// ASTextNodeSnapshotTests.m +// AsyncDisplayKit +// +// Created by Garrett Moon on 8/12/16. +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#import "ASSnapshotTestCase.h" + +#import + +@interface ASTextNodeSnapshotTests : ASSnapshotTestCase + +@end + +@implementation ASTextNodeSnapshotTests + +- (void)testTextContainerInset +{ + // trivial test case to ensure ASSnapshotTestCase works + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"judar" + attributes:@{NSFontAttributeName : [UIFont italicSystemFontOfSize:24]}]; + [textNode measureWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + textNode.frame = CGRectMake(0, 0, textNode.calculatedSize.width, textNode.calculatedSize.height); + textNode.textContainerInset = UIEdgeInsetsMake(0, 2, 0, 2); + + ASSnapshotVerifyNode(textNode, nil); +} + +@end diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASTextNodeSnapshotTests/testTextContainerInset@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASTextNodeSnapshotTests/testTextContainerInset@2x.png new file mode 100644 index 00000000..7e6cac14 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASTextNodeSnapshotTests/testTextContainerInset@2x.png differ diff --git a/examples/ASDKLayoutTransition/Default-568h@2x.png b/examples/ASDKLayoutTransition/Default-568h@2x.png new file mode 100644 index 00000000..6ee80b93 Binary files /dev/null and b/examples/ASDKLayoutTransition/Default-568h@2x.png differ diff --git a/examples/ASDKLayoutTransition/Default-667h@2x.png b/examples/ASDKLayoutTransition/Default-667h@2x.png new file mode 100644 index 00000000..e7b975e2 Binary files /dev/null and b/examples/ASDKLayoutTransition/Default-667h@2x.png differ diff --git a/examples/ASDKLayoutTransition/Default-736h@3x.png b/examples/ASDKLayoutTransition/Default-736h@3x.png new file mode 100644 index 00000000..c8949cae Binary files /dev/null and b/examples/ASDKLayoutTransition/Default-736h@3x.png differ diff --git a/examples/ASDKLayoutTransition/Podfile b/examples/ASDKLayoutTransition/Podfile new file mode 100644 index 00000000..919de4b3 --- /dev/null +++ b/examples/ASDKLayoutTransition/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '7.0' +target 'Sample' do + pod 'AsyncDisplayKit', :path => '../..' +end diff --git a/examples/ASDKLayoutTransition/Sample.xcodeproj/project.pbxproj b/examples/ASDKLayoutTransition/Sample.xcodeproj/project.pbxproj new file mode 100644 index 00000000..494f407d --- /dev/null +++ b/examples/ASDKLayoutTransition/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,368 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AA19EE274300767484 /* Default-667h@2x.png */; }; + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AB19EE274300767484 /* Default-736h@3x.png */; }; + DFE855DDBC731242D3515B58 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C284F7E957985CA251284B05 /* libPods-Sample.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 1C47DEC3F9D2BD9AD5F5CD67 /* Pods-Sample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sample.release.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.release.xcconfig"; sourceTree = ""; }; + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-667h@2x.png"; sourceTree = SOURCE_ROOT; }; + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-736h@3x.png"; sourceTree = SOURCE_ROOT; }; + 79ED4D85CC60068C341CFD77 /* Pods-Sample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.debug.xcconfig"; sourceTree = ""; }; + C284F7E957985CA251284B05 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DFE855DDBC731242D3515B58 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + C284F7E957985CA251284B05 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + 79ED4D85CC60068C341CFD77 /* Pods-Sample.debug.xcconfig */, + 1C47DEC3F9D2BD9AD5F5CD67 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, + 6E05308BEF86AD80AEB4EEE7 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0710; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 6E05308BEF86AD80AEB4EEE7 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 79ED4D85CC60068C341CFD77 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 7.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1C47DEC3F9D2BD9AD5F5CD67 /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 7.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/examples/ASDKLayoutTransition/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/ASDKLayoutTransition/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..a80c0382 --- /dev/null +++ b/examples/ASDKLayoutTransition/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/ASDKLayoutTransition/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/examples/ASDKLayoutTransition/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 00000000..0b71c455 --- /dev/null +++ b/examples/ASDKLayoutTransition/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/ASDKLayoutTransition/Sample/AppDelegate.h b/examples/ASDKLayoutTransition/Sample/AppDelegate.h new file mode 100644 index 00000000..27e560aa --- /dev/null +++ b/examples/ASDKLayoutTransition/Sample/AppDelegate.h @@ -0,0 +1,24 @@ +// +// AppDelegate.h +// Sample +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/examples/ASDKLayoutTransition/Sample/AppDelegate.m b/examples/ASDKLayoutTransition/Sample/AppDelegate.m new file mode 100644 index 00000000..c62355c0 --- /dev/null +++ b/examples/ASDKLayoutTransition/Sample/AppDelegate.m @@ -0,0 +1,33 @@ +// +// AppDelegate.m +// Sample +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +#import "AppDelegate.h" + +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[ViewController alloc] init]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/examples/ASDKLayoutTransition/Sample/Info.plist b/examples/ASDKLayoutTransition/Sample/Info.plist new file mode 100644 index 00000000..fb4115c8 --- /dev/null +++ b/examples/ASDKLayoutTransition/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/examples/ASDKLayoutTransition/Sample/ViewController.h b/examples/ASDKLayoutTransition/Sample/ViewController.h new file mode 100644 index 00000000..fc52c022 --- /dev/null +++ b/examples/ASDKLayoutTransition/Sample/ViewController.h @@ -0,0 +1,22 @@ +// +// ViewController.h +// Sample +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +#import + +@interface ViewController : UIViewController + +@end diff --git a/examples/ASDKLayoutTransition/Sample/ViewController.m b/examples/ASDKLayoutTransition/Sample/ViewController.m new file mode 100644 index 00000000..c15ae141 --- /dev/null +++ b/examples/ASDKLayoutTransition/Sample/ViewController.m @@ -0,0 +1,199 @@ +// +// ViewController.m +// Sample +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +#import "ViewController.h" + +#import + +#pragma mark - TransitionNode + +#define USE_CUSTOM_LAYOUT_TRANSITION 0 + +@interface TransitionNode : ASDisplayNode +@property (nonatomic, assign) BOOL enabled; +@property (nonatomic, strong) ASButtonNode *buttonNode; +@property (nonatomic, strong) ASTextNode *textNodeOne; +@property (nonatomic, strong) ASTextNode *textNodeTwo; +@end + +@implementation TransitionNode + + +#pragma mark - Lifecycle + +- (instancetype)init +{ + self = [super init]; + if (self == nil) { return self; } + + self.usesImplicitHierarchyManagement = YES; + + // Define the layout transition duration for the default transition + self.defaultLayoutTransitionDuration = 1.0; + + _enabled = NO; + + // Setup text nodes + _textNodeOne = [[ASTextNode alloc] init]; + _textNodeOne.attributedText = [[NSAttributedString alloc] initWithString:@"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled"]; + + _textNodeTwo = [[ASTextNode alloc] init]; + _textNodeTwo.attributedText = [[NSAttributedString alloc] initWithString:@"It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English."]; + + // Setup button + NSString *buttonTitle = @"Start Layout Transition"; + UIFont *buttonFont = [UIFont systemFontOfSize:16.0]; + UIColor *buttonColor = [UIColor blueColor]; + + _buttonNode = [[ASButtonNode alloc] init]; + [_buttonNode setTitle:buttonTitle withFont:buttonFont withColor:buttonColor forState:ASControlStateNormal]; + + // Note: Currently we have to set all the button properties to the same one as for ASControlStateNormal. Otherwise + // if the button is involved in the layout transition it would break the transition as it does a layout pass + // while changing the title. This needs and will be fixed in the future! + [_buttonNode setTitle:buttonTitle withFont:buttonFont withColor:buttonColor forState:ASControlStateHighlighted]; + + + // Some debug colors + _textNodeOne.backgroundColor = [UIColor orangeColor]; + _textNodeTwo.backgroundColor = [UIColor greenColor]; + + + return self; +} + +- (void)didLoad +{ + [super didLoad]; + + [self.buttonNode addTarget:self action:@selector(buttonPressed:) forControlEvents:ASControlNodeEventTouchDown]; +} + +#pragma mark - Actions + +- (void)buttonPressed:(id)sender +{ + self.enabled = !self.enabled; + + [self transitionLayoutAnimated:YES measurementCompletion:nil]; +} + + +#pragma mark - Layout + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASTextNode *nextTextNode = self.enabled ? self.textNodeTwo : self.textNodeOne; + nextTextNode.flexGrow = YES; + nextTextNode.flexShrink = YES; + + ASStackLayoutSpec *horizontalStackLayout = [ASStackLayoutSpec horizontalStackLayoutSpec]; + horizontalStackLayout.children = @[nextTextNode]; + + self.buttonNode.alignSelf = ASStackLayoutAlignSelfCenter; + + ASStackLayoutSpec *verticalStackLayout = [ASStackLayoutSpec verticalStackLayoutSpec]; + verticalStackLayout.spacing = 10.0; + verticalStackLayout.children = @[horizontalStackLayout, self.buttonNode]; + + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(15.0, 15.0, 15.0, 15.0) child:verticalStackLayout]; +} + + +#pragma mark - Transition + +#if USE_CUSTOM_LAYOUT_TRANSITION + +- (void)animateLayoutTransition:(id)context +{ + ASDisplayNode *fromNode = [[context removedSubnodes] objectAtIndex:0]; + ASDisplayNode *toNode = [[context insertedSubnodes] objectAtIndex:0]; + + ASButtonNode *buttonNode = nil; + for (ASDisplayNode *node in [context subnodesForKey:ASTransitionContextToLayoutKey]) { + if ([node isKindOfClass:[ASButtonNode class]]) { + buttonNode = (ASButtonNode *)node; + break; + } + } + + CGRect toNodeFrame = [context finalFrameForNode:toNode]; + toNodeFrame.origin.x += (self.enabled ? toNodeFrame.size.width : -toNodeFrame.size.width); + toNode.frame = toNodeFrame; + toNode.alpha = 0.0; + + CGRect fromNodeFrame = fromNode.frame; + fromNodeFrame.origin.x += (self.enabled ? -fromNodeFrame.size.width : fromNodeFrame.size.width); + + // We will use the same transition duration as the default transition + [UIView animateWithDuration:self.defaultLayoutTransitionDuration animations:^{ + toNode.frame = [context finalFrameForNode:toNode]; + toNode.alpha = 1.0; + + fromNode.frame = fromNodeFrame; + fromNode.alpha = 0.0; + + // Update frame of self + CGSize fromSize = [context layoutForKey:ASTransitionContextFromLayoutKey].size; + CGSize toSize = [context layoutForKey:ASTransitionContextToLayoutKey].size; + BOOL isResized = (CGSizeEqualToSize(fromSize, toSize) == NO); + if (isResized == YES) { + CGPoint position = self.frame.origin; + self.frame = CGRectMake(position.x, position.y, toSize.width, toSize.height); + } + + buttonNode.frame = [context finalFrameForNode:buttonNode]; + } completion:^(BOOL finished) { + [context completeTransition:finished]; + }]; +} + +#endif + +@end + + +#pragma mark - ViewController + +@interface ViewController () +@property (nonatomic, strong) TransitionNode *transitionNode; +@end + +@implementation ViewController + +#pragma mark - UIViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + _transitionNode = [TransitionNode new]; + [self.view addSubnode:_transitionNode]; + + // Some debug colors + _transitionNode.backgroundColor = [UIColor grayColor]; +} + +- (void)viewDidLayoutSubviews +{ + [super viewDidLayoutSubviews]; + + CGSize size = [self.transitionNode measure:self.view.frame.size]; + self.transitionNode.frame = CGRectMake(0, 20, size.width, size.height); +} + +@end diff --git a/examples/ASDKLayoutTransition/Sample/main.m b/examples/ASDKLayoutTransition/Sample/main.m new file mode 100644 index 00000000..756080fb --- /dev/null +++ b/examples/ASDKLayoutTransition/Sample/main.m @@ -0,0 +1,26 @@ +// +// main.m +// Sample +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/examples/ASDKgram/Sample/Utilities.m b/examples/ASDKgram/Sample/Utilities.m index b5a144a0..556a3a2d 100644 --- a/examples/ASDKgram/Sample/Utilities.m +++ b/examples/ASDKgram/Sample/Utilities.m @@ -21,6 +21,85 @@ #define StrokeRoundedImages 0 +#define IsDigit(v) (v >= '0' && v <= '9') + +static time_t parseRfc3339ToTimeT(const char *string) +{ + int dy, dm, dd; + int th, tm, ts; + int oh, om, osign; + char current; + + if (!string) + return (time_t)0; + + // date + if (sscanf(string, "%04d-%02d-%02d", &dy, &dm, &dd) == 3) { + string += 10; + + if (*string++ != 'T') + return (time_t)0; + + // time + if (sscanf(string, "%02d:%02d:%02d", &th, &tm, &ts) == 3) { + string += 8; + + current = *string; + + // optional: second fraction + if (current == '.') { + ++string; + while(IsDigit(*string)) + ++string; + + current = *string; + } + + if (current == 'Z') { + oh = om = 0; + osign = 1; + } else if (current == '-') { + ++string; + if (sscanf(string, "%02d:%02d", &oh, &om) != 2) + return (time_t)0; + osign = -1; + } else if (current == '+') { + ++string; + if (sscanf(string, "%02d:%02d", &oh, &om) != 2) + return (time_t)0; + osign = 1; + } else { + return (time_t)0; + } + + struct tm timeinfo; + timeinfo.tm_wday = timeinfo.tm_yday = 0; + timeinfo.tm_zone = NULL; + timeinfo.tm_isdst = -1; + + timeinfo.tm_year = dy - 1900; + timeinfo.tm_mon = dm - 1; + timeinfo.tm_mday = dd; + + timeinfo.tm_hour = th; + timeinfo.tm_min = tm; + timeinfo.tm_sec = ts; + + // convert to utc + return timegm(&timeinfo) - (((oh * 60 * 60) + (om * 60)) * osign); + } + } + + return (time_t)0; +} + +static NSDate *parseRfc3339ToNSDate(NSString *rfc3339DateTimeString) +{ + time_t t = parseRfc3339ToTimeT([rfc3339DateTimeString cStringUsingEncoding:NSUTF8StringEncoding]); + return [NSDate dateWithTimeIntervalSince1970:t]; +} + + @implementation UIColor (Additions) + (UIColor *)darkBlueColor @@ -142,26 +221,15 @@ @implementation NSString (Additions) -// Returns a user-visible date time string that corresponds to the -// specified RFC 3339 date time string. Note that this does not handle -// all possible RFC 3339 date time strings, just one of the most common -// styles. +/* + * Returns a user-visible date time string that corresponds to the + * specified RFC 3339 date time string. Note that this does not handle + * all possible RFC 3339 date time strings, just one of the most common + * styles. + */ + (NSDate *)userVisibleDateTimeStringForRFC3339DateTimeString:(NSString *)rfc3339DateTimeString { - NSDateFormatter * rfc3339DateFormatter; - NSLocale * enUSPOSIXLocale; - - // Convert the RFC 3339 date time string to an NSDate. - - rfc3339DateFormatter = [[NSDateFormatter alloc] init]; - - enUSPOSIXLocale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; - - [rfc3339DateFormatter setLocale:enUSPOSIXLocale]; - [rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ssZ'"]; - [rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; - - return [rfc3339DateFormatter dateFromString:rfc3339DateTimeString]; + return parseRfc3339ToNSDate(rfc3339DateTimeString); } + (NSString *)elapsedTimeStringSinceDate:(NSString *)uploadDateString diff --git a/examples/VerticalWithinHorizontalScrolling/Sample/ViewController.m b/examples/VerticalWithinHorizontalScrolling/Sample/ViewController.m index a0db6621..cd93566f 100644 --- a/examples/VerticalWithinHorizontalScrolling/Sample/ViewController.m +++ b/examples/VerticalWithinHorizontalScrolling/Sample/ViewController.m @@ -40,6 +40,8 @@ _pagerNode.dataSource = self; _pagerNode.delegate = self; + [ASRangeController setShouldShowRangeDebugOverlay:YES]; + // Could implement ASCollectionDelegate if we wanted extra callbacks, like from UIScrollView. //_pagerNode.delegate = self;