RumTime实践之--UITableView和UICollectionView缺省页的实现
有关RunTime的知识点已经看过很久了,但是一直苦于在项目中没有好的机会进行实际运用,俗话说“光说不练假把式”,正好最近在项目中碰到一个UITableView和UICollectionView在数据缺省的情况下展示默认缺省页的需求,这个时候RunTime大展拳脚的时候就到了。
大致的实现思路是这样的,因为UITableView和UICollectionView都是继承自系统的UIScrollView,所以为了同时实现UITableView和UICollectionView的缺省页,我们可以通过对UIScrollView进行扩展来达到这一目标。UITableView和UICollectionView都可以通过dataSource设置它们的数据源,而我们需要做的就是在UITableView和UICollectionView设置数据源之后去获取它们的数据源,从而得知当前UITableView和UICollectionView是否处于缺省状态。当UITableView和UICollectionView处于缺省状态时,我们就将加载缺省页视图。
- #import <UIKit/UIKit.h>
- @protocol EmptyDataViewDataSource;
- @interface UIScrollView (EmptyData)
- @property (nonatomic, weak) id <EmptyDataViewDataSource> emptyViewDataSource;
- @end
- @protocol EmptyDataViewDataSource <NSObject>
- - (UIView *)emptyDataCustomView;
- @end
首先实现一个UIScrollView的Category,在这个Category中声明一个EmptyDataViewDataSource的代理。当然,由于Category是在编译期间进行决议的,而OC的类对象在内存中是以结构体的形式排布的,结构体的大小是不能动态改变的,因此Category是不能动态地添加成员变量的,但是我们可以通过runtime中的
objc_setAssociatedObject和objc_getAssociatedObject方法去创建一个关联对象,从而在外部看起来好像我们为一个Category添加了一个成员变量。
我们自定义EmptyDataViewDataSource的set和get方法。创建对象ProtocolContainer,并将它关联到self上。之后执行SelectorShouldBeSwizzle方法,对系统方法进行替换。
- - (void)setEmptyViewDataSource:(id<EmptyDataViewDataSource>)emptyViewDataSource
- {
- ProtocolContainer *container = [[ProtocolContainer alloc] initWithProtocol:emptyViewDataSource];
- objc_setAssociatedObject(self, kEmptyDataDataSource, container, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- [self SelectorShouldBeSwizzle]; //在EmptyViewDataSource的set方法中实现系统方法的替换操作
- }
- - (id<EmptyDataViewDataSource>)emptyViewDataSource
- {
- ProtocolContainer *container = objc_getAssociatedObject(self, kEmptyDataDataSource);
- return container.protocolContainer;
- }
EmptyDataViewDataSource set&get方法
在SelectorShouldBeSwizzle方法中不论是UITableView还是UICollectionView我们都需要替换系统的reloadData方法。由于UITableView还有插入,删除等操作,在这些操作后系统会自动去调用UITableView中的endUpdates方法,因此我们还需要替换endUpdates方法。
- /**
- 需要被替换的系统方法
- */
- - (void)SelectorShouldBeSwizzle
- {
- [self methodSwizzle:@selector(reloadData)]; //替换系统的reloadData实现方法
- if ([self isKindOfClass:[UITableView class]]) //由于tableView有Cell的插入,删除实现,需要替换tableView的endUpdates方法,在操作结束后判断是否为空
- {
- [self methodSwizzle:@selector(endUpdates)];
- }
- }
方法替换的实现主要依赖于runtime中的class_getInstanceMethod和method_setImplementation方法。我们先来看看class_getInstanceMethod方法的描述
- /**
- * Returns a specified instance method for a given class.
- *
- * @param cls The class you want to inspect.
- * @param name The selector of the method you want to retrieve.
- *
- * @return The method that corresponds to the implementation of the selector specified by
- * \e name for the class specified by \e cls, or \c NULL if the specified class or its
- * superclasses do not contain an instance method with the specified selector.
- *
- * @note This function searches superclasses for implementations, whereas \c class_copyMethodList does not.
- */
- OBJC_EXPORT Method class_getInstanceMethod(Class cls, SEL name)
- OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
从中我们可以看到这个方法的返回值是一个Method,那么什么是Method呢?Method是一个指向objc_method类型的结构体的指针,这个结构体中有三个成员变量,SEL类型的method_name(我的理解为方法的名称),char*类型的method_types(方法的参数)以及IMP类型的method_imp(方法的实现)。SEL和IMP是一种映射关系,OC在运行时通过SEL方法名去找方法对应的IMP实现,这也是为什么OC不能像C++那样实现函数重载的原因,因为在OC中方法名和实现是一一对应的关系,重名了OC就没办法去找对应的实现方法了(貌似有点扯远了。。。)现在我们就可以很好的理解class_getInstanceMethod这个方法了,这个方法实现的就是去Class cls中去找一个叫做SEL叫做name的方法, 并把这个方法返回。
- /**
- * Sets the implementation of a method.
- *
- * @param m The method for which to set an implementation.
- * @param imp The implemention to set to this method.
- *
- * @return The previous implementation of the method.
- */
- OBJC_EXPORT IMP method_setImplementation(Method m, IMP imp)
- OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
现在再来看method_setImplementation方法就很好理解了。使用我们传入的IMP imp去替换Method中原来的IMP实现,并返回原来的IMP实现。在理解完了这两个方法以后我们再回到代码上来。
- /**
- 替换系统方法的实现
- @param selector 需要被替换的系统方法
- */
- - (void)methodSwizzle:(SEL)selector
- {
- NSAssert([self respondsToSelector:selector], @"self不能响应selector方法");
- if (!_cacheDictionary) //如果缓存字典为空,开辟空间
- {
- _cacheDictionary = [[NSMutableDictionary alloc] init];
- }
- NSString *selfClass = NSStringFromClass([self class]); //获取当前类的类型
- NSString *selectorName = NSStringFromSelector(selector); //获取方法名
- NSString *key = [NSString stringWithFormat:@"%@_%@",selfClass,selectorName]; //类型+方法名组成需要被替换的方法的key
- NSValue *value = [_cacheDictionary objectForKey:key]; //查询方法是否被替换
- if (value)
- {
- return; //方法被替换时,直接return
- }
- Method method = class_getInstanceMethod([self class], selector);
- IMP originalImplemention = method_setImplementation(method, (IMP)newImplemention); //获取替换前的系统方法实现
- [_cacheDictionary setObject:[NSValue valueWithPointer:originalImplemention] forKey:key]; //缓存替换前的系统方法实现
- }
methodSwizzle
我们为乐不每一次都去执行方法的替换操作,我们使用一个缓存字典来存储已经被替换过的方法。我们使用类的类型和被替换掉实现的方法名组合起来作为缓存字典中存储的key值,value当然就是被替换下来的系统方法的实现。接下来我们看看我们在替换上去的newImplemention实现中做了什么。
- /**
- 被替换后的方法
- @param self self
- @param _cmd 方法名称
- */
- void newImplemention(id self, SEL _cmd)
- {
- NSString *selfClass = NSStringFromClass([self class]);
- NSString *selectorName = NSStringFromSelector(_cmd);
- NSString *key = [NSString stringWithFormat:@"%@_%@",selfClass,selectorName]; //使用当前的类名和当前方法的名称组合称为key值
- NSValue *value = [_cacheDictionary objectForKey:key]; //通过key从缓存字典中取出系统原来方法实现
- IMP originalImplemention = [value pointerValue];
- if (originalImplemention)
- {
- ((void(*)(id,SEL))originalImplemention)(self,_cmd); //执行被替换前系统原来的方法
- }
- if([self canEmptyDataViewShow]) //判断是否需要展示缺省页
- {
- [self reloadEmptyView];
- }
- }
newImplemention(id self, SEL _cmd)
在OC中,每个方法被调用时,系统都会自动的为方法添加两个参数,一个self,另一个就是被调用的方法的名称,也就是我们的newImplemention中的两个入参。我们取出来系统原来的实现方法,这一步是必须的,毕竟我们的本意并不是抹除掉系统方法,而是在调用系统方法执行后判断是否需要展示缺省页而已啦。。。因此通过调用IMP的方式,直接调用系统方法实现。之后判断是否需要展示缺省页。
- - (BOOL)canEmptyDataViewShow
- {
- NSInteger itemCount = ;
- NSAssert([self respondsToSelector:@selector(dataSource)], @"tableView或CollectionView没有实现dataSource");
- if ([self isKindOfClass:[UITableView class]])
- {
- UITableView *tableView = (UITableView *)self;
- id<UITableViewDataSource> dataSource = tableView.dataSource;
- NSInteger sections = ;
- if (dataSource && [dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)])
- {
- sections = [dataSource numberOfSectionsInTableView:tableView];
- }
- if(dataSource && [dataSource respondsToSelector:@selector(tableView:numberOfRowsInSection:)])
- {
- for (NSInteger i=; i<sections; i++) {
- itemCount += [dataSource tableView:tableView numberOfRowsInSection:i];
- }
- }
- }
- if ([self isKindOfClass:[UICollectionView class]])
- {
- UICollectionView *collectionView = (UICollectionView *)self;
- id<UICollectionViewDataSource> dataSource = collectionView.dataSource;
- NSInteger sections = ;
- if (!dataSource || [dataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)])
- {
- sections = [dataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)];
- }
- if (!dataSource || [dataSource respondsToSelector:@selector(collectionView:numberOfItemsInSection:)])
- {
- for (NSInteger i=; i<sections; i++) {
- itemCount += [dataSource collectionView:collectionView numberOfItemsInSection:i];
- }
- }
- }
- if (itemCount == )
- {
- return YES;
- }
- else
- {
- return NO;
- }
- }
- (BOOL)canEmptyDataViewShow
我们获取到对象本身,并从对象中取出它的数据源,然后判断数据源的item是否为0 ,当为0时,就可以自动展示缺省页了。
- - (void)reloadEmptyView
- {
- UIView *emptyView;
- if (self.emptyViewDataSource && [self.emptyViewDataSource respondsToSelector:@selector(emptyDataCustomView)])
- {
- emptyView = [self.emptyViewDataSource emptyDataCustomView];
- }
- else
- {
- emptyView = self.emptyDataView;
- emptyView.backgroundColor = [UIColor redColor];
- }
- if (!emptyView.superview)
- {
- if (self.subviews.count > )
- {
- [self insertSubview:emptyView atIndex:];
- }
- }
- emptyView.frame = self.frame;
- }
- (void)reloadEmptyView
还记得我们在之前设置的EmptyDataViewDataSource吗?既然已经设置了EmptyDataViewDataSource,那我们就要把它充分利用起来嘛,来个自定义的emptyView也不是什么问题嘛~
源码github地址:https://github.com/cgy-tiaopi/CGYEmptyData
RumTime实践之--UITableView和UICollectionView缺省页的实现的更多相关文章
- UITableView和UICollectionView的方法学习一
参考资料 UITableView UICollectionView UICollectionViewDataSource UICollectionViewDelegate UICollectionVi ...
- UItableView嵌套UICollectionView
首先我们需要继承一下UITableView并且遵守<UITableViewDelegate,UITableViewDataSource,UICollectionViewDataSource,UI ...
- UITableView和UICollectionView的Cell高度的几种设置方式
UITableViewCell 1.UITableView的Cell高度默认由rowHeight属性指定一个低优先级的隐式约束 2.XIB中可向UITableViewCell的contentView添 ...
- iOS 8自动调整UITableView和UICollectionView布局
本文转载自:http://tech.techweb.com.cn/thread-635784-1-1.html 本文讲述了UITableView.UICollectionView实现 self-siz ...
- iOS中UITableView和UICollectionView的默认空态页
项目中想实现空态页风格统一控制的效果,就封装了一个默认空态页,使用的技术点有:1 方法替换 ,2 给分类(Category)添加属性. 我们知道,扩展(extension)可以给类添加私有变量和方法. ...
- UITableVIew与UICollectionView带动画删除cell时崩溃的处理
UITableVIew与UICollectionView带动画删除cell时崩溃的处理 -会崩溃的原因是因为没有处理好数据源与cell之间的协调关系- 效果: tableView的源码: ModelC ...
- [转]iOS8 自动调整UITableView和UICollectionView布局
转自:http://www.cocoachina.com/industry/20140825/9450.html (via:玉令天下的Blog) 本文讲述了UITableView.UICollec ...
- 封装Button ,封装UITableView,封装UICollectionView
---恢复内容开始--- 封装Button ,封装UITableView,封装UICollectionView: 1.实现Button的创建和点击事件不用分开操作处理; 2.实现UITableView ...
- iOS全埋点解决方案-UITableView和UICollectionView点击事件
前言 在 $AppClick 事件采集中,还有两个比较特殊的控件: UITableView •UICollectionView 这两个控件的点击事件,一般指的是点击 UITableViewCell 和 ...
随机推荐
- Gulp基础
1.什么是gulp? gulp是前端开发过程中对代码进行构建的工具,是自动化项目的构建利器. 2.为什么使用gulp? gulp不仅能对网站资源进行优化,而且在开发过程中很多重复的任务能够使用正确的工 ...
- .htaccess基本语法和应用 (2012-11-09 16:13:47)转载▼
htaccess基本语法和应用 (2012-11-09 16:13:47) 转载▼ 标签: htaccess it 分类: 网络 .htaccess是Apache服务器的一个非常强大的分布式配置文件 ...
- 4. web前端开发分享-css,js工具篇
web前端开发乃及其它的相关开发,推荐sublime text, webstorm(jetbrains公司系列产品)这两个的原因在于,有个技术叫emmet, http://docs.emmet.io, ...
- c#生成二维码
String link ="www.baidu.com";//这里一般是一个链接 封装后的方法,直接调用就可以了 public void CreateQRCode ...
- 使用ICSharpCode.SharpZipLib.Zip类库解压zip文件的方法
public static bool ZipExtractFile(string zipFilePath,string targetPath) { FastZip fastZip = new Fast ...
- VSTO开发之一
VSTO是微软推出一种对Office产品进行操作的技术,其中提供了一些类库来让开发人员可以更方便地开发出Office的解决方案,即对Word/Excel/Outlook实现一些扩展功能. 新建-> ...
- linux vi(vim)常用命令汇总
1 查找 /xxx(?xxx) 表示在整篇文档中搜索匹配xxx的字符串, / 表示向下查找, ? 表示向上查找其中xxx可以是正规表达式,关于正规式就不多说了. 一般来说是区分大小写的, 要想不区分大 ...
- linux 下安装mongodb
1.初始化docker: -v 设置docker和host共享目录,格式hostPath:dockerContainerPath -p 端口映射 --name,容器名称 cen ...
- BMP图像差分/比较
#include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char ...
- day21
1.文件上传 a. Html Form表单提交 b. Ajax提交 原生XMLHttpRequest XmlHttpReqeust() 类 ...