有关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. hibernate学习二(HelloWorld)

    一.建立hibernate配置文件 在工程Hibernate_01_HelloWorld下的src上建立hibernate.cfg.xml,打开hibernate-release-4.3.11.Fin ...

  2. 2.0、Hibernate框架的简单搭建

    一.Hibernate:是一个开放源代码的对象关系映射框架,对JDBC进行了非常轻量级的对象封装,它将POJO与数据库表建立映射关系,是一个全自动的orm框架,hibernate可以自动生成SQL语句 ...

  3. D3.js学习(四)

    上一节我们已经学习了线条样式和格栅的绘制,在这一节中我们将要根据之前绘制的线条对图表进行填充,首先来看一下我们的目标吧 在这个图表中,我们对位于线条下面的空间进行了填充,那么,如何改做到呢? 设置填充 ...

  4. 使用ICSharpCode.SharpZipLib.Zip类库解压zip文件的方法

    public static bool ZipExtractFile(string zipFilePath,string targetPath) { FastZip fastZip = new Fast ...

  5. RxJava简单的介绍

    1. RxJava简介 Rx(ReactiveX,响应式编程)是一种事件驱动的基于异步数据流的编程模式,整个数据流就像一条河流,它可以被观测(监听),过滤,操控或者与其他数据流合并为一条新的数据流.而 ...

  6. 在程序中执行shell命令

    在linux系统下的操作中我们会经常用到shell命令来进行,一开始学习进程的时候对于shell命令也进行了思考,认为shell命令就是一个进程的外壳,经过了后来的学习对于这一点也有了更多的认识. 用 ...

  7. C语言 活动安排问题

    有若干个活动,第i个开始时间和结束时间是[Si,fi),只有一个教室,活动之间不能交叠,求最多安排多少个活动? #include <stdio.h> #include <stdlib ...

  8. erlang 故障排查工具

    系统级别perf top, dstat -tam, vtune 都能很好分析beam 瓶颈,本文主要erlang 级别排查: 1. 反编译 确认线上运行代码是否正确,reltools没掌握好,升级偶尔 ...

  9. 总结初用erlang 时的遇到一些问题

    算起来接触erlang 三个多月快四个月来,期间从零开始看书写erlang代码.修改RabbitMQ.业务开发.系统调优,总算是有点入门了. 最难受的是边学边修改RabbitMQ,开始真心的看不懂,不 ...

  10. python 使用pymssql连接sql server数据库

    python 使用pymssql连接sql server数据库   #coding=utf-8 #!/usr/bin/env python#------------------------------ ...