12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076 |
- //
- // 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;
-
- // Configure empty dataset fade in display
- view.fadeInOnDisplay = [self dzn_shouldFadeIn];
-
- 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];
-
- [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
- {
- CGRect superviewBounds = self.superview.bounds;
- self.frame = CGRectMake(0.0, 0.0, CGRectGetWidth(superviewBounds), CGRectGetHeight(superviewBounds));
-
- 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
|