DZNEmptyDataSet 源码学习笔记

DZNEmptyDataSet是一个用于UITableView&UICollectionView空数据时,设置显示空数据提示库。我们可以通过这个库提供的代理方法,进行简单的配置,实现数据为空时的视图显示。

emptyDataSetSource & emptyDataSetDelegate

我们在使用这个库时,首先需要做的就是设置个两个代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@implementation UIScrollView (DZNEmptyDataSet)
- (id<DZNEmptyDataSetSource>)emptyDataSetSource
{
DZNWeakObjectContainer *container = objc_getAssociatedObject(self, kEmptyDataSetSource);
return container.weakObject;
}
- (id<DZNEmptyDataSetDelegate>)emptyDataSetDelegate
{
DZNWeakObjectContainer *container = objc_getAssociatedObject(self, kEmptyDataSetDelegate);
return container.weakObject;
}
@end

这个通过rumtime的Associated Objects添加了两个代理对象。注意,这里是对UIScrollView使用的分类,是为了可以同时支持UITableView&UICollectionView。

setEmptyDataSetSource & setEmptyDataSetDelegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (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);
}

dzn_invalidate 相当于是这个库的初始化方法,各种控件的初始化都在这里进行处理。swizzleIfPossible这个函数封装了runtime的Swizzling,替换了原来UITableView&UICollectionView的reloadData和UITableView的endUpates,这一点很重要。

swizzleIfPossible

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)swizzleIfPossible:(SEL)selector
{
....
// 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];
}

swizzleIfPossible关键部分在这里。这里他将方法都替换成了dzn_original_implementation。嗯,我们继续往下看。

dzn_original_implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}
}

这里先执行了dzn_reloadEmptyDataSet,然后取回原函数的函数指针,去执行原函数。相当于在reloadData&endUpates执行先增加了执行dzn_reloadEmptyDataSet的代码。继续看dzn_reloadEmptyDataSet做了些什么。

dzn_reloadEmptyDataSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)dzn_reloadEmptyDataSet
{
if (![self dzn_canDisplay]) {
return;
}
if (([self dzn_shouldDisplay] && [self dzn_itemsCount] == 0) || [self dzn_shouldBeForcedToDisplay])
{
DZNEmptyDataSetView *view = self.emptyDataSetView;
...
下面的代码主要是根据通过代理方法的配置进行视图的布局
}
else if (self.isEmptyDataSetVisible) {
[self dzn_invalidate];
}
}

这个函数就是这个库的核心函数了。这里我们发现dzn_itemsCount不等于,并且用户配置为可以显示则进行视图的布局。DZNEmptyDataSetView是用于空数据是显示的视图。dzn_itemsCount的值是UITableView&UICollectionView的cell的总数。这是这么获取到的呢?继续往下看。。。

dzn_itemsCount

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- (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
...
UICollectionView 的处理大致一致。
return items;
}

原来这里是通过获取UITableView&UICollectionView的dataSource,然后去执行numberOfSectionsInTableView获取section个数,然后通过numberOfRowsInSection获取cell的个数。

总结

这个库实现思路很简单。

  1. 通过runtime的Associated Objects添加了两个代理属性。
  2. 在设置代理属性时,通过runtime的Swizzling实现函数替换。
  3. 然后通过执行UITabView&UICollectionView的代理方法计算出来cell的个数。
  4. 通过cell个数监控到数据为空,则添加空数据视图的显示。

setupConstraints是这个库的布局处理方法,这里的自动布局使用的了Visual Format Language,对应我个人而言也是个学习的点。

$$ t’ = \dfrac{t} {\sqrt{1- \dfrac{v^2} {c^2}}} $$

最后,本人将这个通过使用Swift 3.0,重写了一遍,代码已经放到GitHub了。求星,欢迎大家点评。