iOS CollectionView/TableView使用多个cell的解耦

本文摘自:数据显示和事件处理与controller解耦

在日常开发时,经常需要使一个列表通过多种不同样式的cell来展示,比如下图中的情况:


0EE7330D-BDE2-4950-A008-3FA6AAEC3A1F.png

问题描述

多种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];
    }
}

你可能感兴趣的:(iOS CollectionView/TableView使用多个cell的解耦)