有关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缺省页的实现的更多相关文章

  1. UITableView和UICollectionView的方法学习一

    参考资料 UITableView UICollectionView UICollectionViewDataSource UICollectionViewDelegate UICollectionVi ...

  2. UItableView嵌套UICollectionView

    首先我们需要继承一下UITableView并且遵守<UITableViewDelegate,UITableViewDataSource,UICollectionViewDataSource,UI ...

  3. UITableView和UICollectionView的Cell高度的几种设置方式

    UITableViewCell 1.UITableView的Cell高度默认由rowHeight属性指定一个低优先级的隐式约束 2.XIB中可向UITableViewCell的contentView添 ...

  4. iOS 8自动调整UITableView和UICollectionView布局

    本文转载自:http://tech.techweb.com.cn/thread-635784-1-1.html 本文讲述了UITableView.UICollectionView实现 self-siz ...

  5. iOS中UITableView和UICollectionView的默认空态页

    项目中想实现空态页风格统一控制的效果,就封装了一个默认空态页,使用的技术点有:1 方法替换 ,2 给分类(Category)添加属性. 我们知道,扩展(extension)可以给类添加私有变量和方法. ...

  6. UITableVIew与UICollectionView带动画删除cell时崩溃的处理

    UITableVIew与UICollectionView带动画删除cell时崩溃的处理 -会崩溃的原因是因为没有处理好数据源与cell之间的协调关系- 效果: tableView的源码: ModelC ...

  7. [转]iOS8 自动调整UITableView和UICollectionView布局

    转自:http://www.cocoachina.com/industry/20140825/9450.html (via:玉令天下的Blog)   本文讲述了UITableView.UICollec ...

  8. 封装Button ,封装UITableView,封装UICollectionView

    ---恢复内容开始--- 封装Button ,封装UITableView,封装UICollectionView: 1.实现Button的创建和点击事件不用分开操作处理; 2.实现UITableView ...

  9. iOS全埋点解决方案-UITableView和UICollectionView点击事件

    前言 在 $AppClick 事件采集中,还有两个比较特殊的控件: UITableView •UICollectionView 这两个控件的点击事件,一般指的是点击 UITableViewCell 和 ...

随机推荐

  1. Ajax工作原理

    在写这篇文章之前,曾经写过一篇关于AJAX技术的随笔,不过涉及到的方面很窄,对AJAX技术的背景.原理.优缺点等各个方面都很少涉及null.这次写这篇文章的背景是因为公司需要对内部程序员做一个培训.项 ...

  2. (一)SQL Server分区详解Partition(目录)

    一.SQL Server分区介绍 在SQL Server中,数据库的所有表和索引都视为已分区表和索引,默认这些表和索引值包含一个分区:也就是说表或索引至少包含一个分区.SQL Server中数据是按水 ...

  3. socket (一)

    socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求. socket起源于Un ...

  4. 初识java泛型

    1 协变数组类型(covariant array type) 数组的协变性: if A IS-A B then A[] IS-A B[] 也就是说,java中的数组兼容,一个类型的数组兼容他的子类类型 ...

  5. 使用maven命令将jar包导入本地仓库

    mvn install:install-file-DgroupId=包名-DartifactId=项目名-Dversion=版本号-Dpackaging=jar-Dfile=jar文件所在路径 1,本 ...

  6. 耿丹CS16-2班课堂测试作业汇总

    Deadline: 2016-11-01 11:59 作业内容 课堂测试作业总结 00.题目得5分,多半扣在格式上,有些同学代码写得很过分,已经很仁慈对待,同学们珍惜之: 01.界面设计得分不好,换行 ...

  7. CAD打印线条太粗、线条颜色设置

    不管你是使用打印机,还是将CAD转换为PDF文件,如果出现以下情况,线条太粗,根本看不清楚,怎么解决呢? 或者,不想通过图层复杂.繁琐的设置,想将各种颜色线条的CAD全部打印成黑白,或者指定某一种颜色 ...

  8. mysql基础类型知识总结

    Mysql知识回顾 http://www.educity.cn/wenda/596225.html http://blog.csdn.net/dyllove98/article/details/928 ...

  9. Android带图片的Toast(自定义Toast)

    使用Android默认的Toast Toast简介: Toast是一个简单的消息显示框,能够短暂的出现在屏幕的某个位置,显示提示消息. 默认的位置是屏幕的下方正中,一般Toast的使用如下: Toas ...

  10. MySQL 5.6 OOM 问题解决分享【转】

    本文来自:杨德华的原创分享 | MySQL 5.6 OOM 问题解决分享 延伸阅读:Linux的内存回收和交换 当遇到应用程序OOM的时候,大多数时候只能用头疼来形容,应用程序还可以通过引流来临时重启 ...