Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for having multiple interface state delegates. #979

Merged
merged 6 commits into from
Jun 27, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions Source/ASDisplayNode+InterfaceState.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//
// ASDisplayNode+InterfaceState.h
// Texture
//
// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//

#import <Foundation/Foundation.h>

/**
* Interface state is available on ASDisplayNode and ASViewController, and
* allows checking whether a node is in an interface situation where it is prudent to trigger certain
* actions: measurement, data loading, display, and visibility (the latter for animations or other onscreen-only effects).
*
* The defualt state, ASInterfaceStateNone, means that the element is not predicted to be onscreen soon and
* preloading should not be performed. Swift: use [] for the default behavior.
*/
typedef NS_OPTIONS(NSUInteger, ASInterfaceState)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just moved, no changes.

{
/** The element is not predicted to be onscreen soon and preloading should not be performed */
ASInterfaceStateNone = 0,
/** The element may be added to a view soon that could become visible. Measure the layout, including size calculation. */
ASInterfaceStateMeasureLayout = 1 << 0,
/** The element is likely enough to come onscreen that disk and/or network data required for display should be fetched. */
ASInterfaceStatePreload = 1 << 1,
/** The element is very likely to become visible, and concurrent rendering should be executed for any -setNeedsDisplay. */
ASInterfaceStateDisplay = 1 << 2,
/** The element is physically onscreen by at least 1 pixel.
In practice, all other bit fields should also be set when this flag is set. */
ASInterfaceStateVisible = 1 << 3,

/**
* The node is not contained in a cell but it is in a window.
*
* Currently we only set `interfaceState` to other values for
* nodes contained in table views or collection views.
*/
ASInterfaceStateInHierarchy = ASInterfaceStateMeasureLayout | ASInterfaceStatePreload | ASInterfaceStateDisplay | ASInterfaceStateVisible,
};

@protocol ASInterfaceStateDelegate <NSObject>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only change here is that these are now all optional.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also added hierarchyDisplayDidFinish

@optional

/**
* @abstract Called whenever any bit in the ASInterfaceState bitfield is changed.
* @discussion Subclasses may use this to monitor when they become visible, should free cached data, and much more.
* @see ASInterfaceState
*/
- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState;

/**
* @abstract Called whenever the node becomes visible.
* @discussion Subclasses may use this to monitor when they become visible.
* @note This method is guaranteed to be called on main.
*/
- (void)didEnterVisibleState;

/**
* @abstract Called whenever the node is no longer visible.
* @discussion Subclasses may use this to monitor when they are no longer visible.
* @note This method is guaranteed to be called on main.
*/
- (void)didExitVisibleState;

/**
* @abstract Called whenever the the node has entered the display state.
* @discussion Subclasses may use this to monitor when a node should be rendering its content.
* @note This method is guaranteed to be called on main.
*/
- (void)didEnterDisplayState;

/**
* @abstract Called whenever the the node has exited the display state.
* @discussion Subclasses may use this to monitor when a node should no longer be rendering its content.
* @note This method is guaranteed to be called on main.
*/
- (void)didExitDisplayState;

/**
* @abstract Called whenever the the node has entered the preload state.
* @discussion Subclasses may use this to monitor data for a node should be preloaded, either from a local or remote source.
* @note This method is guaranteed to be called on main.
*/
- (void)didEnterPreloadState;

/**
* @abstract Called whenever the the node has exited the preload state.
* @discussion Subclasses may use this to monitor whether preloading data for a node should be canceled.
* @note This method is guaranteed to be called on main.
*/
- (void)didExitPreloadState;

/**
* @abstract Called when the node has completed applying the layout.
* @discussion Can be used for operations that are performed after layout has completed.
* @note This method is guaranteed to be called on main.
*/
- (void)nodeDidLayout;

/**
* @abstract Called when the node loads.
* @discussion Can be used for operations that are performed after the node's view is available.
* @note This method is guaranteed to be called on main.
*/
- (void)nodeDidLoad;

@end

@interface ASDisplayNodeInterfaceDelegateManager : NSObject <ASInterfaceStateDelegate>

- (void)addDelegate:(id <ASInterfaceStateDelegate>)delegate;
- (void)removeDelegate:(id <ASInterfaceStateDelegate>)delegate;

@end
154 changes: 154 additions & 0 deletions Source/ASDisplayNode+InterfaceState.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//
// ASDisplayNode+InterfaceState.m
// Texture
//
// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//


#import <AsyncDisplayKit/ASDisplayNode+InterfaceState.h>

