You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1075 lines
37 KiB

//
// 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