数据显示和事件处理与controller解耦

5.8更新demo,欢迎拍砖😄😄

问题描述

在开发应用时,经常遇到一个列表多种不同样式的cell展示的情况,如图:

WX20170426-182305@2x.png

多种cell就会造成cellForRowAtIndexPath(tableview)大量的if /else,如果再加上显示数据和事件处理简直是灾难,而且不利于以后的扩展,难以维护。

解决方案

1.定义一个协议

/**
 显示数据协议
 */
@protocol BFDisplayProtocol <NSObject>
- (void)em_displayWithModel:(BFEventModel *)model;
@end

2.cell中实现BFDisplayProtocol协议


#pragma mark - BFDisplayProtocol

- (void)em_displayWithModel:(CircleItem *)model {
    self.titleLabel.text = model.circleName;
    self.distanceLabel.text = [NSString stringWithFormat:@"%ldm",model.distance];
}

此处cell无需将子view属性暴露出来。

3.在CollectionView/TableView代理中调用显示数据方法

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    BFDCardNode *model = self.dataSources[indexPath.section];
    UICollectionViewCell<BFDisplayProtocol> *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kIdentifier forIndexPath:indexPath];
    [cell em_displayWithModel:model];
    return cell;
}

如此,产品经理说列表再加一种cell,你就只需要创建新的cell,然后实现BFDisplayProtocol协议就行了,甚至CollectionView/TableView代理都不需要修改。这样做的好处就是减少cell对controller的依赖,将controller中的逻辑分散道每个cell中自己实现,减少view对controller的耦合。最后代理方法cellForItemAtIndexPath看上去非常整洁舒服😌。

现在问题来了😂,2.中cell对model是有依赖的,也就是说有另一个列表也需要用到这个cell,而且model不同,就无法重用此cell了。现在要做的是解除cell对model的依赖,这时也可以用上面协议的方法实现,就是为model的每一个属性生成一个get方法的协议集合,然后所有的model实现这一个协议,在model中实现协议的方法返回数据。这种情况当model字段少时可以一试,但是当model属性很多时,就会出发大量的协议方法,而且有新的cell共用又要新建大量的共用协议。所以实现协议不能很好的解决cell对model的依赖问题。


问题描述

解决cell对model的依赖

解决方案

既然协议不能很好的解决该问题,那么我们就曲线救国,有一种轻量的解决办法,就是利用消息转发实现。

1.定义一个model基类BFPropertyExchange

@interface BFPropertyExchange : NSObject
- (NSDictionary *)em_exchangeKeyFromPropertyName;
@end

2.model实现em_exchangeKeyFromPropertyName方法

- (NSDictionary *)em_exchangeKeyFromPropertyName {
    return @{@"name2":@"name",@"icon1":@"icon",@"iconUnselect1":@"iconUnselect"};
}

返回字典代表调用属性与本地属性的映射关系,cell的调用属性是name2,此时传入另一个modelA,但是modelA并没有name2属性,则通过映射关系自动调用本地属性name。

3.消息转发(最重要的一步)

+ (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:"v@:"];
        objc_setAssociatedObject(methodSignature, kPropertyNameKey, propertyName, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        
        return  [NSMethodSignature signatureWithObjCTypes:"v@:"];
    };
    
    if ( [propertyDic.allKeys containsObject:propertyName] ) {
        
        NSString *targetPropertyName = [NSString stringWithFormat:@"em_%@",propertyName];
        if ( ![self respondsToSelector:NSSelectorFromString(targetPropertyName)] ) {
            // 如果没有em_重写属性,则用model原属性替换
            targetPropertyName = [propertyDic objectForKey:propertyName];
        }
        
        return doGetMethodSignature(targetPropertyName);
    }
    
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    
    NSString *originalPropertyName = objc_getAssociatedObject(anInvocation.methodSignature, kPropertyNameKey);
    
    if ( originalPropertyName ) {
        anInvocation.selector = NSSelectorFromString(originalPropertyName);
        [anInvocation invokeWithTarget:self];
    }
    
}

此处走的是最后一步的完全消息转发,不熟悉消息转发的同学,我找了一个帖子可以看一下:消息转发

4.cell中调用

  • 为了方便调用,此处给model写了一个类别。
@interface NSObject (PropertyExchange)

/**
 调用替换属性 Invocation property
 */
@property (nonatomic, copy) id(^em_property)(NSString *propertyName);

@end

@implementation NSObject (PropertyExchange)

#pragma mark - Getter&&Setter

- (id(^)(NSString *))em_property {
    
    __weak typeof(self) weakSelf = self;
    id (^icp_block)(NSString *propertyName) = ^id (NSString *propertyName) {
        __strong typeof(self) strongSelf = weakSelf;
        
        SEL sel = NSSelectorFromString(propertyName);
        if ( !sel ) return nil;
        SuppressPerformSelectorLeakWarning(
                                           return [strongSelf performSelector:NSSelectorFromString(propertyName)];
                                           );
    };
    
    return icp_block;
}

@end
  • 在cell中调用

