DZNEmptyDataSet用于给UITableView、 UICollectionView、甚至UIScrollView页面空白时,展示占位信息,优化用户体验。
Github地址
https://github.com/dzenbot/DZNEmptyDataSet
实现原理
通过设置DZNEmptyDataSetDelegate,DZNEmptyDataSetSource,并且实现相关协议,即可完成功能。
所以设置协议代理就成了关键,我们来看看
DZNEmptyDataSetDelegate
- (void)setEmptyDataSetDelegate:(id<DZNEmptyDataSetDelegate>)delegate
{
// 如果delegate为空,相当于取消代理
if (!delegate) {
// 取消代理
[self dzn_invalidate];
}
// 运行时关联代理对象
objc_setAssociatedObject(self, kEmptyDataSetDelegate, [[DZNWeakObjectContainer alloc] initWithWeakObject:delegate], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
取消代理
- (void)dzn_invalidate
{
// Notifies that the empty dataset view will disappear
[self dzn_willDisappear];
if (self.emptyDataSetView) {
// 移除代理视图的所有子view及约束,并把emptyDataSetView的控件置空
[self.emptyDataSetView prepareForReuse];
// 移除emptyDataSetView并置空
[self.emptyDataSetView removeFromSuperview];
[self setEmptyDataSetView:nil];
}
// 数据都没了,当然设置不能滚动
self.scrollEnabled = YES;
// Notifies that the empty dataset view did disappear
[self dzn_didDisappear];
}
在prepareForReuse
有个方法值得介绍,makeObjectsPerformSelector
,数组所有元素都调用selector,相当于循环调用。
DZNWeakObjectContainer持有weak的delegate对象。
DZNEmptyDataSetSource
- (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);
// 重点:将原生的-reloadData方法实现与-dzn_reloadData方法进行交换
[self swizzleIfPossible:@selector(reloadData)];
// 专门为UITableView注入-endUpdates方法
if ([self isKindOfClass:[UITableView class]]) {
[self swizzleIfPossible:@selector(endUpdates)];
}
}
方法交换
- (void)swizzleIfPossible:(SEL)selector
{
// 检查对象是否响应该方法
if (![self respondsToSelector:selector]) {
return;
}
// 创建查找表,记录方法及执行对象
if (!_impLookupTable) {
_impLookupTable = [[NSMutableDictionary alloc] initWithCapacity:3]; // 3 代表所支持的基类
}
// 确保每一个类(UITableView,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;
}
}
}
// 判断self是UITableView、UICollectionView、UIScrollView的哪个
Class baseClass = dzn_baseClassToSwizzleForTarget(self);
// key = className + selectorName拼接的字符串
NSString *key = dzn_implementationKey(baseClass, selector);
// 通过key在查找表中,找到实现的对象数据
NSValue *impValue = [[_impLookupTable objectForKey:key] valueForKey:DZNSwizzleInfoPointerKey];
// 如果这个类的实现已经存在,直接跳过
if (impValue || !key || !baseClass) {
return;
}
// 注入额外的实现
// 获取到对象方法
Method method = class_getInstanceMethod(baseClass, selector);
// 给方法设置新的实现,拿到IMP指针
IMP dzn_newImplementation = method_setImplementation(method, (IMP)dzn_original_implementation);
// 在查找表中保存新数据
NSDictionary *swizzledInfo = @{DZNSwizzleInfoOwnerKey: baseClass,
DZNSwizzleInfoSelectorKey: NSStringFromSelector(selector),
DZNSwizzleInfoPointerKey: [NSValue valueWithPointer:dzn_newImplementation]};
[_impLookupTable setObject:swizzledInfo forKey:key];
}
获取对象方法原始的实现
void dzn_original_implementation(id self, SEL _cmd)
{
// 从查找表中找到原始方法的实现
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];
// 为刷新空dataset添加额外的实现
// 在调用原始实现之前,更新'isEmptyDataSetVisible'标识
[self dzn_reloadEmptyDataSet];
// 如果从查找表中找到,立马调用原始实现
if (impPointer) {
((void(*)(id,SEL))impPointer)(self,_cmd);
}
}
DZNEmptyDataSet刷新空视图的方法,调用代理方实现的协议内容。
- (void)dzn_reloadEmptyDataSet
{
// .......
}
总结
框架对UIScrollView、UITableView、UICollectionView的reloadData
及UITableView的endUpdates
方法进行了方法替换,在系统方法调用前,注入dzn_reloadEmptyDataSet
方法,对是否显示空白页进行处理。
举例说明:
如[self.tableView reloadData]
调用刷新方法,实际会先调用框架实现的dzn_reloadEmptyDataSet
方法,再调用系统的reloadData
方法。
在dzn_reloadEmptyDataSet
方法中,通过条件判断是否需要显示空白页。
- 条件1:
dzn_shouldDisplay
并且dzn_itemsCount == 0
,代理方法获取用户的设置,通过UITableViewDataSource
获取数据源数量,两者一起判断是否显示空白页。 - 条件2:
dzn_shouldBeForcedToDisplay
通过代理方法,获取是否设置强制显示空白页。
如果显示空白页,则通过DZNEmptyDataSetSource
获取用户设置的空白页样式。如果不显示空白页,则通过dzn_invalidate
方法移除空白页。