1075 lines
37 KiB
Objective-C
1075 lines
37 KiB
Objective-C
//
|
|
// UIScrollView+EmptyDataSet.m
|
|
// DZNEmptyDataSet
|
|
// https://github.com/dzenbot/DZNEmptyDataSet
|
|
//
|
|
// Created by Ignacio Romero Zurbuchen on 6/20/14.
|
|
// Copyright (c) 2016 DZN Labs. All rights reserved.
|
|
// Licence: MIT-Licence
|
|
//
|
|
|
|
#import "UIScrollView+EmptyDataSet.h"
|
|
#import <objc/runtime.h>
|
|
|
|
@interface UIView (DZNConstraintBasedLayoutExtensions)
|
|
|
|
- (NSLayoutConstraint *)equallyRelatedConstraintWithView:(UIView *)view attribute:(NSLayoutAttribute)attribute;
|
|
|
|
@end
|
|
|
|
@interface DZNWeakObjectContainer : NSObject
|
|
|
|
@property (nonatomic, readonly, weak) id weakObject;
|
|
|
|
- (instancetype)initWithWeakObject:(id)object;
|
|
|
|
@end
|
|
|
|
@interface DZNEmptyDataSetView : UIView
|
|
|
|
@property (nonatomic, readonly) UIView *contentView;
|
|
@property (nonatomic, readonly) UILabel *titleLabel;
|
|
@property (nonatomic, readonly) UILabel *detailLabel;
|
|
@property (nonatomic, readonly) UIImageView *imageView;
|
|
@property (nonatomic, readonly) UIButton *button;
|
|
@property (nonatomic, strong) UIView *customView;
|
|
@property (nonatomic, strong) UITapGestureRecognizer *tapGesture;
|
|
|
|
@property (nonatomic, assign) CGFloat verticalOffset;
|
|
@property (nonatomic, assign) CGFloat verticalSpace;
|
|
|
|
@property (nonatomic, assign) BOOL fadeInOnDisplay;
|
|
|
|
- (void)setupConstraints;
|
|
- (void)prepareForReuse;
|
|
|
|
@end
|
|
|
|
|
|
#pragma mark - UIScrollView+EmptyDataSet
|
|
|
|
static char const * const kEmptyDataSetSource = "emptyDataSetSource";
|
|
static char const * const kEmptyDataSetDelegate = "emptyDataSetDelegate";
|
|
static char const * const kEmptyDataSetView = "emptyDataSetView";
|
|
|
|
#define kEmptyImageViewAnimationKey @"com.dzn.emptyDataSet.imageViewAnimation"
|
|
|
|
@interface UIScrollView () <UIGestureRecognizerDelegate>
|
|
@property (nonatomic, readonly) DZNEmptyDataSetView *emptyDataSetView;
|
|
@end
|
|
|
|
@implementation UIScrollView (DZNEmptyDataSet)
|
|
|
|
#pragma mark - Getters (Public)
|
|
|
|
- (id<DZNEmptyDataSetSource>)emptyDataSetSource
|
|
{
|
|
DZNWeakObjectContainer *container = objc_getAssociatedObject(self, kEmptyDataSetSource);
|
|
return container.weakObject;
|
|
}
|
|
|
|
- (id<DZNEmptyDataSetDelegate>)emptyDataSetDelegate
|
|
{
|
|
DZNWeakObjectContainer *container = objc_getAssociatedObject(self, kEmptyDataSetDelegate);
|
|
return container.weakObject;
|
|
}
|
|
|
|
- (BOOL)isEmptyDataSetVisible
|
|
{
|
|
UIView *view = objc_getAssociatedObject(self, kEmptyDataSetView);
|
|
return view ? !view.hidden : NO;
|
|
}
|
|
|
|
|
|
#pragma mark - Getters (Private)
|
|
|
|
- (DZNEmptyDataSetView *)emptyDataSetView
|
|
{
|
|
DZNEmptyDataSetView *view = objc_getAssociatedObject(self, kEmptyDataSetView);
|
|
|
|
if (!view)
|
|
{
|
|
view = [DZNEmptyDataSetView new];
|
|
view.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
|
|
view.hidden = YES;
|
|
|
|
view.tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dzn_didTapContentView:)];
|
|
view.tapGesture.delegate = self;
|
|
[view addGestureRecognizer:view.tapGesture];
|
|
|
|
[self setEmptyDataSetView:view];
|
|
}
|
|
return view;
|
|
}
|
|
|
|
- (BOOL)dzn_canDisplay
|
|
{
|
|
if (self.emptyDataSetSource && [self.emptyDataSetSource conformsToProtocol:@protocol(DZNEmptyDataSetSource)]) {
|
|
if ([self isKindOfClass:[UITableView class]] || [self isKindOfClass:[UICollectionView class]] || [self isKindOfClass:[UIScrollView class]]) {
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (NSInteger)dzn_itemsCount
|
|
{
|
|
NSInteger items = 0;
|
|
|
|
// UIScollView doesn't respond to 'dataSource' so let's exit
|
|
if (![self respondsToSelector:@selector(dataSource)]) {
|
|
return items;
|
|
}
|
|
|
|
// UITableView support
|
|
if ([self isKindOfClass:[UITableView class]]) {
|
|
|
|
UITableView *tableView = (UITableView *)self;
|
|
id <UITableViewDataSource> dataSource = tableView.dataSource;
|
|
|
|
NSInteger sections = 1;
|
|
|
|
if (dataSource && [dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) {
|
|
sections = [dataSource numberOfSectionsInTableView:tableView];
|
|
}
|
|
|
|
if (dataSource && [dataSource respondsToSelector:@selector(tableView:numberOfRowsInSection:)]) {
|
|
for (NSInteger section = 0; section < sections; section++) {
|
|
items += [dataSource tableView:tableView numberOfRowsInSection:section];
|
|
}
|
|
}
|
|
}
|
|
// UICollectionView support
|
|
else if ([self isKindOfClass:[UICollectionView class]]) {
|
|
|
|
UICollectionView *collectionView = (UICollectionView *)self;
|
|
id <UICollectionViewDataSource> dataSource = collectionView.dataSource;
|
|
|
|
NSInteger sections = 1;
|
|
|
|
if (dataSource && [dataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)]) {
|
|
sections = [dataSource numberOfSectionsInCollectionView:collectionView];
|
|
}
|
|
|
|
if (dataSource && [dataSource respondsToSelector:@selector(collectionView:numberOfItemsInSection:)]) {
|
|
for (NSInteger section = 0; section < sections; section++) {
|
|
items += [dataSource collectionView:collectionView numberOfItemsInSection:section];
|
|
}
|
|
}
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
|
|
#pragma mark - Data Source Getters
|
|
|
|
- (NSAttributedString *)dzn_titleLabelString
|
|
{
|
|
if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(titleForEmptyDataSet:)]) {
|
|
NSAttributedString *string = [self.emptyDataSetSource titleForEmptyDataSet:self];
|
|
if (string) NSAssert([string isKindOfClass:[NSAttributedString class]], @"You must return a valid NSAttributedString object for -titleForEmptyDataSet:");
|
|
return string;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (NSAttributedString *)dzn_detailLabelString
|
|
{
|
|
if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(descriptionForEmptyDataSet:)]) {
|
|
NSAttributedString *string = [self.emptyDataSetSource descriptionForEmptyDataSet:self];
|
|
if (string) NSAssert([string isKindOfClass:[NSAttributedString class]], @"You must return a valid NSAttributedString object for -descriptionForEmptyDataSet:");
|
|
return string;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (UIImage *)dzn_image
|
|
{
|
|
if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(imageForEmptyDataSet:)]) {
|
|
UIImage *image = [self.emptyDataSetSource imageForEmptyDataSet:self];
|
|
if (image) NSAssert([image isKindOfClass:[UIImage class]], @"You must return a valid UIImage object for -imageForEmptyDataSet:");
|
|
return image;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (CAAnimation *)dzn_imageAnimation
|
|
{
|
|
if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(imageAnimationForEmptyDataSet:)]) {
|
|
CAAnimation *imageAnimation = [self.emptyDataSetSource imageAnimationForEmptyDataSet:self];
|
|
if (imageAnimation) NSAssert([imageAnimation isKindOfClass:[CAAnimation class]], @"You must return a valid CAAnimation object for -imageAnimationForEmptyDataSet:");
|
|
return imageAnimation;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (UIColor *)dzn_imageTintColor
|
|
{
|
|
if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(imageTintColorForEmptyDataSet:)]) {
|
|
UIColor *color = [self.emptyDataSetSource imageTintColorForEmptyDataSet:self];
|
|
if (color) NSAssert([color isKindOfClass:[UIColor class]], @"You must return a valid UIColor object for -imageTintColorForEmptyDataSet:");
|
|
return color;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (NSAttributedString *)dzn_buttonTitleForState:(UIControlState)state
|
|
{
|
|
if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(buttonTitleForEmptyDataSet:forState:)]) {
|
|
NSAttributedString *string = [self.emptyDataSetSource buttonTitleForEmptyDataSet:self forState:state];
|
|
if (string) NSAssert([string isKindOfClass:[NSAttributedString class]], @"You must return a valid NSAttributedString object for -buttonTitleForEmptyDataSet:forState:");
|
|
return string;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (UIImage *)dzn_buttonImageForState:(UIControlState)state
|
|
{
|
|
if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(buttonImageForEmptyDataSet:forState:)]) {
|
|
UIImage *image = [self.emptyDataSetSource buttonImageForEmptyDataSet:self forState:state];
|
|
if (image) NSAssert([image isKindOfClass:[UIImage class]], @"You must return a valid UIImage object for -buttonImageForEmptyDataSet:forState:");
|
|
return image;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (UIImage *)dzn_buttonBackgroundImageForState:(UIControlState)state
|
|
{
|
|
if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(buttonBackgroundImageForEmptyDataSet:forState:)]) {
|
|
UIImage *image = [self.emptyDataSetSource buttonBackgroundImageForEmptyDataSet:self forState:state];
|
|
if (image) NSAssert([image isKindOfClass:[UIImage class]], @"You must return a valid UIImage object for -buttonBackgroundImageForEmptyDataSet:forState:");
|
|
return image;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (UIColor *)dzn_dataSetBackgroundColor
|
|
{
|
|
if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(backgroundColorForEmptyDataSet:)]) {
|
|
UIColor *color = [self.emptyDataSetSource backgroundColorForEmptyDataSet:self];
|
|
if (color) NSAssert([color isKindOfClass:[UIColor class]], @"You must return a valid UIColor object for -backgroundColorForEmptyDataSet:");
|
|
return color;
|
|
}
|
|
return [UIColor clearColor];
|
|
}
|
|
|
|
- (UIView *)dzn_customView
|
|
{
|
|
if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(customViewForEmptyDataSet:)]) {
|
|
UIView *view = [self.emptyDataSetSource customViewForEmptyDataSet:self];
|
|
if (view) NSAssert([view isKindOfClass:[UIView class]], @"You must return a valid UIView object for -customViewForEmptyDataSet:");
|
|
return view;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (CGFloat)dzn_verticalOffset
|
|
{
|
|
CGFloat offset = 0.0;
|
|
|
|
if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(verticalOffsetForEmptyDataSet:)]) {
|
|
offset = [self.emptyDataSetSource verticalOffsetForEmptyDataSet:self];
|
|
}
|
|
return offset;
|
|
}
|
|
|
|
- (CGFloat)dzn_verticalSpace
|
|
{
|
|
if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(spaceHeightForEmptyDataSet:)]) {
|
|
return [self.emptyDataSetSource spaceHeightForEmptyDataSet:self];
|
|
}
|
|
return 0.0;
|
|
}
|
|
|
|
|
|
#pragma mark - Delegate Getters & Events (Private)
|
|
|
|
- (BOOL)dzn_shouldFadeIn {
|
|
if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldFadeIn:)]) {
|
|
return [self.emptyDataSetDelegate emptyDataSetShouldFadeIn:self];
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)dzn_shouldDisplay
|
|
{
|
|
if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldDisplay:)]) {
|
|
return [self.emptyDataSetDelegate emptyDataSetShouldDisplay:self];
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)dzn_shouldBeForcedToDisplay
|
|
{
|
|
if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldBeForcedToDisplay:)]) {
|
|
return [self.emptyDataSetDelegate emptyDataSetShouldBeForcedToDisplay:self];
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)dzn_isTouchAllowed
|
|
{
|
|
if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldAllowTouch:)]) {
|
|
return [self.emptyDataSetDelegate emptyDataSetShouldAllowTouch:self];
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)dzn_isScrollAllowed
|
|
{
|
|
if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldAllowScroll:)]) {
|
|
return [self.emptyDataSetDelegate emptyDataSetShouldAllowScroll:self];
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)dzn_isImageViewAnimateAllowed
|
|
{
|
|
if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldAnimateImageView:)]) {
|
|
return [self.emptyDataSetDelegate emptyDataSetShouldAnimateImageView:self];
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (void)dzn_willAppear
|
|
{
|
|
if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetWillAppear:)]) {
|
|
[self.emptyDataSetDelegate emptyDataSetWillAppear:self];
|
|
}
|
|
}
|
|
|
|
- (void)dzn_didAppear
|
|
{
|
|
if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetDidAppear:)]) {
|
|
[self.emptyDataSetDelegate emptyDataSetDidAppear:self];
|
|
}
|
|
}
|
|
|
|
- (void)dzn_willDisappear
|
|
{
|
|
if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetWillDisappear:)]) {
|
|
[self.emptyDataSetDelegate emptyDataSetWillDisappear:self];
|
|
}
|
|
}
|
|
|
|
- (void)dzn_didDisappear
|
|
{
|
|
if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetDidDisappear:)]) {
|
|
[self.emptyDataSetDelegate emptyDataSetDidDisappear:self];
|
|
}
|
|
}
|
|
|
|
- (void)dzn_didTapContentView:(id)sender
|
|
{
|
|
if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSet:didTapView:)]) {
|
|
[self.emptyDataSetDelegate emptyDataSet:self didTapView:sender];
|
|
}
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
else if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetDidTapView:)]) {
|
|
[self.emptyDataSetDelegate emptyDataSetDidTapView:self];
|
|
}
|
|
#pragma clang diagnostic pop
|
|
}
|
|
|
|
- (void)dzn_didTapDataButton:(id)sender
|
|
{
|
|
if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSet:didTapButton:)]) {
|
|
[self.emptyDataSetDelegate emptyDataSet:self didTapButton:sender];
|
|
}
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
else if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetDidTapButton:)]) {
|
|
[self.emptyDataSetDelegate emptyDataSetDidTapButton:self];
|
|
}
|
|
#pragma clang diagnostic pop
|
|
}
|
|
|
|
|
|
#pragma mark - Setters (Public)
|
|
|
|
- (void)setEmptyDataSetSource:(id<DZNEmptyDataSetSource>)datasource
|
|
{
|
|
if (!datasource || ![self dzn_canDisplay]) {
|
|
[self dzn_invalidate];
|
|
}
|
|
|
|
objc_setAssociatedObject(self, kEmptyDataSetSource, [[DZNWeakObjectContainer alloc] initWithWeakObject:datasource], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
|
|
// We add method sizzling for injecting -dzn_reloadData implementation to the native -reloadData implementation
|
|
[self swizzleIfPossible:@selector(reloadData)];
|
|
|
|
// Exclusively for UITableView, we also inject -dzn_reloadData to -endUpdates
|
|
if ([self isKindOfClass:[UITableView class]]) {
|
|
[self swizzleIfPossible:@selector(endUpdates)];
|
|
}
|
|
}
|
|
|
|
- (void)setEmptyDataSetDelegate:(id<DZNEmptyDataSetDelegate>)delegate
|
|
{
|
|
if (!delegate) {
|
|
[self dzn_invalidate];
|
|
}
|
|
|
|
objc_setAssociatedObject(self, kEmptyDataSetDelegate, [[DZNWeakObjectContainer alloc] initWithWeakObject:delegate], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
}
|
|
|
|
|
|
#pragma mark - Setters (Private)
|
|
|
|
- (void)setEmptyDataSetView:(DZNEmptyDataSetView *)view
|
|
{
|
|
objc_setAssociatedObject(self, kEmptyDataSetView, view, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
}
|
|
|
|
|
|
#pragma mark - Reload APIs (Public)
|
|
|
|
- (void)reloadEmptyDataSet
|
|
{
|
|
[self dzn_reloadEmptyDataSet];
|
|
}
|
|
|
|
|
|
#pragma mark - Reload APIs (Private)
|
|
|
|
- (void)dzn_reloadEmptyDataSet
|
|
{
|
|
if (![self dzn_canDisplay]) {
|
|
return;
|
|
}
|
|
|
|
if (([self dzn_shouldDisplay] && [self dzn_itemsCount] == 0) || [self dzn_shouldBeForcedToDisplay])
|
|
{
|
|
// Notifies that the empty dataset view will appear
|
|
[self dzn_willAppear];
|
|
|
|
DZNEmptyDataSetView *view = self.emptyDataSetView;
|
|
|
|
if (!view.superview) {
|
|
// Send the view all the way to the back, in case a header and/or footer is present, as well as for sectionHeaders or any other content
|
|
if (([self isKindOfClass:[UITableView class]] || [self isKindOfClass:[UICollectionView class]]) && self.subviews.count > 1) {
|
|
[self insertSubview:view atIndex:0];
|
|
}
|
|
else {
|
|
[self addSubview:view];
|
|
}
|
|
}
|
|
|
|
// Removing view resetting the view and its constraints it very important to guarantee a good state
|
|
[view prepareForReuse];
|
|
|
|
UIView *customView = [self dzn_customView];
|
|
|
|
// If a non-nil custom view is available, let's configure it instead
|
|
if (customView) {
|
|
view.customView = customView;
|
|
}
|
|
else {
|
|
// Get the data from the data source
|
|
NSAttributedString *titleLabelString = [self dzn_titleLabelString];
|
|
NSAttributedString *detailLabelString = [self dzn_detailLabelString];
|
|
|
|
UIImage *buttonImage = [self dzn_buttonImageForState:UIControlStateNormal];
|
|
NSAttributedString *buttonTitle = [self dzn_buttonTitleForState:UIControlStateNormal];
|
|
|
|
UIImage *image = [self dzn_image];
|
|
UIColor *imageTintColor = [self dzn_imageTintColor];
|
|
UIImageRenderingMode renderingMode = imageTintColor ? UIImageRenderingModeAlwaysTemplate : UIImageRenderingModeAlwaysOriginal;
|
|
|
|
view.verticalSpace = [self dzn_verticalSpace];
|
|
|
|
// Configure Image
|
|
if (image) {
|
|
if ([image respondsToSelector:@selector(imageWithRenderingMode:)]) {
|
|
view.imageView.image = [image imageWithRenderingMode:renderingMode];
|
|
view.imageView.tintColor = imageTintColor;
|
|
}
|
|
else {
|
|
// iOS 6 fallback: insert code to convert imaged if needed
|
|
view.imageView.image = image;
|
|
}
|
|
}
|
|
|
|
// Configure title label
|
|
if (titleLabelString) {
|
|
view.titleLabel.attributedText = titleLabelString;
|
|
}
|
|
|
|
// Configure detail label
|
|
if (detailLabelString) {
|
|
view.detailLabel.attributedText = detailLabelString;
|
|
}
|
|
|
|
// Configure button
|
|
if (buttonImage) {
|
|
[view.button setImage:buttonImage forState:UIControlStateNormal];
|
|
[view.button setImage:[self dzn_buttonImageForState:UIControlStateHighlighted] forState:UIControlStateHighlighted];
|
|
}
|
|
else if (buttonTitle) {
|
|
[view.button setAttributedTitle:buttonTitle forState:UIControlStateNormal];
|
|
[view.button setAttributedTitle:[self dzn_buttonTitleForState:UIControlStateHighlighted] forState:UIControlStateHighlighted];
|
|
[view.button setBackgroundImage:[self dzn_buttonBackgroundImageForState:UIControlStateNormal] forState:UIControlStateNormal];
|
|
[view.button setBackgroundImage:[self dzn_buttonBackgroundImageForState:UIControlStateHighlighted] forState:UIControlStateHighlighted];
|
|
}
|
|
}
|
|
|
|
// Configure offset
|
|
view.verticalOffset = [self dzn_verticalOffset];
|
|
|
|
// Configure the empty dataset view
|
|
view.backgroundColor = [self dzn_dataSetBackgroundColor];
|
|
view.hidden = NO;
|
|
view.clipsToBounds = YES;
|
|
|
|
// Configure empty dataset userInteraction permission
|
|
view.userInteractionEnabled = [self dzn_isTouchAllowed];
|
|
|
|
// Configure empty dataset fade in display
|
|
view.fadeInOnDisplay = [self dzn_shouldFadeIn];
|
|
|
|
[view setupConstraints];
|
|
|
|
[UIView performWithoutAnimation:^{
|
|
[view layoutIfNeeded];
|
|
}];
|
|
|
|
// Configure scroll permission
|
|
self.scrollEnabled = [self dzn_isScrollAllowed];
|
|
|
|
// Configure image view animation
|
|
if ([self dzn_isImageViewAnimateAllowed])
|
|
{
|
|
CAAnimation *animation = [self dzn_imageAnimation];
|
|
|
|
if (animation) {
|
|
[self.emptyDataSetView.imageView.layer addAnimation:animation forKey:kEmptyImageViewAnimationKey];
|
|
}
|
|
}
|
|
else if ([self.emptyDataSetView.imageView.layer animationForKey:kEmptyImageViewAnimationKey]) {
|
|
[self.emptyDataSetView.imageView.layer removeAnimationForKey:kEmptyImageViewAnimationKey];
|
|
}
|
|
|
|
// Notifies that the empty dataset view did appear
|
|
[self dzn_didAppear];
|
|
}
|
|
else if (self.isEmptyDataSetVisible) {
|
|
[self dzn_invalidate];
|
|
}
|
|
}
|
|
|
|
- (void)dzn_invalidate
|
|
{
|
|
// Notifies that the empty dataset view will disappear
|
|
[self dzn_willDisappear];
|
|
|
|
if (self.emptyDataSetView) {
|
|
[self.emptyDataSetView prepareForReuse];
|
|
[self.emptyDataSetView removeFromSuperview];
|
|
|
|
[self setEmptyDataSetView:nil];
|
|
}
|
|
|
|
self.scrollEnabled = YES;
|
|
|
|
// Notifies that the empty dataset view did disappear
|
|
[self dzn_didDisappear];
|
|
}
|
|
|
|
|
|
#pragma mark - Method Swizzling
|
|
|
|
static NSMutableDictionary *_impLookupTable;
|
|
static NSString *const DZNSwizzleInfoPointerKey = @"pointer";
|
|
static NSString *const DZNSwizzleInfoOwnerKey = @"owner";
|
|
static NSString *const DZNSwizzleInfoSelectorKey = @"selector";
|
|
|
|
// Based on Bryce Buchanan's swizzling technique http://blog.newrelic.com/2014/04/16/right-way-to-swizzle/
|
|
// And Juzzin's ideas https://github.com/juzzin/JUSEmptyViewController
|
|
|
|
void dzn_original_implementation(id self, SEL _cmd)
|
|
{
|
|
// Fetch original implementation from lookup table
|
|
Class baseClass = dzn_baseClassToSwizzleForTarget(self);
|
|
NSString *key = dzn_implementationKey(baseClass, _cmd);
|
|
|
|
NSDictionary *swizzleInfo = [_impLookupTable objectForKey:key];
|
|
NSValue *impValue = [swizzleInfo valueForKey:DZNSwizzleInfoPointerKey];
|
|
|
|
IMP impPointer = [impValue pointerValue];
|
|
|
|
// We then inject the additional implementation for reloading the empty dataset
|
|
// Doing it before calling the original implementation does update the 'isEmptyDataSetVisible' flag on time.
|
|
[self dzn_reloadEmptyDataSet];
|
|
|
|
// If found, call original implementation
|
|
if (impPointer) {
|
|
((void(*)(id,SEL))impPointer)(self,_cmd);
|
|
}
|
|
}
|
|
|
|
NSString *dzn_implementationKey(Class class, SEL selector)
|
|
{
|
|
if (!class || !selector) {
|
|
return nil;
|
|
}
|
|
|
|
NSString *className = NSStringFromClass([class class]);
|
|
|
|
NSString *selectorName = NSStringFromSelector(selector);
|
|
return [NSString stringWithFormat:@"%@_%@",className,selectorName];
|
|
}
|
|
|
|
Class dzn_baseClassToSwizzleForTarget(id target)
|
|
{
|
|
if ([target isKindOfClass:[UITableView class]]) {
|
|
return [UITableView class];
|
|
}
|
|
else if ([target isKindOfClass:[UICollectionView class]]) {
|
|
return [UICollectionView class];
|
|
}
|
|
else if ([target isKindOfClass:[UIScrollView class]]) {
|
|
return [UIScrollView class];
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (void)swizzleIfPossible:(SEL)selector
|
|
{
|
|
// Check if the target responds to selector
|
|
if (![self respondsToSelector:selector]) {
|
|
return;
|
|
}
|
|
|
|
// Create the lookup table
|
|
if (!_impLookupTable) {
|
|
_impLookupTable = [[NSMutableDictionary alloc] initWithCapacity:3]; // 3 represent the supported base classes
|
|
}
|
|
|
|
// We make sure that setImplementation is called once per class kind, UITableView or UICollectionView.
|
|
for (NSDictionary *info in [_impLookupTable allValues]) {
|
|
Class class = [info objectForKey:DZNSwizzleInfoOwnerKey];
|
|
NSString *selectorName = [info objectForKey:DZNSwizzleInfoSelectorKey];
|
|
|
|
if ([selectorName isEqualToString:NSStringFromSelector(selector)]) {
|
|
if ([self isKindOfClass:class]) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
Class baseClass = dzn_baseClassToSwizzleForTarget(self);
|
|
NSString *key = dzn_implementationKey(baseClass, selector);
|
|
NSValue *impValue = [[_impLookupTable objectForKey:key] valueForKey:DZNSwizzleInfoPointerKey];
|
|
|
|
// If the implementation for this class already exist, skip!!
|
|
if (impValue || !key || !baseClass) {
|
|
return;
|
|
}
|
|
|
|
// Swizzle by injecting additional implementation
|
|
Method method = class_getInstanceMethod(baseClass, selector);
|
|
IMP dzn_newImplementation = method_setImplementation(method, (IMP)dzn_original_implementation);
|
|
|
|
// Store the new implementation in the lookup table
|
|
NSDictionary *swizzledInfo = @{DZNSwizzleInfoOwnerKey: baseClass,
|
|
DZNSwizzleInfoSelectorKey: NSStringFromSelector(selector),
|
|
DZNSwizzleInfoPointerKey: [NSValue valueWithPointer:dzn_newImplementation]};
|
|
|
|
[_impLookupTable setObject:swizzledInfo forKey:key];
|
|
}
|
|
|
|
|
|
#pragma mark - UIGestureRecognizerDelegate Methods
|
|
|
|
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
|
|
{
|
|
if ([gestureRecognizer.view isEqual:self.emptyDataSetView]) {
|
|
return [self dzn_isTouchAllowed];
|
|
}
|
|
|
|
return [super gestureRecognizerShouldBegin:gestureRecognizer];
|
|
}
|
|
|
|
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
|
|
{
|
|
UIGestureRecognizer *tapGesture = self.emptyDataSetView.tapGesture;
|
|
|
|
if ([gestureRecognizer isEqual:tapGesture] || [otherGestureRecognizer isEqual:tapGesture]) {
|
|
return YES;
|
|
}
|
|
|
|
// defer to emptyDataSetDelegate's implementation if available
|
|
if ( (self.emptyDataSetDelegate != (id)self) && [self.emptyDataSetDelegate respondsToSelector:@selector(gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:)]) {
|
|
return [(id)self.emptyDataSetDelegate gestureRecognizer:gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:otherGestureRecognizer];
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
#pragma mark - DZNEmptyDataSetView
|
|
|
|
@interface DZNEmptyDataSetView ()
|
|
@end
|
|
|
|
@implementation DZNEmptyDataSetView
|
|
@synthesize contentView = _contentView;
|
|
@synthesize titleLabel = _titleLabel, detailLabel = _detailLabel, imageView = _imageView, button = _button;
|
|
|
|
#pragma mark - Initialization Methods
|
|
|
|
- (instancetype)init
|
|
{
|
|
self = [super init];
|
|
if (self) {
|
|
[self addSubview:self.contentView];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)didMoveToSuperview
|
|
{
|
|
self.frame = self.superview.bounds;
|
|
|
|
void(^fadeInBlock)(void) = ^{_contentView.alpha = 1.0;};
|
|
|
|
if (self.fadeInOnDisplay) {
|
|
[UIView animateWithDuration:0.25
|
|
animations:fadeInBlock
|
|
completion:NULL];
|
|
}
|
|
else {
|
|
fadeInBlock();
|
|
}
|
|
}
|
|
|
|
|
|
#pragma mark - Getters
|
|
|
|
- (UIView *)contentView
|
|
{
|
|
if (!_contentView)
|
|
{
|
|
_contentView = [UIView new];
|
|
_contentView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
_contentView.backgroundColor = [UIColor clearColor];
|
|
_contentView.userInteractionEnabled = YES;
|
|
_contentView.alpha = 0;
|
|
}
|
|
return _contentView;
|
|
}
|
|
|
|
- (UIImageView *)imageView
|
|
{
|
|
if (!_imageView)
|
|
{
|
|
_imageView = [UIImageView new];
|
|
_imageView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
_imageView.backgroundColor = [UIColor clearColor];
|
|
_imageView.contentMode = UIViewContentModeScaleAspectFit;
|
|
_imageView.userInteractionEnabled = NO;
|
|
_imageView.accessibilityIdentifier = @"empty set background image";
|
|
|
|
[_contentView addSubview:_imageView];
|
|
}
|
|
return _imageView;
|
|
}
|
|
|
|
- (UILabel *)titleLabel
|
|
{
|
|
if (!_titleLabel)
|
|
{
|
|
_titleLabel = [UILabel new];
|
|
_titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
|
_titleLabel.backgroundColor = [UIColor clearColor];
|
|
|
|
_titleLabel.font = [UIFont systemFontOfSize:27.0];
|
|
_titleLabel.textColor = [UIColor colorWithWhite:0.6 alpha:1.0];
|
|
_titleLabel.textAlignment = NSTextAlignmentCenter;
|
|
_titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
|
_titleLabel.numberOfLines = 0;
|
|
_titleLabel.accessibilityIdentifier = @"empty set title";
|
|
|
|
[_contentView addSubview:_titleLabel];
|
|
}
|
|
return _titleLabel;
|
|
}
|
|
|
|
- (UILabel *)detailLabel
|
|
{
|
|
if (!_detailLabel)
|
|
{
|
|
_detailLabel = [UILabel new];
|
|
_detailLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
|
_detailLabel.backgroundColor = [UIColor clearColor];
|
|
|
|
_detailLabel.font = [UIFont systemFontOfSize:17.0];
|
|
_detailLabel.textColor = [UIColor colorWithWhite:0.6 alpha:1.0];
|
|
_detailLabel.textAlignment = NSTextAlignmentCenter;
|
|
_detailLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
|
_detailLabel.numberOfLines = 0;
|
|
_detailLabel.accessibilityIdentifier = @"empty set detail label";
|
|
|
|
[_contentView addSubview:_detailLabel];
|
|
}
|
|
return _detailLabel;
|
|
}
|
|
|
|
- (UIButton *)button
|
|
{
|
|
if (!_button)
|
|
{
|
|
_button = [UIButton buttonWithType:UIButtonTypeCustom];
|
|
_button.translatesAutoresizingMaskIntoConstraints = NO;
|
|
_button.backgroundColor = [UIColor clearColor];
|
|
_button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
|
|
_button.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
|
|
_button.accessibilityIdentifier = @"empty set button";
|
|
|
|
[_button addTarget:self action:@selector(didTapButton:) forControlEvents:UIControlEventTouchUpInside];
|
|
|
|
[_contentView addSubview:_button];
|
|
}
|
|
return _button;
|
|
}
|
|
|
|
- (BOOL)canShowImage
|
|
{
|
|
return (_imageView.image && _imageView.superview);
|
|
}
|
|
|
|
- (BOOL)canShowTitle
|
|
{
|
|
return (_titleLabel.attributedText.string.length > 0 && _titleLabel.superview);
|
|
}
|
|
|
|
- (BOOL)canShowDetail
|
|
{
|
|
return (_detailLabel.attributedText.string.length > 0 && _detailLabel.superview);
|
|
}
|
|
|
|
- (BOOL)canShowButton
|
|
{
|
|
if ([_button attributedTitleForState:UIControlStateNormal].string.length > 0 || [_button imageForState:UIControlStateNormal]) {
|
|
return (_button.superview != nil);
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
|
|
#pragma mark - Setters
|
|
|
|
- (void)setCustomView:(UIView *)view
|
|
{
|
|
if (!view) {
|
|
return;
|
|
}
|
|
|
|
if (_customView) {
|
|
[_customView removeFromSuperview];
|
|
_customView = nil;
|
|
}
|
|
|
|
_customView = view;
|
|
_customView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
[self.contentView addSubview:_customView];
|
|
}
|
|
|
|
|
|
#pragma mark - Action Methods
|
|
|
|
- (void)didTapButton:(id)sender
|
|
{
|
|
SEL selector = NSSelectorFromString(@"dzn_didTapDataButton:");
|
|
|
|
if ([self.superview respondsToSelector:selector]) {
|
|
[self.superview performSelector:selector withObject:sender afterDelay:0.0f];
|
|
}
|
|
}
|
|
|
|
- (void)removeAllConstraints
|
|
{
|
|
[self removeConstraints:self.constraints];
|
|
[_contentView removeConstraints:_contentView.constraints];
|
|
}
|
|
|
|
- (void)prepareForReuse
|
|
{
|
|
[self.contentView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
|
|
|
|
_titleLabel = nil;
|
|
_detailLabel = nil;
|
|
_imageView = nil;
|
|
_button = nil;
|
|
_customView = nil;
|
|
|
|
[self removeAllConstraints];
|
|
}
|
|
|
|
|
|
#pragma mark - Auto-Layout Configuration
|
|
|
|
- (void)setupConstraints
|
|
{
|
|
// First, configure the content view constaints
|
|
// The content view must alway be centered to its superview
|
|
NSLayoutConstraint *centerXConstraint = [self equallyRelatedConstraintWithView:self.contentView attribute:NSLayoutAttributeCenterX];
|
|
NSLayoutConstraint *centerYConstraint = [self equallyRelatedConstraintWithView:self.contentView attribute:NSLayoutAttributeCenterY];
|
|
|
|
[self addConstraint:centerXConstraint];
|
|
[self addConstraint:centerYConstraint];
|
|
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentView]|" options:0 metrics:nil views:@{@"contentView": self.contentView}]];
|
|
|
|
// When a custom offset is available, we adjust the vertical constraints' constants
|
|
if (self.verticalOffset != 0 && self.constraints.count > 0) {
|
|
centerYConstraint.constant = self.verticalOffset;
|
|
}
|
|
|
|
// If applicable, set the custom view's constraints
|
|
if (_customView) {
|
|
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[customView]|" options:0 metrics:nil views:@{@"customView":_customView}]];
|
|
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[customView]|" options:0 metrics:nil views:@{@"customView":_customView}]];
|
|
}
|
|
else {
|
|
CGFloat width = CGRectGetWidth(self.frame) ? : CGRectGetWidth([UIScreen mainScreen].bounds);
|
|
CGFloat padding = roundf(width/16.0);
|
|
CGFloat verticalSpace = self.verticalSpace ? : 11.0; // Default is 11 pts
|
|
|
|
NSMutableArray *subviewStrings = [NSMutableArray array];
|
|
NSMutableDictionary *views = [NSMutableDictionary dictionary];
|
|
NSDictionary *metrics = @{@"padding": @(padding)};
|
|
|
|
// Assign the image view's horizontal constraints
|
|
if (_imageView.superview) {
|
|
|
|
[subviewStrings addObject:@"imageView"];
|
|
views[[subviewStrings lastObject]] = _imageView;
|
|
|
|
[self.contentView addConstraint:[self.contentView equallyRelatedConstraintWithView:_imageView attribute:NSLayoutAttributeCenterX]];
|
|
}
|
|
|
|
// Assign the title label's horizontal constraints
|
|
if ([self canShowTitle]) {
|
|
|
|
[subviewStrings addObject:@"titleLabel"];
|
|
views[[subviewStrings lastObject]] = _titleLabel;
|
|
|
|
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(padding@750)-[titleLabel(>=0)]-(padding@750)-|"
|
|
options:0 metrics:metrics views:views]];
|
|
}
|
|
// or removes from its superview
|
|
else {
|
|
[_titleLabel removeFromSuperview];
|
|
_titleLabel = nil;
|
|
}
|
|
|
|
// Assign the detail label's horizontal constraints
|
|
if ([self canShowDetail]) {
|
|
|
|
[subviewStrings addObject:@"detailLabel"];
|
|
views[[subviewStrings lastObject]] = _detailLabel;
|
|
|
|
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(padding@750)-[detailLabel(>=0)]-(padding@750)-|"
|
|
options:0 metrics:metrics views:views]];
|
|
}
|
|
// or removes from its superview
|
|
else {
|
|
[_detailLabel removeFromSuperview];
|
|
_detailLabel = nil;
|
|
}
|
|
|
|
// Assign the button's horizontal constraints
|
|
if ([self canShowButton]) {
|
|
|
|
[subviewStrings addObject:@"button"];
|
|
views[[subviewStrings lastObject]] = _button;
|
|
|
|
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(padding@750)-[button(>=0)]-(padding@750)-|"
|
|
options:0 metrics:metrics views:views]];
|
|
}
|
|
// or removes from its superview
|
|
else {
|
|
[_button removeFromSuperview];
|
|
_button = nil;
|
|
}
|
|
|
|
|
|
NSMutableString *verticalFormat = [NSMutableString new];
|
|
|
|
// Build a dynamic string format for the vertical constraints, adding a margin between each element. Default is 11 pts.
|
|
for (int i = 0; i < subviewStrings.count; i++) {
|
|
|
|
NSString *string = subviewStrings[i];
|
|
[verticalFormat appendFormat:@"[%@]", string];
|
|
|
|
if (i < subviewStrings.count-1) {
|
|
[verticalFormat appendFormat:@"-(%.f@750)-", verticalSpace];
|
|
}
|
|
}
|
|
|
|
// Assign the vertical constraints to the content view
|
|
if (verticalFormat.length > 0) {
|
|
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:@"V:|%@|", verticalFormat]
|
|
options:0 metrics:metrics views:views]];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
|
|
{
|
|
UIView *hitView = [super hitTest:point withEvent:event];
|
|
|
|
// Return any UIControl instance such as buttons, segmented controls, switches, etc.
|
|
if ([hitView isKindOfClass:[UIControl class]]) {
|
|
return hitView;
|
|
}
|
|
|
|
// Return either the contentView or customView
|
|
if ([hitView isEqual:_contentView] || [hitView isEqual:_customView]) {
|
|
return hitView;
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
#pragma mark - UIView+DZNConstraintBasedLayoutExtensions
|
|
|
|
@implementation UIView (DZNConstraintBasedLayoutExtensions)
|
|
|
|
- (NSLayoutConstraint *)equallyRelatedConstraintWithView:(UIView *)view attribute:(NSLayoutAttribute)attribute
|
|
{
|
|
return [NSLayoutConstraint constraintWithItem:view
|
|
attribute:attribute
|
|
relatedBy:NSLayoutRelationEqual
|
|
toItem:self
|
|
attribute:attribute
|
|
multiplier:1.0
|
|
constant:0.0];
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark - DZNWeakObjectContainer
|
|
|
|
@implementation DZNWeakObjectContainer
|
|
|
|
- (instancetype)initWithWeakObject:(id)object
|
|
{
|
|
self = [super init];
|
|
if (self) {
|
|
_weakObject = object;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
@end
|