5.8更新demo,歡迎拍磚????
問題描述
在開發(fā)應用時络凿,經(jīng)常遇到一個列表多種不同樣式的cell展示的情況,如圖:
多種cell就會造成cellForRowAtIndexPath(tableview)大量的if /else,如果再加上顯示數(shù)據(jù)和事件處理簡直是災難,而且不利于以后的擴展秦陋,難以維護蔓彩。
解決方案
1.定義一個協(xié)議
/**
顯示數(shù)據(jù)協(xié)議
*/
@protocol BFDisplayProtocol <NSObject>
- (void)em_displayWithModel:(BFEventModel *)model;
@end
2.cell中實現(xiàn)BFDisplayProtocol協(xié)議
#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代理中調(diào)用顯示數(shù)據(jù)方法
- (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;
}
如此,產(chǎn)品經(jīng)理說列表再加一種cell赤嚼,你就只需要創(chuàng)建新的cell旷赖,然后實現(xiàn)BFDisplayProtocol協(xié)議就行了,甚至CollectionView/TableView代理都不需要修改更卒。這樣做的好處就是減少cell對controller的依賴等孵,將controller中的邏輯分散道每個cell中自己實現(xiàn),減少view對controller的耦合蹂空。最后代理方法cellForItemAtIndexPath看上去非常整潔舒服??俯萌。
現(xiàn)在問題來了??果录,2.中cell對model是有依賴的,也就是說有另一個列表也需要用到這個cell咐熙,而且model不同弱恒,就無法重用此cell了。現(xiàn)在要做的是解除cell對model的依賴棋恼,這時也可以用上面協(xié)議的方法實現(xiàn)返弹,就是為model的每一個屬性生成一個get方法的協(xié)議集合,然后所有的model實現(xiàn)這一個協(xié)議爪飘,在model中實現(xiàn)協(xié)議的方法返回數(shù)據(jù)义起。這種情況當model字段少時可以一試,但是當model屬性很多時师崎,就會出發(fā)大量的協(xié)議方法默终,而且有新的cell共用又要新建大量的共用協(xié)議。所以實現(xiàn)協(xié)議不能很好的解決cell對model的依賴問題抡诞。
問題描述
解決cell對model的依賴
解決方案
既然協(xié)議不能很好的解決該問題穷蛹,那么我們就曲線救國,有一種輕量的解決辦法昼汗,就是利用消息轉(zhuǎn)發(fā)實現(xiàn)肴熏。
1.定義一個model基類BFPropertyExchange
@interface BFPropertyExchange : NSObject
- (NSDictionary *)em_exchangeKeyFromPropertyName;
@end
2.model實現(xiàn)em_exchangeKeyFromPropertyName方法
- (NSDictionary *)em_exchangeKeyFromPropertyName {
return @{@"name2":@"name",@"icon1":@"icon",@"iconUnselect1":@"iconUnselect"};
}
返回字典代表調(diào)用屬性與本地屬性的映射關(guān)系,cell的調(diào)用屬性是name2顷窒,此時傳入另一個modelA,但是modelA并沒有name2屬性蛙吏,則通過映射關(guān)系自動調(diào)用本地屬性name。
3.消息轉(zhuǎn)發(fā)(最重要的一步)
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;
}
/**
消息轉(zhuǎn)發(fā)
@param aSelector 方法
@return 調(diào)用方法的描述
*/
- (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];
}
}
此處走的是最后一步的完全消息轉(zhuǎn)發(fā)鸦做,不熟悉消息轉(zhuǎn)發(fā)的同學,我找了一個帖子可以看一下:消息轉(zhuǎn)發(fā)
4.cell中調(diào)用
- 為了方便調(diào)用谓着,此處給model寫了一個類別泼诱。
@interface NSObject (PropertyExchange)
/**
調(diào)用替換屬性 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中調(diào)用
#pragma mark - BFDisplayProtocol
- (void)em_displayWithModel:(CircleItem *)model {
self.titleLabel.text = model.em_property(@"name2");
......
}
梳理一下調(diào)用流程:調(diào)用model的name2屬性,通過em_exchangeKeyFromPropertyName方法返回屬性映射關(guān)系找到name赊锚,然后通過消息轉(zhuǎn)發(fā)調(diào)用name屬性治筒。
至此間接了完成cell對model的依賴,如果只是顯示屬性那么已經(jīng)可以重用了舷蒲。那么現(xiàn)在問題又來了??耸袜,如果cell中有事件處理操作,那么就無法重用了牲平?堤框??
問題描述
實現(xiàn)cell中事件處理解耦
解決方案
1.定義點擊事件的協(xié)議
/**
點擊事件協(xié)議
*/
@protocol BFEventManagerProtocol <NSObject>
- (void)em_didSelectItemWithModel:(BFEventModel *)eventModel;
- (NSString *)em_eventManagerWithPropertName;
@end
2.定義基類BFEventManager并實現(xiàn)BFEventManagerProtocol協(xié)議,然后定義BFEventManager的子類蜈抓,在子類中實現(xiàn)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中調(diào)用事件處理
- (void)em_displayWithModel:(CircleItem *)model {
@weakify(self)
[self.button addActionHandler:^(NSInteger tag) {
@normalize(self)
[self.eventManager em_didSelectItemWithModel:model];
}];
......
}
以上中eventManager定義一個類別來獲取,通過runtime實現(xiàn)獲取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;
}
現(xiàn)在將cell中的事件處理交由EventManager處理酬土,如果重用cell,只需傳入不同的eventType格带,然后在EventManager的子類中根據(jù)不同的eventType做相應的處理撤缴。這樣cell就可以完全重用了,而且頁面的事件做到了統(tǒng)一管理叽唱,相同的事件處理還可以重用屈呕。實際項目中還體會了統(tǒng)一管理的好處,就是當別人還去繁雜的頁面去尋找事件設置埋點時棺亭,而你卻只需要優(yōu)雅的打開EventManager設置埋點了虎眨。
以上就算是拋磚引玉吧,排版有點亂镶摘,代碼可以在這里找到嗽桩,如果覺得有幫助順便加個??,謝謝????凄敢。