美文网首页iOS开发iOS Developer
数据显示和事件处理与controller解耦

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

作者: 东篱先生_ | 来源:发表于2017-04-26 18:34 被阅读0次

    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设置埋点了。

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

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/plhizttx.html