DZNEmptyDataSet-原理

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方法移除空白页。