Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Empty data set for ASDisplayNode
//
// 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
You can’t perform that action at this time.