Created
November 14, 2017 16:07
-
-
Save DreamingInBinary/e4218c00dbeff815e26426af402ca2ad to your computer and use it in GitHub Desktop.
Empty data set for ASDisplayNode
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ASDisplayNode+BFREmptyView.m | |
// Buffer | |
// | |
// Created by Jordan Morgan on 10/27/17. | |
// | |
#import "ASDisplayNode+BFREmptyView.h" | |
#import <objc/runtime.h> | |
#import <AsyncDisplayKit/AsyncDisplayKit.h> | |
static NSMutableDictionary *implementationLookupTable; | |
static BOOL experimentEnableGesture; | |
@interface ASDisplayNode () | |
void swizzleInstanceUpdateMethods(id self); | |
void swizzledBatchUpdates(id self, SEL _cmd, void(^updates)(), void(^completion)(BOOL)); | |
void swizzledReloadDataWithCompletion(id self, SEL _cmd, void(^completion)()); | |
@property (strong, nonatomic, nullable, readonly) UIView *emptyView; | |
@property (strong, nonatomic, nullable, readonly) UIPanGestureRecognizer *pan; | |
@end | |
@implementation ASDisplayNode (BFREmptyView) | |
#pragma mark - Getters/Setters | |
static char const * BFREmptyDataViewDataSourcePropertyKey = "BFREmptyDataViewDataSourcePropertyKey"; | |
static char const * BFREmptyDataViewPropertyKey = "BFREmptyDataViewPropertyKey"; | |
static char const * BFREmptyDataViewPanPropertyKey = "BFREmptyDataViewPanPropertyKey"; | |
- (id <BFREmptyDataViewDataSource>)emptyDataViewDataSourceDelegate { | |
return objc_getAssociatedObject(self, BFREmptyDataViewDataSourcePropertyKey); | |
} | |
- (void)setEmptyDataViewDataSourceDelegate:(id <BFREmptyDataViewDataSource>)emptyDataSource { | |
objc_setAssociatedObject(self, BFREmptyDataViewDataSourcePropertyKey, emptyDataSource, OBJC_ASSOCIATION_RETAIN_NONATOMIC); | |
if ([self respondsToSelector:@selector(performBatchUpdates:completion:)]) { | |
if ([self isKindOfClass:[ASCollectionNode class]]) { | |
static dispatch_once_t onceCollectionNodeToken; | |
dispatch_once(&onceCollectionNodeToken, ^{ | |
swizzleInstanceUpdateMethods(self); | |
}); | |
} else if ([self isKindOfClass:[ASTableNode class]]) { | |
static dispatch_once_t onceTableNodeToken; | |
dispatch_once(&onceTableNodeToken, ^{ | |
swizzleInstanceUpdateMethods(self); | |
}); | |
} | |
} | |
// Ensuring the empty view stays centered is a nightware with offsets. For that reason we add it to the view controller's view, | |
// Not the collection node. This mimics the user scrollingn down on the collection node during pull to refresh. | |
if (experimentEnableGesture) { | |
// We will implement this later if we want it to scroll with the refresh | |
self.pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; | |
self.pan.delegate = self; | |
[self.view addGestureRecognizer:self.pan]; | |
} | |
} | |
- (UIView *)emptyView { | |
return objc_getAssociatedObject(self, BFREmptyDataViewPropertyKey); | |
} | |
- (void)setEmptyView:(UIView *)view { | |
objc_setAssociatedObject(self, BFREmptyDataViewPropertyKey, view, OBJC_ASSOCIATION_RETAIN_NONATOMIC); | |
} | |
- (UIPanGestureRecognizer *)pan { | |
return objc_getAssociatedObject(self, BFREmptyDataViewPanPropertyKey); | |
} | |
- (void)setPan:(UIPanGestureRecognizer *)pan { | |
objc_setAssociatedObject(self, BFREmptyDataViewPanPropertyKey, pan, OBJC_ASSOCIATION_RETAIN_NONATOMIC); | |
} | |
#pragma mark - Swizzle | |
void swizzleInstanceUpdateMethods(id self) { | |
if (!implementationLookupTable) implementationLookupTable = [[NSMutableDictionary alloc] initWithCapacity:2]; | |
if ([self isKindOfClass:[ASCollectionNode class]]) { | |
// Swizzle batchUpdates for collection node which is all we'll need | |
Method methodBatchUpdates = class_getInstanceMethod([self class], @selector(performBatchUpdates:completion:)); | |
IMP performBatchUpdates_orig = method_setImplementation(methodBatchUpdates, (IMP)swizzledBatchUpdates); | |
[implementationLookupTable setValue:[NSValue valueWithPointer:performBatchUpdates_orig] forKey:[self instanceLookupKeyForSelector:@selector(performBatchUpdates:completion:)]]; | |
} else if ([self isKindOfClass:[ASTableNode class]]) { | |
// Swizzle batchUpdates and reloadData for table node since we'll need both | |
Method methodBatchUpdates = class_getInstanceMethod([self class], @selector(performBatchUpdates:completion:)); | |
IMP performBatchUpdates_orig = method_setImplementation(methodBatchUpdates, (IMP)swizzledBatchUpdates); | |
[implementationLookupTable setValue:[NSValue valueWithPointer:performBatchUpdates_orig] forKey:[self instanceLookupKeyForSelector:@selector(performBatchUpdates:completion:)]]; | |
Method methodReloadDataWithCompletion = class_getInstanceMethod([self class], @selector(reloadDataWithCompletion:)); | |
IMP performReloadDataWithCompletion_orig = method_setImplementation(methodReloadDataWithCompletion, (IMP)swizzledReloadDataWithCompletion); | |
[implementationLookupTable setValue:[NSValue valueWithPointer:performReloadDataWithCompletion_orig] forKey:[self instanceLookupKeyForSelector:@selector(reloadDataWithCompletion:)]]; | |
} | |
} | |
void swizzledBatchUpdates(id self, SEL _cmd, void(^updates)(), void(^completion)(BOOL)) { | |
// Get the original performBatchUpdates | |
NSValue *impValue = [implementationLookupTable valueForKey:[self instanceLookupKeyForSelector:_cmd]]; | |
IMP performBatchUpdates_orig = [impValue pointerValue]; | |
// Call OG implementation for whichever instance we have | |
if (performBatchUpdates_orig) { | |
((void(*)(id,SEL, void(^updates)(), void(^completion)(BOOL)))performBatchUpdates_orig)(self,_cmd, updates, completion); | |
} | |
// Now trigger empty view | |
[self showEmptyViewIfNeeded]; | |
} | |
void swizzledReloadDataWithCompletion(id self, SEL _cmd, void(^completion)()) { | |
// Get the original reloadDataWithCompletion | |
NSValue *impValue = [implementationLookupTable valueForKey:[self instanceLookupKeyForSelector:_cmd]]; | |
IMP performReloadDataWithCompletion_orig = [impValue pointerValue]; | |
// Call OG implementation for table node | |
if (performReloadDataWithCompletion_orig) { | |
((void(*)(id,SEL, void(^completion)()))performReloadDataWithCompletion_orig)(self,_cmd, completion); | |
} | |
// Now trigger empty view | |
[self showEmptyViewIfNeeded]; | |
} | |
- (NSString *)instanceLookupKeyForSelector:(SEL)selector { | |
return [NSString stringWithFormat:@"%@-%@", NSStringFromClass([self class]), NSStringFromSelector(selector)]; | |
} | |
#pragma mark - Empty Data View Hide/Show | |
- (void)showEmptyViewIfNeeded { | |
BOOL dataIsEmpty; | |
NSInteger sections = 1; | |
NSInteger totalItems = 0; | |
if ([self isKindOfClass:[ASCollectionNode class]]) { | |
id <ASCollectionDataSource> nodeDataSource = ((ASCollectionNode *)self).dataSource; | |
if ([nodeDataSource respondsToSelector:@selector(numberOfSectionsInCollectionNode:)]) { | |
sections = [nodeDataSource numberOfSectionsInCollectionNode:(ASCollectionNode *)self]; | |
} | |
if ([nodeDataSource respondsToSelector:@selector(collectionNode:numberOfItemsInSection:)]) { | |
for (NSInteger sectionIDX = 0; sectionIDX < sections; sectionIDX++) { | |
totalItems += [nodeDataSource collectionNode:(ASCollectionNode *)self numberOfItemsInSection:sectionIDX]; | |
} | |
} | |
} else if ([self isKindOfClass:[ASTableNode class]]) { | |
id <ASTableDataSource> nodeDataSource = ((ASTableNode *)self).dataSource; | |
if ([nodeDataSource respondsToSelector:@selector(numberOfSectionsInTableNode:)]) { | |
sections = [nodeDataSource numberOfSectionsInTableNode:(ASTableNode *)self]; | |
} | |
if ([nodeDataSource respondsToSelector:@selector(tableNode:numberOfRowsInSection:)]) { | |
for (NSInteger sectionIDX = 0; sectionIDX < sections; sectionIDX++) { | |
totalItems += [nodeDataSource tableNode:(ASTableNode *)self numberOfRowsInSection:sectionIDX]; | |
} | |
} | |
} | |
dataIsEmpty = totalItems <= 0; | |
if (self.emptyDataViewDataSourceDelegate != nil && [NSThread currentThread].isMainThread) { | |
if (dataIsEmpty) { | |
if (self.emptyView.superview) [self.emptyView removeFromSuperview]; | |
self.emptyView = [self.emptyDataViewDataSourceDelegate viewForEmptyData]; | |
if (self.emptyView == nil) return; | |
// Add empty view | |
CGFloat offset = 0; | |
if ([self.emptyDataViewDataSourceDelegate respondsToSelector:@selector(offsetForEmptyView)]) { | |
offset = [self.emptyDataViewDataSourceDelegate offsetForEmptyView]; | |
} | |
if (self.closestViewController == nil) { | |
DDLogWarn(@"Closet view controller was nil when attempting to display empty data set."); | |
return; | |
} | |
[self.closestViewController.view addSubview:self.emptyView]; | |
[self.emptyView mas_makeConstraints:^(MASConstraintMaker *make) { | |
if (@available(iOS 11.0, *)) { | |
make.width.equalTo(self.closestViewController.view.mas_safeAreaLayoutGuideWidth); | |
make.height.equalTo(self.closestViewController.view.mas_safeAreaLayoutGuideHeight); | |
make.centerX.equalTo(self.closestViewController.view.mas_safeAreaLayoutGuideCenterX); | |
make.centerY.equalTo(self.closestViewController.view.mas_safeAreaLayoutGuideCenterY).with.offset(offset); | |
} else { | |
make.width.equalTo(self.closestViewController.view.mas_width); | |
make.height.equalTo(self.closestViewController.view.mas_height); | |
make.centerX.equalTo(self.closestViewController.view.mas_centerX); | |
make.centerY.equalTo(self.closestViewController.view.mas_centerY).with.offset(offset); | |
} | |
}]; | |
} else { | |
[self.emptyView removeFromSuperview]; | |
} | |
} | |
} | |
#pragma mark - UIGestureRecognizer Delegate | |
// Pans the empty view down, making it appear like it's part of the collectionnode's content view | |
- (void)handlePan:(UIPanGestureRecognizer *)pan { | |
CGPoint pointsToMove = [pan translationInView:self.emptyView]; | |
[self.emptyView setCenter:CGPointMake(self.emptyView.center.x, self.emptyView.center.y + pointsToMove.y)]; | |
[pan setTranslation:CGPointZero inView:self.emptyView]; | |
if (pan.state == UIGestureRecognizerStateEnded) { | |
[self.emptyView setCenter:self.closestViewController.view.center]; | |
} | |
} | |
// This allows users to still scroll the collection node to pull to refresh | |
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { | |
return YES; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment