本文摘自:数据显示和事件处理与controller解耦
在日常开发时,经常需要使一个列表通过多种不同样式的cell来展示,比如下图中的情况:
问题描述
多种cell就会造成cellForRowAtIndexPath/cellForItemAtIndexPath
大量的if /else,再加上数据和事件的处理简直是灾难,不利于以后的扩展,难以维护。
不同cell的数据显示
通过protocol依赖的方式,无需将子view属性暴露出来。
1. 定义一个协议
// BFDisplayEventProtocol.h
/**
显示数据协议
*/
@protocol BFDisplayProtocol
- (void)em_displayWithModel:(BFEventModel *)model;
@end
2. 创建数据传递模型
// BFEventModel.h
#import
#import
@interface BFEventModel : NSObject
@property (nonatomic,strong) id model; //!< 数据模型
@property (nonatomic,strong) NSIndexPath *indexPath; //!< 序号
@property (nonatomic,assign) NSInteger eventType; //!< 事件类型
@property (nonatomic,assign) NSInteger index; //!< 事件int标识
@property (nonatomic,assign) NSString *identifier; //!< 事件标识
@property (nonatomic,strong) id target; //!< target
@end
// BFEventModel.m
#import "BFEventModel.h"
@implementation BFEventModel
@end
3. cell中实现BFDisplayProtocol
协议
// BFCell1TableViewCell.m
@implementation BFCell1TableViewCell
#pragma mark - BFDisplayProtocol
- (void)em_displayWithModel:(BFEventModel *) theModel {
BFModel *model = theModel.model;
self.textLabel.text = model.title;
}
@end
4. 在代理中调用显示数据方法
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:!indexPath.section ? @"BFCell1TableViewCell" : @"BFCell2TableViewCell"];
id object = ((NSMutableArray *)self.objects[indexPath.section])[indexPath.row];
BFEventModel *model = [[BFEventModel alloc] init];
model.model = object;
model.indexPath = indexPath;
[cell em_displayWithModel: model];
return cell;
}
如果需要再加一种cell,就只需要创建新的cell,然后实现
BFDisplayProtocol
协议就行了。减少cell对controller的依赖,将controller中的逻辑分散道每个cell中自己实现,减少view对controller的耦合。
至此完成了不同cell的数据显示。但是cell对model是有依赖的,如果另外一个列表用到这个cell,而且model不同,就做不到重用。
相同的cell展示不同的model
针对上面提到的重用问题,采用了消息转发机制来实现
1.定义一个model基类BFPropertyExchange
// BFPropertyExchange.h
#import
@interface BFPropertyExchange : NSObject
- (NSDictionary *)em_exchangeKeyFromPropertyName;
@end
2.在基类中实现消息转发
// BFPropertyExchange.m
#import "BFPropertyExchange.h"
#import "objc/runtime.h"
#import "objc/message.h"
static void *kPropertyNameKey = &kPropertyNameKey;
@implementation BFPropertyExchange
- (NSDictionary *)em_exchangeKeyFromPropertyName{
return nil;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;
}
/**
消息转发
@param aSelector 方法
@return 调用方法的描述签名
*/
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSString *propertyName = NSStringFromSelector(aSelector);
NSDictionary *propertyDic = [self em_exchangeKeyFromPropertyName];
NSMethodSignature* (^doGetMethodSignature)(NSString *propertyName) = ^(NSString *propertyName){
//创建新签名
NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:"@@:"];
objc_setAssociatedObject(methodSignature, kPropertyNameKey, propertyName, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return methodSignature;
};
if ( [propertyDic.allKeys containsObject:propertyName] ) {
NSString *targetPropertyName = [NSString stringWithFormat:@"em_%@",propertyName];
if ( ![self respondsToSelector:NSSelectorFromString(targetPropertyName)] ) {
// 如果没有em_重写属性,则用转换字典中属性替换
targetPropertyName = [propertyDic objectForKey:propertyName];
}
return doGetMethodSignature(targetPropertyName);
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSString *originalPropertyName = objc_getAssociatedObject(anInvocation.methodSignature, kPropertyNameKey);
if ( originalPropertyName ) {
//NSInvocation对原来签名的方法执行新的方法,必须指定Selector和Target,invoke或invokeWithTarget执行
anInvocation.selector = NSSelectorFromString(originalPropertyName);
[anInvocation invokeWithTarget:self];
}
}
@end
3. 赋值模型继承基类BFPropertyExchange
实现em_exchangeKeyFromPropertyName
方法
方法原理解析
当cell通过不同模型赋值的时候,下面的theModel.model
除了是BFModel
外,还有可能是BFModel1,BFModel2...
等等。
#pragma mark - BFDisplayProtocol
- (void)em_displayWithModel:(BFEventModel *) theModel {
BFModel *model = theModel.model;
self.titleLabel.text = model.title;
......
}
这里我们统一用BFModel来接收,当传入为BFModel1
的时候,BFModel1
中没有title
属性,这时候就会报找不到方法的错误。这个时候就用了上面的消息转发机制。首先会走到+ (BOOL)resolveInstanceMethod:(SEL)sel;
方法,先征询消息接收者所属的类也就是BFModel1
,看其是否能动态添加方法,以处理当前这个无法响应的selector
。这边我们不做处理,所以返回NO
;
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
然后方法走到- (id)forwardingTargetForSelector:(SEL)aSelector;
看看有没有其它对象能处理此消息。如果有,则把消息发给那个对象,转发结束;如果没有,则启动完整的消息转发机制。因为处理模型多变,所以我们也不在这边做处理。返回nil
。
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;
}
接下来进入最关键的一步方法- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
,完整的消息转发机制。
让BFModel1
继承基类BFPropertyExchange
实现em_exchangeKeyFromPropertyName
方法。返回字典代表调用属性与本地属性的映射关系,cell的调用属性是title,此时传入BFModel1,但是BFModel1并没有title属性,则通过映射关系自动调用本地属性newtitle。
- (NSDictionary *)em_exchangeKeyFromPropertyName {
return @{@"title":@"newtitle"};
}
通过方法名在em_exchangeKeyFromPropertyName
中取得替换属性targetPropertyName
,然后将新的调用方法封装进NSMethodSignature
中。
NSString *propertyName = NSStringFromSelector(aSelector);
NSDictionary *propertyDic = [self em_exchangeKeyFromPropertyName];
NSMethodSignature* (^doGetMethodSignature)(NSString *propertyName) = ^(NSString *propertyName){
//创建新签名
NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:"@@:"];
objc_setAssociatedObject(methodSignature, kPropertyNameKey, propertyName, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return methodSignature;
};
if ( [propertyDic.allKeys containsObject:propertyName] ) {
NSString *targetPropertyName = [NSString stringWithFormat:@"em_%@",propertyName];
if ( ![self respondsToSelector:NSSelectorFromString(targetPropertyName)] ) {
// 如果没有em_重写属性,则用转换字典中属性替换
targetPropertyName = [propertyDic objectForKey:propertyName];
}
return doGetMethodSignature(targetPropertyName);
}
最后转发至- (void)forwardInvocation:(NSInvocation *)anInvocation;
,通过封装到NSInvocation对象中的属性,再给接收者最后一次机会,解决当前还未处理的问题。
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSString *originalPropertyName = objc_getAssociatedObject(anInvocation.methodSignature, kPropertyNameKey);
if ( originalPropertyName ) {
//NSInvocation对原来签名的方法执行新的方法,必须指定Selector和Target,invoke或invokeWithTarget执行
anInvocation.selector = NSSelectorFromString(originalPropertyName);
[anInvocation invokeWithTarget:self];
}
}