// // 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 @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 () @property (nonatomic, readonly) DZNEmptyDataSetView *emptyDataSetView; @end @implementation UIScrollView (DZNEmptyDataSet) #pragma mark - Getters (Public) - (id)emptyDataSetSource { DZNWeakObjectContainer *container = objc_getAssociatedObject(self, kEmptyDataSetSource); return container.weakObject; } - (id)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 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 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)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)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