#pragma mark - BFDisplayProtocol

- (void)em_displayWithModel:(CircleItem *)model {
    self.titleLabel.text = model.em_property(@"name2");
    ......
}

梳理一下调用流程:调用model的name2属性,通过em_exchangeKeyFromPropertyName方法返回属性映射关系找到name,然后通过消息转发调用name属性。

至此间接了完成cell对model的依赖,如果只是显示属性那么已经可以重用了。那么现在问题又来了😂,如果cell中有事件处理操作,那么就无法重用了???


问题描述

实现cell中事件处理解耦

解决方案

1.定义点击事件的协议

/**
 点击事件协议
 */
@protocol BFEventManagerProtocol <NSObject>

- (void)em_didSelectItemWithModel:(BFEventModel *)eventModel;

- (NSString *)em_eventManagerWithPropertName;

@end

2.定义基类BFEventManager并实现BFEventManagerProtocol协议,然后定义BFEventManager的子类,在子类中实现em_didSelectItemWithModel方法。

static const int BFGSpacEventTypeSectionSearch           = 1;// 搜索
static const int BFGSpacEventTypeSectionBack             = 2;// 返回

@interface BFGSpaceEventManager : BFEventManager

@end

@implementation BFGSpaceEventManager

- (void)em_didSelectItemWithModel:(BFEventModel *)eventModel {
    
    NSInteger eventType = eventModel.eventType;
    
    switch ( eventType ) {
        case BFGSpacEventTypeSectionSearch:
        {
            // 搜索
            [BFAnalyticsHelper event:@"GatherPlace_MorePlaceChoice_MoreNearby"];
            
            [[LKGlobalNavigationController sharedInstance] pushViewControllerWithUrLPattern:URL_GS_SEARCH_LIST];
            
        }
            break;
        case BFGSpacEventTypeSectionBack:
        {
            // 返回
            [BFAnalyticsHelper event:@"GatherPlace_Scan"];
          
            [[LKGlobalNavigationController sharedInstance] popPPViewController];
            
        }
            break;
        default:
            break;
    }
}

3.在controller初始化BFEventManager

- (BFEventManager *)eventManager {
    if( !_eventManager ) {
        _eventManager = [[BFGSpaceEventManager alloc] initWithTarget:self];
    }
    return _eventManager;
}

4.在cell中调用事件处理

- (void)em_displayWithModel:(CircleItem *)model {
    @weakify(self)
    [self.button addActionHandler:^(NSInteger tag) {
        @normalize(self)
        [self.eventManager em_didSelectItemWithModel:model];
    }];
    ......
}

以上中eventManager定义一个类别来获取,通过runtime实现获取eventManager,代码如下:

- (BFEventManager *)eventManager {
    
    BFEventManager *tempEventManager = objc_getAssociatedObject(self, kEventManagerKey);
    if ( !tempEventManager ) {
        
        UIViewController<BFEventManagerProtocol> *controller = (UIViewController<BFEventManagerProtocol> *)self.em_viewController;
        
        if ( [controller respondsToSelector:@selector(em_eventManagerWithPropertName)]) {
            
            NSString *propertyName = [controller em_eventManagerWithPropertName];
            
            tempEventManager =  [controller valueForKey:propertyName];
            
        } else {
        
            unsigned int propertCount = 0;
            objc_property_t *properts = class_copyPropertyList(controller.class, &propertCount);
            for (int i = 0; i < propertCount; i++) {
                objc_property_t property = properts[i];
                NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
//                NSString *property_Attributes = [NSString stringWithUTF8String:property_getAttributes(property)];

                id tempPropert = [controller valueForKey:propertyName];
                if ( tempPropert && [tempPropert isKindOfClass:[BFEventManager class]] ) {
                    tempEventManager =  tempPropert;
                    break;
                }
            }
            free(properts);
        }
        
        objc_setAssociatedObject(self, kEventManagerKey, tempEventManager, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    return tempEventManager;
}

现在将cell中的事件处理交由EventManager处理,如果重用cell,只需传入不同的eventType,然后在EventManager的子类中根据不同的eventType做相应的处理。这样cell就可以完全重用了,而且页面的事件做到了统一管理,相同的事件处理还可以重用。实际项目中还体会了统一管理的好处,就是当别人还去繁杂的页面去寻找事件设置埋点时,而你却只需要优雅的打开EventManager设置埋点了。

以上就算是抛砖引玉吧,排版有点乱,代码可以在这里找到,如果觉得有帮助顺便加个🌟,谢谢😁😁。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,590评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,409评论 25 707
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,121评论 29 470
  • 关键词:同情题主:女问:冷大,你好,我97年。因为爸妈离婚纠纷,爸爸自杀了。妈妈也在我很小的时候就已改嫁。所有人都...
    冷爱阅读 516评论 0 0
  • 一 影片以二战后期的日本神户为大背景,通过哥哥清太和妹妹节子命运的变化,折射出战争给日本带来的伤害。 由于空袭和海...
    阿弥陀佛加油鸭阅读 367评论 0 1