@interface ASDisplayNodeInterfaceDelegateManager ()
{
NSHashTable *_interfaceDidChangeDelegates;
NSHashTable *_interfaceDidEnterVisibleDelegates;
NSHashTable *_interfaceDidExitVisibleDelegates;
NSHashTable *_interfaceDidEnterDisplayDelegates;
NSHashTable *_interfaceDidExitDisplayDelegates;
NSHashTable *_interfaceDidEnterPreloadDelegates;
NSHashTable *_interfaceDidExitPreloadDelegates;
NSHashTable *_interfaceNodeDidLayoutDelegates;
NSHashTable *_interfaceNodeDidLoadDelegates;
}
@end

@implementation ASDisplayNodeInterfaceDelegateManager

- (instancetype)init
{
if (self = [super init]) {
_interfaceDidChangeDelegates = [NSHashTable weakObjectsHashTable];
_interfaceDidEnterVisibleDelegates = [NSHashTable weakObjectsHashTable];
_interfaceDidExitVisibleDelegates = [NSHashTable weakObjectsHashTable];
_interfaceDidEnterDisplayDelegates = [NSHashTable weakObjectsHashTable];
_interfaceDidExitDisplayDelegates = [NSHashTable weakObjectsHashTable];
_interfaceDidEnterPreloadDelegates = [NSHashTable weakObjectsHashTable];
_interfaceDidExitPreloadDelegates = [NSHashTable weakObjectsHashTable];
_interfaceNodeDidLayoutDelegates = [NSHashTable weakObjectsHashTable];
_interfaceNodeDidLoadDelegates = [NSHashTable weakObjectsHashTable];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Rather than separate tables, I think it's fine to use class_respondsToSelector before each call. That call actually runs through the same message-send machinery as a normal call, plus it warms the impcache. It's plenty fast.
  • Let's use -[NSHashTable initWithOptions:NSHashTableWeakMemory | NSHashTableObjectPointerPersonality capacity:0] (0 means default.)

That'll let you remove this intermediary object and it won't be slower by any amount that matters.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, using -respondsToSelector: is only a tiny bit slower and allows for proxying, so that's probably the way to go. Still fast.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should have to create all these up front...since we would only have to use the respondsToSelector: once per class (not even once per delegate), we should get a significant win by relying on just one object tracking the delegates, and we could use a simple bitfield / NS_OPTIONS to cache the ASInterfaceStateDelegateMethodsImplemented (similar to the flags in ASCollection that cache what the delegate / datasource implement, but just an NS_OPTIONS).

Copy link
Member

@Adlai-Holler Adlai-Holler Jun 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In recent versions of Objective-C, respondsToSelector: is truly very fast. It amortizes to 1 additional objc_msgSend, 3 static function invocations, and one inline-assembly routine. It also has the added benefit of checking-from and storing-to the ObjC impcache, so the subsequent message-send is optimized.

Writing our own bitfield-by-class would amount to us reimplementing that portion of the impcache, without the benefit of direct access to the class method table. And even accessing our table – say it were in a singleton registry object – would require retaining-releasing it unless we coded around that, which would easily subsume the entire rest of the lookup. The added boilerplate is also nontrivial.

So there is a win available here, but I believe it's small. Let's avoid these calls only if we can do it in a simple and tremendously fast & scalable way. If we can't, then we shouldn't worry about it – I believe we can absolutely achieve far superior optimizations by using resources for other parts of the framework (such as transfer-collections or more judicious applications of __unsafe_unretained).

Implementations:

}
}

- (void)addDelegate:(id<ASInterfaceStateDelegate>)delegate
{
if ([delegate respondsToSelector:@selector(interfaceStateDidChange:fromState:)]) {
[_interfaceDidChangeDelegates addObject:delegate];
}
if ([delegate respondsToSelector:@selector(didEnterVisibleState)]) {
[_interfaceDidEnterVisibleDelegates addObject:delegate];
}
if ([delegate respondsToSelector:@selector(didExitVisibleState)]) {
[_interfaceDidExitVisibleDelegates addObject:delegate];
}
if ([delegate respondsToSelector:@selector(didEnterDisplayState)]) {
[_interfaceDidEnterDisplayDelegates addObject:delegate];
}
if ([delegate respondsToSelector:@selector(didExitDisplayState)]) {
[_interfaceDidExitDisplayDelegates addObject:delegate];
}
if ([delegate respondsToSelector:@selector(didEnterPreloadState)]) {
[_interfaceDidEnterPreloadDelegates addObject:delegate];
}
if ([delegate respondsToSelector:@selector(didExitPreloadState)]) {
[_interfaceDidExitPreloadDelegates addObject:delegate];
}
if ([delegate respondsToSelector:@selector(nodeDidLayout)]) {
[_interfaceNodeDidLayoutDelegates addObject:delegate];
}
if ([delegate respondsToSelector:@selector(nodeDidLoad)]) {
[_interfaceNodeDidLoadDelegates addObject:delegate];
}
}

