diff --git a/AsyncDisplayKit/ASCellNode.mm b/AsyncDisplayKit/ASCellNode.mm index 0803c81f79..cb50b6cb05 100644 --- a/AsyncDisplayKit/ASCellNode.mm +++ b/AsyncDisplayKit/ASCellNode.mm @@ -214,9 +214,9 @@ // To be overriden by subclasses } -- (void)visibilityDidChange:(BOOL)isVisible +- (void)visibleStateDidChange:(BOOL)isVisible { - [super visibilityDidChange:isVisible]; + [super visibleStateDidChange:isVisible]; CGRect cellFrame = CGRectZero; if (_scrollView) { diff --git a/AsyncDisplayKit/ASCollectionNode.mm b/AsyncDisplayKit/ASCollectionNode.mm index a29ee08cf2..2ddb0d35b8 100644 --- a/AsyncDisplayKit/ASCollectionNode.mm +++ b/AsyncDisplayKit/ASCollectionNode.mm @@ -173,9 +173,9 @@ } #if ASRangeControllerLoggingEnabled -- (void)visibilityDidChange:(BOOL)isVisible +- (void)visibleStateDidChange:(BOOL)isVisible { - [super visibilityDidChange:isVisible]; + [super visibleStateDidChange:isVisible]; NSLog(@"%@ - visible: %d", self, isVisible); } #endif diff --git a/AsyncDisplayKit/ASDisplayNode+Subclasses.h b/AsyncDisplayKit/ASDisplayNode+Subclasses.h index 240f111f97..381184c849 100644 --- a/AsyncDisplayKit/ASDisplayNode+Subclasses.h +++ b/AsyncDisplayKit/ASDisplayNode+Subclasses.h @@ -242,6 +242,31 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)visibilityDidChange:(BOOL)isVisible ASDISPLAYNODE_REQUIRES_SUPER; +/** + * @abstract Called whenever the visiblity of the node changed. + * + * @discussion Subclasses may use this to monitor when they become visible. + */ +- (void)visibleStateDidChange:(BOOL)isVisible ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * @abstract Called whenever the the node has entered or exited the display state. + * + * @discussion Subclasses may use this to monitor when a node should be rendering its content. + * + * @note This method can be called from any thread and should therefore be thread safe. + */ +- (void)displayStateDidChange:(BOOL)inDisplayState ASDISPLAYNODE_REQUIRES_SUPER; + +/** + * @abstract Called whenever the the node has entered or left the load state. + * + * @discussion Subclasses may use this to monitor data for a node should be loaded, either from a local or remote source. + * + * @note This method can be called from any thread and should therefore be thread safe. + */ +- (void)loadStateDidChange:(BOOL)inLoadState ASDISPLAYNODE_REQUIRES_SUPER; + /** * Called just before the view is added to a window. */ diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 7f59ef65df..6e178dba0a 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -2044,6 +2044,8 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) ASDisplayNodeAssertMainThread(); } +#pragma mark Hierarchy State + - (void)willEnterHierarchy { ASDisplayNodeAssertMainThread(); @@ -2064,7 +2066,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) if (![self supportsRangeManagedInterfaceState]) { self.interfaceState = ASInterfaceStateNone; } else { - // This case is important when tearing down hierarchies. We must deliver a visibilityDidChange:NO callback, as part our API guarantee that this method can be used for + // This case is important when tearing down hierarchies. We must deliver a visibileStateDidChange:NO callback, as part our API guarantee that this method can be used for // things like data analytics about user content viewing. We cannot call the method in the dealloc as any incidental retain operations in client code would fail. // Additionally, it may be that a Standard UIView which is containing us is moving between hierarchies, and we should not send the call if we will be re-added in the // same runloop. Strategy: strong reference (might be the last!), wait one runloop, and confirm we are still outside the hierarchy (both layer-backed and view-backed). @@ -2083,6 +2085,8 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) } } +#pragma mark Interface State + - (void)clearContents { // No-op if these haven't been created yet, as that guarantees they don't have contents that needs to be released. @@ -2131,7 +2135,22 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (void)visibilityDidChange:(BOOL)isVisible { - // subclass override + // subclass override +} + +- (void)visibleStateDidChange:(BOOL)isVisible +{ + // subclass override +} + +- (void)displayStateDidChange:(BOOL)inDisplayState +{ + //subclass override +} + +- (void)loadStateDidChange:(BOOL)inLoadState +{ + //subclass override } /** @@ -2178,10 +2197,12 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) if (nowFetchData != wasFetchData) { if (nowFetchData) { [self fetchData]; + [self loadStateDidChange:YES]; } else { if ([self supportsRangeManagedInterfaceState]) { [self clearFetchedData]; } + [self loadStateDidChange:NO]; } } @@ -2225,6 +2246,8 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) } } } + + [self displayStateDidChange:nowDisplay]; } // Became visible or invisible. When range-managed, this represents literal visibility - at least one pixel @@ -2233,14 +2256,16 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) BOOL wasVisible = ASInterfaceStateIncludesVisible(oldState); if (nowVisible != wasVisible) { - [self visibilityDidChange:nowVisible]; + [self visibleStateDidChange:nowVisible]; + [self visibilityDidChange:nowVisible]; //TODO: remove once this method has been deprecated } - + [self interfaceStateDidChange:newState fromState:oldState]; } - (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState { + // subclass hook } - (void)enterInterfaceState:(ASInterfaceState)interfaceState diff --git a/AsyncDisplayKit/ASImageNode+AnimatedImage.mm b/AsyncDisplayKit/ASImageNode+AnimatedImage.mm index da73d5bcca..998194ed00 100644 --- a/AsyncDisplayKit/ASImageNode+AnimatedImage.mm +++ b/AsyncDisplayKit/ASImageNode+AnimatedImage.mm @@ -158,9 +158,9 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSDefaultRunLoopMode; [self.animatedImage clearAnimatedImageCache]; } -- (void)visibilityDidChange:(BOOL)isVisible +- (void)visibleStateDidChange:(BOOL)isVisible { - [super visibilityDidChange:isVisible]; + [super visibleStateDidChange:isVisible]; ASDisplayNodeAssertMainThread(); if (isVisible) { diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index 8dca4e6495..d121a052f6 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -295,11 +295,11 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent } } -/* visibilityDidChange in ASNetworkImageNode has a very similar implementation. Changes here are likely necessary +/* visibileStateDidChange in ASNetworkImageNode has a very similar implementation. Changes here are likely necessary in ASNetworkImageNode as well. */ -- (void)visibilityDidChange:(BOOL)isVisible +- (void)visibleStateDidChange:(BOOL)isVisible { - [super visibilityDidChange:isVisible]; + [super visibleStateDidChange:isVisible]; if (_downloaderImplementsSetPriority) { ASDN::MutexLocker l(_downloadIdentifierLock); diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index eaf9882f5e..38f3381929 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -278,11 +278,11 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; } } -/* visibilityDidChange in ASMultiplexImageNode has a very similar implementation. Changes here are likely necessary +/* visibileStateDidChange in ASMultiplexImageNode has a very similar implementation. Changes here are likely necessary in ASMultiplexImageNode as well. */ -- (void)visibilityDidChange:(BOOL)isVisible +- (void)visibleStateDidChange:(BOOL)isVisible { - [super visibilityDidChange:isVisible]; + [super visibleStateDidChange:isVisible]; if (_downloaderImplementsSetPriority) { _lock.lock(); diff --git a/AsyncDisplayKit/ASTableNode.mm b/AsyncDisplayKit/ASTableNode.mm index 1642478d2d..1e81e2e059 100644 --- a/AsyncDisplayKit/ASTableNode.mm +++ b/AsyncDisplayKit/ASTableNode.mm @@ -145,9 +145,9 @@ } #if ASRangeControllerLoggingEnabled -- (void)visibilityDidChange:(BOOL)isVisible +- (void)visibleStateDidChange:(BOOL)isVisible { - [super visibilityDidChange:isVisible]; + [super visibleStateDidChange:isVisible]; NSLog(@"%@ - visible: %d", self, isVisible); } #endif diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index 6240ec55f6..5b629ccd97 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -382,9 +382,9 @@ static NSString * const kStatus = @"status"; } } -- (void)visibilityDidChange:(BOOL)isVisible +- (void)visibleStateDidChange:(BOOL)isVisible { - [super visibilityDidChange:isVisible]; + [super visibleStateDidChange:isVisible]; ASDN::MutexLocker l(_videoLock); diff --git a/AsyncDisplayKit/ASVideoPlayerNode.mm b/AsyncDisplayKit/ASVideoPlayerNode.mm index ec19fda312..3e046e57a2 100644 --- a/AsyncDisplayKit/ASVideoPlayerNode.mm +++ b/AsyncDisplayKit/ASVideoPlayerNode.mm @@ -163,9 +163,9 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; } } -- (void)visibilityDidChange:(BOOL)isVisible +- (void)visibleStateDidChange:(BOOL)isVisible { - [super visibilityDidChange:isVisible]; + [super visibleStateDidChange:isVisible]; ASDN::MutexLocker l(_videoPlayerLock); diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 7935564a3d..6221ff02e0 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -95,7 +95,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; { _scrollDirection = scrollDirection; - // Perform update immediately, so that cells receive a visibilityDidChange: call before their first pixel is visible. + // Perform update immediately, so that cells receive a visibleStateDidChange: call before their first pixel is visible. [self scheduleRangeUpdate]; } diff --git a/AsyncDisplayKitTests/ASDisplayNodeTests.m b/AsyncDisplayKitTests/ASDisplayNodeTests.m index 36b01040bf..c6c8ec8903 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeTests.m @@ -86,6 +86,12 @@ for (ASDisplayNode *n in @[ nodes ]) {\ @property (atomic, copy) void (^willDeallocBlock)(ASTestDisplayNode *node); @property (atomic, copy) CGSize(^calculateSizeBlock)(ASTestDisplayNode *node, CGSize size); @property (atomic) BOOL hasFetchedData; + +@property (atomic) BOOL displayRangeStateChangedToYES; +@property (atomic) BOOL displayRangeStateChangedToNO; + +@property (atomic) BOOL loadStateChangedToYES; +@property (atomic) BOOL loadStateChangedToNO; @end @interface ASTestResponderNode : ASTestDisplayNode @@ -110,6 +116,28 @@ for (ASDisplayNode *n in @[ nodes ]) {\ self.hasFetchedData = NO; } +- (void)displayStateDidChange:(BOOL)inDisplayState +{ + [super displayStateDidChange:inDisplayState]; + + if (inDisplayState) { + self.displayRangeStateChangedToYES = YES; + } else { + self.displayRangeStateChangedToNO = YES; + } +} + +- (void)loadStateDidChange:(BOOL)inLoadState +{ + [super loadStateDidChange:inLoadState]; + + if (inLoadState) { + self.loadStateChangedToYES = YES; + } else { + self.loadStateChangedToNO = YES; + } +} + - (void)dealloc { if (_willDeallocBlock) { @@ -1878,4 +1906,42 @@ static bool stringContainsPointer(NSString *description, const void *p) { XCTAssert(node.bounds.size.height == 8, @"Wrong ASDisplayNode.bounds.size.height"); } +- (void)testDidEnterDisplayIsCalledWhenNodesEnterDisplayRange +{ + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + + [node recursivelySetInterfaceState:ASInterfaceStateDisplay]; + + XCTAssert([node displayRangeStateChangedToYES]); +} + +- (void)testDidExitDisplayIsCalledWhenNodesExitDisplayRange +{ + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + + [node recursivelySetInterfaceState:ASInterfaceStateDisplay]; + [node recursivelySetInterfaceState:ASInterfaceStateFetchData]; + + XCTAssert([node displayRangeStateChangedToNO]); +} + +- (void)testDidEnterFetchDataIsCalledWhenNodesEnterFetchDataRange +{ + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + + [node recursivelySetInterfaceState:ASInterfaceStateFetchData]; + + XCTAssert([node loadStateChangedToYES]); +} + +- (void)testDidExitFetchDataIsCalledWhenNodesExitFetchDataRange +{ + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + + [node recursivelySetInterfaceState:ASInterfaceStateFetchData]; + [node recursivelySetInterfaceState:ASInterfaceStateDisplay]; + + XCTAssert([node loadStateChangedToNO]); +} + @end diff --git a/AsyncDisplayKitTests/ASVideoNodeTests.m b/AsyncDisplayKitTests/ASVideoNodeTests.m index f74ba735f9..d03fdcfea2 100644 --- a/AsyncDisplayKitTests/ASVideoNodeTests.m +++ b/AsyncDisplayKitTests/ASVideoNodeTests.m @@ -223,12 +223,11 @@ }]; _videoNode.playerNode.layer.frame = CGRectZero; - [_videoNode visibilityDidChange:YES]; + [_videoNode visibleStateDidChange:YES]; XCTAssertTrue(_videoNode.shouldBePlaying); } - - (void)testVideoShouldPauseWhenItLeavesVisibleButShouldKnowPlayingShouldRestartLater { _videoNode.asset = _firstAsset;