在IOS开发中,UITableView是使用较多的一个控件。如果UITableView在数据源没有数据时会是一片空白,用户体验较差。所以在数据源为空的时候,我们应该给用户一些相应的提示。而DZNEmptyDataSet就是用来解决这个问题的,简单快捷的帮我们解决了空数据时的界面处理。
使用CocoaPods可以导入,pod 'DZNEmptyDataSet'
Github地址
简单使用
- (UITableView *)tableView {
if (!_tableView) {
_tableView = [[UITableView alloc]initWithFrame:self.view.bounds style:UITableViewStylePlain];
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.emptyDataSetSource = self;
_tableView.emptyDataSetDelegate = self;
_tableView.tableFooterView = [[UIView alloc]initWithFrame:CGRectMake(0,0,0,CGFLOAT_MIN)];
}
return _tableView;
}
- (UIImage *)buttonImageForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state {
return [UIImage imageNamed:@"image"];
}
- (void)emptyDataSet:(UIScrollView *)scrollView didTapButton:(UIButton *)button {
NSLog(@"%s",__func__);
}
只需设置代理并且实现对应的方法就可以完成string,image等效果,并且可以添加点击事件。
DZNEmptyDataSet文件
DZNEmptyDataSet很简单,只有一个文件。两个代理,分别是DZNEmptyDataSetDelegate和DZNEmptyDataSetSource。
- DZNEmptyDataSetDelegate
//是否淡入显示,默认YES
- (BOOL)emptyDataSetShouldFadeIn:(UIScrollView *)scrollView;
//当数据源为空时仍然展示,默认为NO不显示
- (BOOL)emptyDataSetShouldBeForcedToDisplay:(UIScrollView *)scrollView;
//是否能显示。默认YES显示
- (BOOL)emptyDataSetShouldDisplay:(UIScrollView *)scrollView;
//是否支持点击,默认YES可点击
- (BOOL)emptyDataSetShouldAllowTouch:(UIScrollView *)scrollView;
//是否允许滚动,默认NO不可滚动
- (BOOL)emptyDataSetShouldAllowScroll:(UIScrollView *)scrollView;
//是否播放imageView(图片数组播放),默认NO不播放
- (BOOL)emptyDataSetShouldAnimateImageView:(UIScrollView *)scrollView;
//view的点击事件
- (void)emptyDataSet:(UIScrollView *)scrollView didTapView:(UIView *)view;
//button的点击事件
- (void)emptyDataSet:(UIScrollView *)scrollView didTapButton:(UIButton *)button;
//emptyDataSet将要出现
- (void)emptyDataSetWillAppear:(UIScrollView *)scrollView;
//emptyDataSet已经出现
- (void)emptyDataSetDidAppear:(UIScrollView *)scrollView;
//emptyDataSet将要消失
- (void)emptyDataSetWillDisappear:(UIScrollView *)scrollView;
//emptyDataSet已经消失
- (void)emptyDataSetDidDisappear:(UIScrollView *)scrollView;
- DZNEmptyDataSetSource
//标题
- (NSAttributedString *)titleForEmptyDataSet:(UIScrollView *)scrollView;
//描述
- (NSAttributedString *)descriptionForEmptyDataSet:(UIScrollView *)scrollView;
//图片
- (UIImage *)imageForEmptyDataSet:(UIScrollView *)scrollView;
//设置图片渲染色
- (UIColor *)imageTintColorForEmptyDataSet:(UIScrollView *)scrollView;
//图片动画
- (CAAnimation *) imageAnimationForEmptyDataSet:(UIScrollView *) scrollView;
//设置button不同state下的标题
- (NSAttributedString *)buttonTitleForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state;
//设置button不同state下的图片
- (UIImage *)buttonImageForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state;
//设置button背景图片,没有默认值
- (UIImage *)buttonBackgroundImageForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state;
//设置背景颜色
- (UIColor *)backgroundColorForEmptyDataSet:(UIScrollView *)scrollView;
//自定义想要显示的view(而不是自带的label,imageview,button)
- (UIView *)customViewForEmptyDataSet:(UIScrollView *)scrollView;
//垂直偏移量
- (CGFloat)verticalOffsetForEmptyDataSet:(UIScrollView *)scrollView;
//两个对象的垂直偏移量,默认11
- (CGFloat)spaceHeightForEmptyDataSet:(UIScrollView *)scrollView;
源码分析
在setEmptyDataSetSource
和setEmptyDataSetDelegate
中,如果代理为空或者不允许显示时调用dzn_invalidate
释放self. emptyDataSetView
。
否则当代理存在并且允许显示时,动态添加属性emptyDataSetSource
或者emptyDataSetDelegate
。并且将dzn_reloadData
注入到reloadData
方法中,涂过是UITableview,同时注入到endUpdates
中。
- (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);
// 将dzn_reloadData注入到系统的reloadData中
[self swizzleIfPossible:@selector(reloadData)];
// 如果是UITableView,则额外将dzn_reloadData注入到系统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
先发送视图将要消失的通知,如果self.emptyDataSetView
存在则释放其子视图及约束,从父视图中移除,最后将其置空。恢复ScrollView的可滚动性,最后发送视图已消失的通知。
- (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;
[self dzn_didDisappear];
}
- (void)prepareForReuse {
[self.contentView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
_titleLabel = nil;
_detailLabel = nil;
_imageView = nil;
_button = nil;
_customView = nil;
[self removeAllConstraints];
}
swizzleIfPossible
用于给系统方法添加一些内容。
- (void)swizzleIfPossible:(SEL)selector {
// 检查传入的方法是否实现,未实现则return
if (![self respondsToSelector:selector]) {
return;
}
//创建表保存已注入的方法(给传入的方法注入dzn_reloadEmptyDataSet,再调用该方法。)
//即调用reloadData前或tableview调用endUpdate前先调用dzn_reloadEmptyDataSet。
if (!_impLookupTable) {
// 3 种支持的类型,UIS从rollView,UITableView,UIColloectionView
_impLookupTable = [[NSMutableDictionary alloc] initWithCapacity:3];
}
// 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;
}
}
}
//获取self的基类(scroll,table,collection)
Class baseClass = dzn_baseClassToSwizzleForTarget(self);
//获取方法名(dzn_implementationKey返回 className_SELName 的拼写)
NSString *key = dzn_implementationKey(baseClass, selector);
//获取以方法名对应的方法体
NSValue *impValue = [[_impLookupTable objectForKey:key] valueForKey:DZNSwizzleInfoPointerKey];
// 如果方法体存在或者key不存在或者基类不是规定的那三种则跳过
if (impValue || !key || !baseClass) {
return;
}
//在传入方法中注入dzn_reloadEmptyDataSet,然后调用传入的方法。
Method method = class_getInstanceMethod(baseClass, selector);
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];
}
reloadEmptyDataSet
调用了私有方法dzn_reloadEmptyDataSet
。
- (void)reloadEmptyDataSet {
[self dzn_reloadEmptyDataSet];
}
- (void)dzn_reloadEmptyDataSet {
if (![self dzn_canDisplay]) return;//如果数据源存在并且实现了代理则显示,否则返回
//如果可以允许显示并且dataSouce的item count为0可以显示,如果无论是否有数据都一直显示的代理被返回YES也能显示。
if (([self dzn_shouldDisplay] && [self dzn_itemsCount] == 0) || [self dzn_shouldBeForcedToDisplay]) {
[self dzn_willAppear];
//获取当前的emptyDataSetView
DZNEmptyDataSetView *view = self.emptyDataSetView;
//如果它没有被加入则加入并且要放在最前方。
if (!view.superview) {
if (([self isKindOfClass:[UITableView class]] || [self isKindOfClass:[UICollectionView class]]) && self.subviews.count > 1) {
[self insertSubview:view atIndex:0];
}
else {
[self addSubview:view];
}
}
//此时移除子视图(为了等会重新添加子视图刷新内容做准备)
[view prepareForReuse];
//如果实现了自定义的视图则赋值给view.customView,否则重新给view.customView添加默认控件。
UIView *customView = [self dzn_customView];
if (customView) {
view.customView = customView;
}
else {
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];
if (image) {
if ([image respondsToSelector:@selector(imageWithRenderingMode:)]) {
view.imageView.image = [image imageWithRenderingMode:renderingMode];
view.imageView.tintColor = imageTintColor;
}
else {
view.imageView.image = image;
}
}
if (titleLabelString) {
view.titleLabel.attributedText = titleLabelString;
}
if (detailLabelString) {
view.detailLabel.attributedText = detailLabelString;
}
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];
}
}
view.verticalOffset = [self dzn_verticalOffset];
view.backgroundColor = [self dzn_dataSetBackgroundColor];
view.hidden = NO;
view.clipsToBounds = YES;
view.userInteractionEnabled = [self dzn_isTouchAllowed];
view.fadeInOnDisplay = [self dzn_shouldFadeIn];
[view setupConstraints];
[UIView performWithoutAnimation:^{
[view layoutIfNeeded];
}];
self.scrollEnabled = [self dzn_isScrollAllowed];
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];
}
[self dzn_didAppear];
}
else if (self.isEmptyDataSetVisible) {
[self dzn_invalidate];
}
}
网友评论