- (void)removeDelegate:(id<ASInterfaceStateDelegate>)delegate
{
[_interfaceDidChangeDelegates removeObject:delegate];
[_interfaceDidEnterVisibleDelegates removeObject:delegate];
[_interfaceDidExitVisibleDelegates removeObject:delegate];
[_interfaceDidEnterDisplayDelegates removeObject:delegate];
[_interfaceDidExitDisplayDelegates removeObject:delegate];
[_interfaceDidEnterPreloadDelegates removeObject:delegate];
[_interfaceDidExitPreloadDelegates removeObject:delegate];
[_interfaceNodeDidLayoutDelegates removeObject:delegate];
[_interfaceNodeDidLoadDelegates removeObject:delegate];
}

- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState
{
for (id <ASInterfaceStateDelegate>delegate in _interfaceDidChangeDelegates) {
[delegate interfaceStateDidChange:newState fromState:oldState];
}
}

- (void)didEnterVisibleState
{
for (id <ASInterfaceStateDelegate>delegate in _interfaceDidEnterVisibleDelegates) {
[delegate didEnterVisibleState];
}
}

- (void)didExitVisibleState
{
for (id <ASInterfaceStateDelegate>delegate in _interfaceDidExitVisibleDelegates) {
[delegate didExitVisibleState];
}
}

- (void)didEnterDisplayState
{
for (id <ASInterfaceStateDelegate>delegate in _interfaceDidEnterDisplayDelegates) {
[delegate didEnterDisplayState];
}
}

- (void)didExitDisplayState
{
for (id <ASInterfaceStateDelegate>delegate in _interfaceDidExitDisplayDelegates) {
[delegate didExitDisplayState];
}
}

- (void)didEnterPreloadState
{
for (id <ASInterfaceStateDelegate>delegate in _interfaceDidEnterPreloadDelegates) {
[delegate didEnterPreloadState];
}
}

- (void)didExitPreloadState
{
for (id <ASInterfaceStateDelegate>delegate in _interfaceDidExitPreloadDelegates) {
[delegate didExitPreloadState];
}
}

- (void)nodeDidLayout
{
for (id <ASInterfaceStateDelegate>delegate in _interfaceNodeDidLayoutDelegates) {
[delegate nodeDidLayout];
}
}

- (void)nodeDidLoad
{
for (id <ASInterfaceStateDelegate>delegate in _interfaceNodeDidLoadDelegates) {
[delegate nodeDidLoad];
}
}

@end
68 changes: 0 additions & 68 deletions Source/ASDisplayNode+Subclasses.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,74 +42,6 @@ NS_ASSUME_NONNULL_BEGIN
* variables.
*/

@protocol ASInterfaceStateDelegate <NSObject>
@required

/**
* @abstract Called whenever any bit in the ASInterfaceState bitfield is changed.
* @discussion Subclasses may use this to monitor when they become visible, should free cached data, and much more.
* @see ASInterfaceState
*/
- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState;

/**
* @abstract Called whenever the node becomes visible.
* @discussion Subclasses may use this to monitor when they become visible.
* @note This method is guaranteed to be called on main.
*/
- (void)didEnterVisibleState;

/**
* @abstract Called whenever the node is no longer visible.
* @discussion Subclasses may use this to monitor when they are no longer visible.
* @note This method is guaranteed to be called on main.
*/
- (void)didExitVisibleState;

/**
* @abstract Called whenever the the node has entered the display state.
* @discussion Subclasses may use this to monitor when a node should be rendering its content.
* @note This method is guaranteed to be called on main.
*/
- (void)didEnterDisplayState;

/**
* @abstract Called whenever the the node has exited the display state.
* @discussion Subclasses may use this to monitor when a node should no longer be rendering its content.
* @note This method is guaranteed to be called on main.
*/
- (void)didExitDisplayState;

/**
* @abstract Called whenever the the node has entered the preload state.
* @discussion Subclasses may use this to monitor data for a node should be preloaded, either from a local or remote source.
* @note This method is guaranteed to be called on main.
*/
- (void)didEnterPreloadState;

/**
* @abstract Called whenever the the node has exited the preload state.
* @discussion Subclasses may use this to monitor whether preloading data for a node should be canceled.
* @note This method is guaranteed to be called on main.
*/
- (void)didExitPreloadState;

/**
* @abstract Called when the node has completed applying the layout.
* @discussion Can be used for operations that are performed after layout has completed.
* @note This method is guaranteed to be called on main.
*/
- (void)nodeDidLayout;

/**
* @abstract Called when the node loads.
* @discussion Can be used for operations that are performed after the node's view is available.
* @note This method is guaranteed to be called on main.
*/
- (void)nodeDidLoad;

@end

@interface ASDisplayNode (Subclassing) <ASInterfaceStateDelegate>

#pragma mark - Properties
Expand Down
Loading