UIScrollView+EmptyDataSet.m 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076
  1. //
  2. // UIScrollView+EmptyDataSet.m
  3. // DZNEmptyDataSet
  4. // https://github.com/dzenbot/DZNEmptyDataSet
  5. //
  6. // Created by Ignacio Romero Zurbuchen on 6/20/14.
  7. // Copyright (c) 2016 DZN Labs. All rights reserved.
  8. // Licence: MIT-Licence
  9. //
  10. #import "UIScrollView+EmptyDataSet.h"
  11. #import <objc/runtime.h>
  12. @interface UIView (DZNConstraintBasedLayoutExtensions)
  13. - (NSLayoutConstraint *)equallyRelatedConstraintWithView:(UIView *)view attribute:(NSLayoutAttribute)attribute;
  14. @end
  15. @interface DZNWeakObjectContainer : NSObject
  16. @property (nonatomic, readonly, weak) id weakObject;
  17. - (instancetype)initWithWeakObject:(id)object;
  18. @end
  19. @interface DZNEmptyDataSetView : UIView
  20. @property (nonatomic, readonly) UIView *contentView;
  21. @property (nonatomic, readonly) UILabel *titleLabel;
  22. @property (nonatomic, readonly) UILabel *detailLabel;
  23. @property (nonatomic, readonly) UIImageView *imageView;
  24. @property (nonatomic, readonly) UIButton *button;
  25. @property (nonatomic, strong) UIView *customView;
  26. @property (nonatomic, strong) UITapGestureRecognizer *tapGesture;
  27. @property (nonatomic, assign) CGFloat verticalOffset;
  28. @property (nonatomic, assign) CGFloat verticalSpace;
  29. @property (nonatomic, assign) BOOL fadeInOnDisplay;
  30. - (void)setupConstraints;
  31. - (void)prepareForReuse;
  32. @end
  33. #pragma mark - UIScrollView+EmptyDataSet
  34. static char const * const kEmptyDataSetSource = "emptyDataSetSource";
  35. static char const * const kEmptyDataSetDelegate = "emptyDataSetDelegate";
  36. static char const * const kEmptyDataSetView = "emptyDataSetView";
  37. #define kEmptyImageViewAnimationKey @"com.dzn.emptyDataSet.imageViewAnimation"
  38. @interface UIScrollView () <UIGestureRecognizerDelegate>
  39. @property (nonatomic, readonly) DZNEmptyDataSetView *emptyDataSetView;
  40. @end
  41. @implementation UIScrollView (DZNEmptyDataSet)
  42. #pragma mark - Getters (Public)
  43. - (id<DZNEmptyDataSetSource>)emptyDataSetSource
  44. {
  45. DZNWeakObjectContainer *container = objc_getAssociatedObject(self, kEmptyDataSetSource);
  46. return container.weakObject;
  47. }
  48. - (id<DZNEmptyDataSetDelegate>)emptyDataSetDelegate
  49. {
  50. DZNWeakObjectContainer *container = objc_getAssociatedObject(self, kEmptyDataSetDelegate);
  51. return container.weakObject;
  52. }
  53. - (BOOL)isEmptyDataSetVisible
  54. {
  55. UIView *view = objc_getAssociatedObject(self, kEmptyDataSetView);
  56. return view ? !view.hidden : NO;
  57. }
  58. #pragma mark - Getters (Private)
  59. - (DZNEmptyDataSetView *)emptyDataSetView
  60. {
  61. DZNEmptyDataSetView *view = objc_getAssociatedObject(self, kEmptyDataSetView);
  62. if (!view)
  63. {
  64. view = [DZNEmptyDataSetView new];
  65. view.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
  66. view.hidden = YES;
  67. view.tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dzn_didTapContentView:)];
  68. view.tapGesture.delegate = self;
  69. [view addGestureRecognizer:view.tapGesture];
  70. [self setEmptyDataSetView:view];
  71. }
  72. return view;
  73. }
  74. - (BOOL)dzn_canDisplay
  75. {
  76. if (self.emptyDataSetSource && [self.emptyDataSetSource conformsToProtocol:@protocol(DZNEmptyDataSetSource)]) {
  77. if ([self isKindOfClass:[UITableView class]] || [self isKindOfClass:[UICollectionView class]] || [self isKindOfClass:[UIScrollView class]]) {
  78. return YES;
  79. }
  80. }
  81. return NO;
  82. }
  83. - (NSInteger)dzn_itemsCount
  84. {
  85. NSInteger items = 0;
  86. // UIScollView doesn't respond to 'dataSource' so let's exit
  87. if (![self respondsToSelector:@selector(dataSource)]) {
  88. return items;
  89. }
  90. // UITableView support
  91. if ([self isKindOfClass:[UITableView class]]) {
  92. UITableView *tableView = (UITableView *)self;
  93. id <UITableViewDataSource> dataSource = tableView.dataSource;
  94. NSInteger sections = 1;
  95. if (dataSource && [dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) {
  96. sections = [dataSource numberOfSectionsInTableView:tableView];
  97. }
  98. if (dataSource && [dataSource respondsToSelector:@selector(tableView:numberOfRowsInSection:)]) {
  99. for (NSInteger section = 0; section < sections; section++) {
  100. items += [dataSource tableView:tableView numberOfRowsInSection:section];
  101. }
  102. }
  103. }
  104. // UICollectionView support
  105. else if ([self isKindOfClass:[UICollectionView class]]) {
  106. UICollectionView *collectionView = (UICollectionView *)self;
  107. id <UICollectionViewDataSource> dataSource = collectionView.dataSource;
  108. NSInteger sections = 1;
  109. if (dataSource && [dataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)]) {
  110. sections = [dataSource numberOfSectionsInCollectionView:collectionView];
  111. }
  112. if (dataSource && [dataSource respondsToSelector:@selector(collectionView:numberOfItemsInSection:)]) {
  113. for (NSInteger section = 0; section < sections; section++) {
  114. items += [dataSource collectionView:collectionView numberOfItemsInSection:section];
  115. }
  116. }
  117. }
  118. return items;
  119. }
  120. #pragma mark - Data Source Getters
  121. - (NSAttributedString *)dzn_titleLabelString
  122. {
  123. if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(titleForEmptyDataSet:)]) {
  124. NSAttributedString *string = [self.emptyDataSetSource titleForEmptyDataSet:self];
  125. if (string) NSAssert([string isKindOfClass:[NSAttributedString class]], @"You must return a valid NSAttributedString object for -titleForEmptyDataSet:");
  126. return string;
  127. }
  128. return nil;
  129. }
  130. - (NSAttributedString *)dzn_detailLabelString
  131. {
  132. if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(descriptionForEmptyDataSet:)]) {
  133. NSAttributedString *string = [self.emptyDataSetSource descriptionForEmptyDataSet:self];
  134. if (string) NSAssert([string isKindOfClass:[NSAttributedString class]], @"You must return a valid NSAttributedString object for -descriptionForEmptyDataSet:");
  135. return string;
  136. }
  137. return nil;
  138. }
  139. - (UIImage *)dzn_image
  140. {
  141. if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(imageForEmptyDataSet:)]) {
  142. UIImage *image = [self.emptyDataSetSource imageForEmptyDataSet:self];
  143. if (image) NSAssert([image isKindOfClass:[UIImage class]], @"You must return a valid UIImage object for -imageForEmptyDataSet:");
  144. return image;
  145. }
  146. return nil;
  147. }
  148. - (CAAnimation *)dzn_imageAnimation
  149. {
  150. if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(imageAnimationForEmptyDataSet:)]) {
  151. CAAnimation *imageAnimation = [self.emptyDataSetSource imageAnimationForEmptyDataSet:self];
  152. if (imageAnimation) NSAssert([imageAnimation isKindOfClass:[CAAnimation class]], @"You must return a valid CAAnimation object for -imageAnimationForEmptyDataSet:");
  153. return imageAnimation;
  154. }
  155. return nil;
  156. }
  157. - (UIColor *)dzn_imageTintColor
  158. {
  159. if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(imageTintColorForEmptyDataSet:)]) {
  160. UIColor *color = [self.emptyDataSetSource imageTintColorForEmptyDataSet:self];
  161. if (color) NSAssert([color isKindOfClass:[UIColor class]], @"You must return a valid UIColor object for -imageTintColorForEmptyDataSet:");
  162. return color;
  163. }
  164. return nil;
  165. }
  166. - (NSAttributedString *)dzn_buttonTitleForState:(UIControlState)state
  167. {
  168. if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(buttonTitleForEmptyDataSet:forState:)]) {
  169. NSAttributedString *string = [self.emptyDataSetSource buttonTitleForEmptyDataSet:self forState:state];
  170. if (string) NSAssert([string isKindOfClass:[NSAttributedString class]], @"You must return a valid NSAttributedString object for -buttonTitleForEmptyDataSet:forState:");
  171. return string;
  172. }
  173. return nil;
  174. }
  175. - (UIImage *)dzn_buttonImageForState:(UIControlState)state
  176. {
  177. if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(buttonImageForEmptyDataSet:forState:)]) {
  178. UIImage *image = [self.emptyDataSetSource buttonImageForEmptyDataSet:self forState:state];
  179. if (image) NSAssert([image isKindOfClass:[UIImage class]], @"You must return a valid UIImage object for -buttonImageForEmptyDataSet:forState:");
  180. return image;
  181. }
  182. return nil;
  183. }
  184. - (UIImage *)dzn_buttonBackgroundImageForState:(UIControlState)state
  185. {
  186. if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(buttonBackgroundImageForEmptyDataSet:forState:)]) {
  187. UIImage *image = [self.emptyDataSetSource buttonBackgroundImageForEmptyDataSet:self forState:state];
  188. if (image) NSAssert([image isKindOfClass:[UIImage class]], @"You must return a valid UIImage object for -buttonBackgroundImageForEmptyDataSet:forState:");
  189. return image;
  190. }
  191. return nil;
  192. }
  193. - (UIColor *)dzn_dataSetBackgroundColor
  194. {
  195. if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(backgroundColorForEmptyDataSet:)]) {
  196. UIColor *color = [self.emptyDataSetSource backgroundColorForEmptyDataSet:self];
  197. if (color) NSAssert([color isKindOfClass:[UIColor class]], @"You must return a valid UIColor object for -backgroundColorForEmptyDataSet:");
  198. return color;
  199. }
  200. return [UIColor clearColor];
  201. }
  202. - (UIView *)dzn_customView
  203. {
  204. if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(customViewForEmptyDataSet:)]) {
  205. UIView *view = [self.emptyDataSetSource customViewForEmptyDataSet:self];
  206. if (view) NSAssert([view isKindOfClass:[UIView class]], @"You must return a valid UIView object for -customViewForEmptyDataSet:");
  207. return view;
  208. }
  209. return nil;
  210. }
  211. - (CGFloat)dzn_verticalOffset
  212. {
  213. CGFloat offset = 0.0;
  214. if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(verticalOffsetForEmptyDataSet:)]) {
  215. offset = [self.emptyDataSetSource verticalOffsetForEmptyDataSet:self];
  216. }
  217. return offset;
  218. }
  219. - (CGFloat)dzn_verticalSpace
  220. {
  221. if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(spaceHeightForEmptyDataSet:)]) {
  222. return [self.emptyDataSetSource spaceHeightForEmptyDataSet:self];
  223. }
  224. return 0.0;
  225. }
  226. #pragma mark - Delegate Getters & Events (Private)
  227. - (BOOL)dzn_shouldFadeIn {
  228. if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldFadeIn:)]) {
  229. return [self.emptyDataSetDelegate emptyDataSetShouldFadeIn:self];
  230. }
  231. return YES;
  232. }
  233. - (BOOL)dzn_shouldDisplay
  234. {
  235. if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldDisplay:)]) {
  236. return [self.emptyDataSetDelegate emptyDataSetShouldDisplay:self];
  237. }
  238. return YES;
  239. }
  240. - (BOOL)dzn_shouldBeForcedToDisplay
  241. {
  242. if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldBeForcedToDisplay:)]) {
  243. return [self.emptyDataSetDelegate emptyDataSetShouldBeForcedToDisplay:self];
  244. }
  245. return NO;
  246. }
  247. - (BOOL)dzn_isTouchAllowed
  248. {
  249. if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldAllowTouch:)]) {
  250. return [self.emptyDataSetDelegate emptyDataSetShouldAllowTouch:self];
  251. }
  252. return YES;
  253. }
  254. - (BOOL)dzn_isScrollAllowed
  255. {
  256. if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldAllowScroll:)]) {
  257. return [self.emptyDataSetDelegate emptyDataSetShouldAllowScroll:self];
  258. }
  259. return NO;
  260. }
  261. - (BOOL)dzn_isImageViewAnimateAllowed
  262. {
  263. if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldAnimateImageView:)]) {
  264. return [self.emptyDataSetDelegate emptyDataSetShouldAnimateImageView:self];
  265. }
  266. return NO;
  267. }
  268. - (void)dzn_willAppear
  269. {
  270. if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetWillAppear:)]) {
  271. [self.emptyDataSetDelegate emptyDataSetWillAppear:self];
  272. }
  273. }
  274. - (void)dzn_didAppear
  275. {
  276. if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetDidAppear:)]) {
  277. [self.emptyDataSetDelegate emptyDataSetDidAppear:self];
  278. }
  279. }
  280. - (void)dzn_willDisappear
  281. {
  282. if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetWillDisappear:)]) {
  283. [self.emptyDataSetDelegate emptyDataSetWillDisappear:self];
  284. }
  285. }
  286. - (void)dzn_didDisappear
  287. {
  288. if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetDidDisappear:)]) {
  289. [self.emptyDataSetDelegate emptyDataSetDidDisappear:self];
  290. }
  291. }
  292. - (void)dzn_didTapContentView:(id)sender
  293. {
  294. if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSet:didTapView:)]) {
  295. [self.emptyDataSetDelegate emptyDataSet:self didTapView:sender];
  296. }
  297. #pragma clang diagnostic push
  298. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  299. else if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetDidTapView:)]) {
  300. [self.emptyDataSetDelegate emptyDataSetDidTapView:self];
  301. }
  302. #pragma clang diagnostic pop
  303. }
  304. - (void)dzn_didTapDataButton:(id)sender
  305. {
  306. if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSet:didTapButton:)]) {
  307. [self.emptyDataSetDelegate emptyDataSet:self didTapButton:sender];
  308. }
  309. #pragma clang diagnostic push
  310. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  311. else if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetDidTapButton:)]) {
  312. [self.emptyDataSetDelegate emptyDataSetDidTapButton:self];
  313. }
  314. #pragma clang diagnostic pop
  315. }
  316. #pragma mark - Setters (Public)
  317. - (void)setEmptyDataSetSource:(id<DZNEmptyDataSetSource>)datasource
  318. {
  319. if (!datasource || ![self dzn_canDisplay]) {
  320. [self dzn_invalidate];
  321. }
  322. objc_setAssociatedObject(self, kEmptyDataSetSource, [[DZNWeakObjectContainer alloc] initWithWeakObject:datasource], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  323. // We add method sizzling for injecting -dzn_reloadData implementation to the native -reloadData implementation
  324. [self swizzleIfPossible:@selector(reloadData)];
  325. // Exclusively for UITableView, we also inject -dzn_reloadData to -endUpdates
  326. if ([self isKindOfClass:[UITableView class]]) {
  327. [self swizzleIfPossible:@selector(endUpdates)];
  328. }
  329. }
  330. - (void)setEmptyDataSetDelegate:(id<DZNEmptyDataSetDelegate>)delegate
  331. {
  332. if (!delegate) {
  333. [self dzn_invalidate];
  334. }
  335. objc_setAssociatedObject(self, kEmptyDataSetDelegate, [[DZNWeakObjectContainer alloc] initWithWeakObject:delegate], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  336. }
  337. #pragma mark - Setters (Private)
  338. - (void)setEmptyDataSetView:(DZNEmptyDataSetView *)view
  339. {
  340. objc_setAssociatedObject(self, kEmptyDataSetView, view, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  341. }
  342. #pragma mark - Reload APIs (Public)
  343. - (void)reloadEmptyDataSet
  344. {
  345. [self dzn_reloadEmptyDataSet];
  346. }
  347. #pragma mark - Reload APIs (Private)
  348. - (void)dzn_reloadEmptyDataSet
  349. {
  350. if (![self dzn_canDisplay]) {
  351. return;
  352. }
  353. if (([self dzn_shouldDisplay] && [self dzn_itemsCount] == 0) || [self dzn_shouldBeForcedToDisplay])
  354. {
  355. // Notifies that the empty dataset view will appear
  356. [self dzn_willAppear];
  357. DZNEmptyDataSetView *view = self.emptyDataSetView;
  358. // Configure empty dataset fade in display
  359. view.fadeInOnDisplay = [self dzn_shouldFadeIn];
  360. if (!view.superview) {
  361. // 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
  362. if (([self isKindOfClass:[UITableView class]] || [self isKindOfClass:[UICollectionView class]]) && self.subviews.count > 1) {
  363. [self insertSubview:view atIndex:0];
  364. }
  365. else {
  366. [self addSubview:view];
  367. }
  368. }
  369. // Removing view resetting the view and its constraints it very important to guarantee a good state
  370. [view prepareForReuse];
  371. UIView *customView = [self dzn_customView];
  372. // If a non-nil custom view is available, let's configure it instead
  373. if (customView) {
  374. view.customView = customView;
  375. }
  376. else {
  377. // Get the data from the data source
  378. NSAttributedString *titleLabelString = [self dzn_titleLabelString];
  379. NSAttributedString *detailLabelString = [self dzn_detailLabelString];
  380. UIImage *buttonImage = [self dzn_buttonImageForState:UIControlStateNormal];
  381. NSAttributedString *buttonTitle = [self dzn_buttonTitleForState:UIControlStateNormal];
  382. UIImage *image = [self dzn_image];
  383. UIColor *imageTintColor = [self dzn_imageTintColor];
  384. UIImageRenderingMode renderingMode = imageTintColor ? UIImageRenderingModeAlwaysTemplate : UIImageRenderingModeAlwaysOriginal;
  385. view.verticalSpace = [self dzn_verticalSpace];
  386. // Configure Image
  387. if (image) {
  388. if ([image respondsToSelector:@selector(imageWithRenderingMode:)]) {
  389. view.imageView.image = [image imageWithRenderingMode:renderingMode];
  390. view.imageView.tintColor = imageTintColor;
  391. }
  392. else {
  393. // iOS 6 fallback: insert code to convert imaged if needed
  394. view.imageView.image = image;
  395. }
  396. }
  397. // Configure title label
  398. if (titleLabelString) {
  399. view.titleLabel.attributedText = titleLabelString;
  400. }
  401. // Configure detail label
  402. if (detailLabelString) {
  403. view.detailLabel.attributedText = detailLabelString;
  404. }
  405. // Configure button
  406. if (buttonImage) {
  407. [view.button setImage:buttonImage forState:UIControlStateNormal];
  408. [view.button setImage:[self dzn_buttonImageForState:UIControlStateHighlighted] forState:UIControlStateHighlighted];
  409. }
  410. else if (buttonTitle) {
  411. [view.button setAttributedTitle:buttonTitle forState:UIControlStateNormal];
  412. [view.button setAttributedTitle:[self dzn_buttonTitleForState:UIControlStateHighlighted] forState:UIControlStateHighlighted];
  413. [view.button setBackgroundImage:[self dzn_buttonBackgroundImageForState:UIControlStateNormal] forState:UIControlStateNormal];
  414. [view.button setBackgroundImage:[self dzn_buttonBackgroundImageForState:UIControlStateHighlighted] forState:UIControlStateHighlighted];
  415. }
  416. }
  417. // Configure offset
  418. view.verticalOffset = [self dzn_verticalOffset];
  419. // Configure the empty dataset view
  420. view.backgroundColor = [self dzn_dataSetBackgroundColor];
  421. view.hidden = NO;
  422. view.clipsToBounds = YES;
  423. // Configure empty dataset userInteraction permission
  424. view.userInteractionEnabled = [self dzn_isTouchAllowed];
  425. [view setupConstraints];
  426. [UIView performWithoutAnimation:^{
  427. [view layoutIfNeeded];
  428. }];
  429. // Configure scroll permission
  430. self.scrollEnabled = [self dzn_isScrollAllowed];
  431. // Configure image view animation
  432. if ([self dzn_isImageViewAnimateAllowed])
  433. {
  434. CAAnimation *animation = [self dzn_imageAnimation];
  435. if (animation) {
  436. [self.emptyDataSetView.imageView.layer addAnimation:animation forKey:kEmptyImageViewAnimationKey];
  437. }
  438. }
  439. else if ([self.emptyDataSetView.imageView.layer animationForKey:kEmptyImageViewAnimationKey]) {
  440. [self.emptyDataSetView.imageView.layer removeAnimationForKey:kEmptyImageViewAnimationKey];
  441. }
  442. // Notifies that the empty dataset view did appear
  443. [self dzn_didAppear];
  444. }
  445. else if (self.isEmptyDataSetVisible) {
  446. [self dzn_invalidate];
  447. }
  448. }
  449. - (void)dzn_invalidate
  450. {
  451. // Notifies that the empty dataset view will disappear
  452. [self dzn_willDisappear];
  453. if (self.emptyDataSetView) {
  454. [self.emptyDataSetView prepareForReuse];
  455. [self.emptyDataSetView removeFromSuperview];
  456. [self setEmptyDataSetView:nil];
  457. }
  458. self.scrollEnabled = YES;
  459. // Notifies that the empty dataset view did disappear
  460. [self dzn_didDisappear];
  461. }
  462. #pragma mark - Method Swizzling
  463. static NSMutableDictionary *_impLookupTable;
  464. static NSString *const DZNSwizzleInfoPointerKey = @"pointer";
  465. static NSString *const DZNSwizzleInfoOwnerKey = @"owner";
  466. static NSString *const DZNSwizzleInfoSelectorKey = @"selector";
  467. // Based on Bryce Buchanan's swizzling technique http://blog.newrelic.com/2014/04/16/right-way-to-swizzle/
  468. // And Juzzin's ideas https://github.com/juzzin/JUSEmptyViewController
  469. void dzn_original_implementation(id self, SEL _cmd)
  470. {
  471. // Fetch original implementation from lookup table
  472. Class baseClass = dzn_baseClassToSwizzleForTarget(self);
  473. NSString *key = dzn_implementationKey(baseClass, _cmd);
  474. NSDictionary *swizzleInfo = [_impLookupTable objectForKey:key];
  475. NSValue *impValue = [swizzleInfo valueForKey:DZNSwizzleInfoPointerKey];
  476. IMP impPointer = [impValue pointerValue];
  477. // We then inject the additional implementation for reloading the empty dataset
  478. // Doing it before calling the original implementation does update the 'isEmptyDataSetVisible' flag on time.
  479. [self dzn_reloadEmptyDataSet];
  480. // If found, call original implementation
  481. if (impPointer) {
  482. ((void(*)(id,SEL))impPointer)(self,_cmd);
  483. }
  484. }
  485. NSString *dzn_implementationKey(Class class, SEL selector)
  486. {
  487. if (!class || !selector) {
  488. return nil;
  489. }
  490. NSString *className = NSStringFromClass([class class]);
  491. NSString *selectorName = NSStringFromSelector(selector);
  492. return [NSString stringWithFormat:@"%@_%@",className,selectorName];
  493. }
  494. Class dzn_baseClassToSwizzleForTarget(id target)
  495. {
  496. if ([target isKindOfClass:[UITableView class]]) {
  497. return [UITableView class];
  498. }
  499. else if ([target isKindOfClass:[UICollectionView class]]) {
  500. return [UICollectionView class];
  501. }
  502. else if ([target isKindOfClass:[UIScrollView class]]) {
  503. return [UIScrollView class];
  504. }
  505. return nil;
  506. }
  507. - (void)swizzleIfPossible:(SEL)selector
  508. {
  509. // Check if the target responds to selector
  510. if (![self respondsToSelector:selector]) {
  511. return;
  512. }
  513. // Create the lookup table
  514. if (!_impLookupTable) {
  515. _impLookupTable = [[NSMutableDictionary alloc] initWithCapacity:3]; // 3 represent the supported base classes
  516. }
  517. // We make sure that setImplementation is called once per class kind, UITableView or UICollectionView.
  518. for (NSDictionary *info in [_impLookupTable allValues]) {
  519. Class class = [info objectForKey:DZNSwizzleInfoOwnerKey];
  520. NSString *selectorName = [info objectForKey:DZNSwizzleInfoSelectorKey];
  521. if ([selectorName isEqualToString:NSStringFromSelector(selector)]) {
  522. if ([self isKindOfClass:class]) {
  523. return;
  524. }
  525. }
  526. }
  527. Class baseClass = dzn_baseClassToSwizzleForTarget(self);
  528. NSString *key = dzn_implementationKey(baseClass, selector);
  529. NSValue *impValue = [[_impLookupTable objectForKey:key] valueForKey:DZNSwizzleInfoPointerKey];
  530. // If the implementation for this class already exist, skip!!
  531. if (impValue || !key || !baseClass) {
  532. return;
  533. }
  534. // Swizzle by injecting additional implementation
  535. Method method = class_getInstanceMethod(baseClass, selector);
  536. IMP dzn_newImplementation = method_setImplementation(method, (IMP)dzn_original_implementation);
  537. // Store the new implementation in the lookup table
  538. NSDictionary *swizzledInfo = @{DZNSwizzleInfoOwnerKey: baseClass,
  539. DZNSwizzleInfoSelectorKey: NSStringFromSelector(selector),
  540. DZNSwizzleInfoPointerKey: [NSValue valueWithPointer:dzn_newImplementation]};
  541. [_impLookupTable setObject:swizzledInfo forKey:key];
  542. }
  543. #pragma mark - UIGestureRecognizerDelegate Methods
  544. - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
  545. {
  546. if ([gestureRecognizer.view isEqual:self.emptyDataSetView]) {
  547. return [self dzn_isTouchAllowed];
  548. }
  549. return [super gestureRecognizerShouldBegin:gestureRecognizer];
  550. }
  551. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
  552. {
  553. UIGestureRecognizer *tapGesture = self.emptyDataSetView.tapGesture;
  554. if ([gestureRecognizer isEqual:tapGesture] || [otherGestureRecognizer isEqual:tapGesture]) {
  555. return YES;
  556. }
  557. // defer to emptyDataSetDelegate's implementation if available
  558. if ( (self.emptyDataSetDelegate != (id)self) && [self.emptyDataSetDelegate respondsToSelector:@selector(gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:)]) {
  559. return [(id)self.emptyDataSetDelegate gestureRecognizer:gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:otherGestureRecognizer];
  560. }
  561. return NO;
  562. }
  563. @end
  564. #pragma mark - DZNEmptyDataSetView
  565. @interface DZNEmptyDataSetView ()
  566. @end
  567. @implementation DZNEmptyDataSetView
  568. @synthesize contentView = _contentView;
  569. @synthesize titleLabel = _titleLabel, detailLabel = _detailLabel, imageView = _imageView, button = _button;
  570. #pragma mark - Initialization Methods
  571. - (instancetype)init
  572. {
  573. self = [super init];
  574. if (self) {
  575. [self addSubview:self.contentView];
  576. }
  577. return self;
  578. }
  579. - (void)didMoveToSuperview
  580. {
  581. CGRect superviewBounds = self.superview.bounds;
  582. self.frame = CGRectMake(0.0, 0.0, CGRectGetWidth(superviewBounds), CGRectGetHeight(superviewBounds));
  583. void(^fadeInBlock)(void) = ^{_contentView.alpha = 1.0;};
  584. if (self.fadeInOnDisplay) {
  585. [UIView animateWithDuration:0.25
  586. animations:fadeInBlock
  587. completion:NULL];
  588. }
  589. else {
  590. fadeInBlock();
  591. }
  592. }
  593. #pragma mark - Getters
  594. - (UIView *)contentView
  595. {
  596. if (!_contentView)
  597. {
  598. _contentView = [UIView new];
  599. _contentView.translatesAutoresizingMaskIntoConstraints = NO;
  600. _contentView.backgroundColor = [UIColor clearColor];
  601. _contentView.userInteractionEnabled = YES;
  602. _contentView.alpha = 0;
  603. }
  604. return _contentView;
  605. }
  606. - (UIImageView *)imageView
  607. {
  608. if (!_imageView)
  609. {
  610. _imageView = [UIImageView new];
  611. _imageView.translatesAutoresizingMaskIntoConstraints = NO;
  612. _imageView.backgroundColor = [UIColor clearColor];
  613. _imageView.contentMode = UIViewContentModeScaleAspectFit;
  614. _imageView.userInteractionEnabled = NO;
  615. _imageView.accessibilityIdentifier = @"empty set background image";
  616. [_contentView addSubview:_imageView];
  617. }
  618. return _imageView;
  619. }
  620. - (UILabel *)titleLabel
  621. {
  622. if (!_titleLabel)
  623. {
  624. _titleLabel = [UILabel new];
  625. _titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
  626. _titleLabel.backgroundColor = [UIColor clearColor];
  627. _titleLabel.font = [UIFont systemFontOfSize:27.0];
  628. _titleLabel.textColor = [UIColor colorWithWhite:0.6 alpha:1.0];
  629. _titleLabel.textAlignment = NSTextAlignmentCenter;
  630. _titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
  631. _titleLabel.numberOfLines = 0;
  632. _titleLabel.accessibilityIdentifier = @"empty set title";
  633. [_contentView addSubview:_titleLabel];
  634. }
  635. return _titleLabel;
  636. }
  637. - (UILabel *)detailLabel
  638. {
  639. if (!_detailLabel)
  640. {
  641. _detailLabel = [UILabel new];
  642. _detailLabel.translatesAutoresizingMaskIntoConstraints = NO;
  643. _detailLabel.backgroundColor = [UIColor clearColor];
  644. _detailLabel.font = [UIFont systemFontOfSize:17.0];
  645. _detailLabel.textColor = [UIColor colorWithWhite:0.6 alpha:1.0];
  646. _detailLabel.textAlignment = NSTextAlignmentCenter;
  647. _detailLabel.lineBreakMode = NSLineBreakByWordWrapping;
  648. _detailLabel.numberOfLines = 0;
  649. _detailLabel.accessibilityIdentifier = @"empty set detail label";
  650. [_contentView addSubview:_detailLabel];
  651. }
  652. return _detailLabel;
  653. }
  654. - (UIButton *)button
  655. {
  656. if (!_button)
  657. {
  658. _button = [UIButton buttonWithType:UIButtonTypeCustom];
  659. _button.translatesAutoresizingMaskIntoConstraints = NO;
  660. _button.backgroundColor = [UIColor clearColor];
  661. _button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
  662. _button.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
  663. _button.accessibilityIdentifier = @"empty set button";
  664. [_button addTarget:self action:@selector(didTapButton:) forControlEvents:UIControlEventTouchUpInside];
  665. [_contentView addSubview:_button];
  666. }
  667. return _button;
  668. }
  669. - (BOOL)canShowImage
  670. {
  671. return (_imageView.image && _imageView.superview);
  672. }
  673. - (BOOL)canShowTitle
  674. {
  675. return (_titleLabel.attributedText.string.length > 0 && _titleLabel.superview);
  676. }
  677. - (BOOL)canShowDetail
  678. {
  679. return (_detailLabel.attributedText.string.length > 0 && _detailLabel.superview);
  680. }
  681. - (BOOL)canShowButton
  682. {
  683. if ([_button attributedTitleForState:UIControlStateNormal].string.length > 0 || [_button imageForState:UIControlStateNormal]) {
  684. return (_button.superview != nil);
  685. }
  686. return NO;
  687. }
  688. #pragma mark - Setters
  689. - (void)setCustomView:(UIView *)view
  690. {
  691. if (!view) {
  692. return;
  693. }
  694. if (_customView) {
  695. [_customView removeFromSuperview];
  696. _customView = nil;
  697. }
  698. _customView = view;
  699. _customView.translatesAutoresizingMaskIntoConstraints = NO;
  700. [self.contentView addSubview:_customView];
  701. }
  702. #pragma mark - Action Methods
  703. - (void)didTapButton:(id)sender
  704. {
  705. SEL selector = NSSelectorFromString(@"dzn_didTapDataButton:");
  706. if ([self.superview respondsToSelector:selector]) {
  707. [self.superview performSelector:selector withObject:sender afterDelay:0.0f];
  708. }
  709. }
  710. - (void)removeAllConstraints
  711. {
  712. [self removeConstraints:self.constraints];
  713. [_contentView removeConstraints:_contentView.constraints];
  714. }
  715. - (void)prepareForReuse
  716. {
  717. [self.contentView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
  718. _titleLabel = nil;
  719. _detailLabel = nil;
  720. _imageView = nil;
  721. _button = nil;
  722. _customView = nil;
  723. [self removeAllConstraints];
  724. }
  725. #pragma mark - Auto-Layout Configuration
  726. - (void)setupConstraints
  727. {
  728. // First, configure the content view constaints
  729. // The content view must alway be centered to its superview
  730. NSLayoutConstraint *centerXConstraint = [self equallyRelatedConstraintWithView:self.contentView attribute:NSLayoutAttributeCenterX];
  731. NSLayoutConstraint *centerYConstraint = [self equallyRelatedConstraintWithView:self.contentView attribute:NSLayoutAttributeCenterY];
  732. [self addConstraint:centerXConstraint];
  733. [self addConstraint:centerYConstraint];
  734. [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentView]|" options:0 metrics:nil views:@{@"contentView": self.contentView}]];
  735. // When a custom offset is available, we adjust the vertical constraints' constants
  736. if (self.verticalOffset != 0 && self.constraints.count > 0) {
  737. centerYConstraint.constant = self.verticalOffset;
  738. }
  739. // If applicable, set the custom view's constraints
  740. if (_customView) {
  741. [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[customView]|" options:0 metrics:nil views:@{@"customView":_customView}]];
  742. [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[customView]|" options:0 metrics:nil views:@{@"customView":_customView}]];
  743. }
  744. else {
  745. CGFloat width = CGRectGetWidth(self.frame) ? : CGRectGetWidth([UIScreen mainScreen].bounds);
  746. CGFloat padding = roundf(width/16.0);
  747. CGFloat verticalSpace = self.verticalSpace ? : 11.0; // Default is 11 pts
  748. NSMutableArray *subviewStrings = [NSMutableArray array];
  749. NSMutableDictionary *views = [NSMutableDictionary dictionary];
  750. NSDictionary *metrics = @{@"padding": @(padding)};
  751. // Assign the image view's horizontal constraints
  752. if (_imageView.superview) {
  753. [subviewStrings addObject:@"imageView"];
  754. views[[subviewStrings lastObject]] = _imageView;
  755. [self.contentView addConstraint:[self.contentView equallyRelatedConstraintWithView:_imageView attribute:NSLayoutAttributeCenterX]];
  756. }
  757. // Assign the title label's horizontal constraints
  758. if ([self canShowTitle]) {
  759. [subviewStrings addObject:@"titleLabel"];
  760. views[[subviewStrings lastObject]] = _titleLabel;
  761. [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(padding@750)-[titleLabel(>=0)]-(padding@750)-|"
  762. options:0 metrics:metrics views:views]];
  763. }
  764. // or removes from its superview
  765. else {
  766. [_titleLabel removeFromSuperview];
  767. _titleLabel = nil;
  768. }
  769. // Assign the detail label's horizontal constraints
  770. if ([self canShowDetail]) {
  771. [subviewStrings addObject:@"detailLabel"];
  772. views[[subviewStrings lastObject]] = _detailLabel;
  773. [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(padding@750)-[detailLabel(>=0)]-(padding@750)-|"
  774. options:0 metrics:metrics views:views]];
  775. }
  776. // or removes from its superview
  777. else {
  778. [_detailLabel removeFromSuperview];
  779. _detailLabel = nil;
  780. }
  781. // Assign the button's horizontal constraints
  782. if ([self canShowButton]) {
  783. [subviewStrings addObject:@"button"];
  784. views[[subviewStrings lastObject]] = _button;
  785. [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(padding@750)-[button(>=0)]-(padding@750)-|"
  786. options:0 metrics:metrics views:views]];
  787. }
  788. // or removes from its superview
  789. else {
  790. [_button removeFromSuperview];
  791. _button = nil;
  792. }
  793. NSMutableString *verticalFormat = [NSMutableString new];
  794. // Build a dynamic string format for the vertical constraints, adding a margin between each element. Default is 11 pts.
  795. for (int i = 0; i < subviewStrings.count; i++) {
  796. NSString *string = subviewStrings[i];
  797. [verticalFormat appendFormat:@"[%@]", string];
  798. if (i < subviewStrings.count-1) {
  799. [verticalFormat appendFormat:@"-(%.f@750)-", verticalSpace];
  800. }
  801. }
  802. // Assign the vertical constraints to the content view
  803. if (verticalFormat.length > 0) {
  804. [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:@"V:|%@|", verticalFormat]
  805. options:0 metrics:metrics views:views]];
  806. }
  807. }
  808. }
  809. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
  810. {
  811. UIView *hitView = [super hitTest:point withEvent:event];
  812. // Return any UIControl instance such as buttons, segmented controls, switches, etc.
  813. if ([hitView isKindOfClass:[UIControl class]]) {
  814. return hitView;
  815. }
  816. // Return either the contentView or customView
  817. if ([hitView isEqual:_contentView] || [hitView isEqual:_customView]) {
  818. return hitView;
  819. }
  820. return nil;
  821. }
  822. @end
  823. #pragma mark - UIView+DZNConstraintBasedLayoutExtensions
  824. @implementation UIView (DZNConstraintBasedLayoutExtensions)
  825. - (NSLayoutConstraint *)equallyRelatedConstraintWithView:(UIView *)view attribute:(NSLayoutAttribute)attribute
  826. {
  827. return [NSLayoutConstraint constraintWithItem:view
  828. attribute:attribute
  829. relatedBy:NSLayoutRelationEqual
  830. toItem:self
  831. attribute:attribute
  832. multiplier:1.0
  833. constant:0.0];
  834. }
  835. @end
  836. #pragma mark - DZNWeakObjectContainer
  837. @implementation DZNWeakObjectContainer
  838. - (instancetype)initWithWeakObject:(id)object
  839. {
  840. self = [super init];
  841. if (self) {
  842. _weakObject = object;
  843. }
  844. return self;
  845. }
  846. @end