iOS开发iOS Developer

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

2017-04-26  本文已影响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中调用

@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

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

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

上一篇下一篇

猜你喜欢

热点阅读