在开发应用时,一般我们都是一个view对应一个model作为它的数据源,model的实例变量值通过一定的对应关系显示到view上,这就造成了view依赖model。一个view的存在必须依赖指定类型的model。
比如有个view
@interface CCView : UIView
@property (nonatomic,strong) UILabel *titleLabel;
@property (nonatomic,strong) UILabel *subtitleLabel;
@property (nonatomic,strong) CCAModel *aModel;
@end
它的数据源model
@interface CCAModel : NSObject
@property (nonatomic,strong) NSString *name;
@property (nonatomic,strong) NSString *age;
@end
我们一定会有如下代码,view内容的显示逻辑
self.titleLabel.text = aModel.name;
self.subtitleLabel.text = aModel.age;
而有的时候两种类型model都显示成同一种类型的view,现在又多了一个数据源类型。
@interface CCBModel : NSObject
@property (nonatomic,strong) NSString *story;
@property (nonatomic,strong) NSString *description;
@end
我猜你马上会想到两种非常简单的方法解决。一种是让view去兼容这两个model,在view显示的时候去辨别当前数据源是哪个类型的model我要显示它的哪个实例变量,也就是要给view添加多个类型model的实例变量;一种重新生成一个新的model来作兼容,把其他类型的model都转为同一种类型。那么问题来了,如果使用第一种方法我一个view会有多个类型model做数据源时代码会写的非常复杂,每次新增都会引起很多代码变动,代码会变得非常庞大并包含很多if else。如果使用第二种方法那每新增一个model类型都要做一次转化。下面来说说最近用到的两个方法。
类簇形式
类簇,在iOS中大量的被使用,它将一些私有的、具体的子类组合在一个公共的、抽象的超类下面,由超类做一个所有对外接口的承载。NSString,NSArray,NSDictionary,NSNumber都是类簇的实现。在背后它隐藏了很多实现类却只暴露最简单的接口。有时你会发现,变量声明的NSString类型,打印出来发现是__NSCFString类型,说明NSString类型的对象实际是用__NSCFString类型实例化的。
接着分析上面的问题,当一个类型的view有多个类型model作为数据源时,这些model具有相似的行为就是给view上显示的元素提供对应的数据,仅仅他们的实现形式可能会有不同,我们就可以抽象出超类。view并不关心是哪个类型model对它提供数据,只关心要显示的内容,model的差异都在工厂内处理,view需要的数据用协议进行声明。
所以在view增加对数据源获取的协议
@protocol CCViewModelProtocol <NSObject>
- (NSString *)title;
- (NSString *)subtitle;
@end
@interface CCView : UIView
@property (nonatomic,strong) UILabel *titleLabel;
@property (nonatomic,strong) UILabel *subtitleLabel;
@property (nonatomic,strong) id<CCViewModelProtocol> model;
@end
model就是view指定的数据源。然后我们创建一个承载对接view接口的类。这个类可以不作为超类,因为在实际开发当中model的来源可能很多。并不一定都能继承子同一个类,所以只要它满足协议就可以了,这样更为灵活。
@interface CCModel : NSObject <CCViewModelProtocol>
- (id)modelWithType:(NSInteger)type;
@end
@implementation CCModel
- (id)modelWithType:(NSInteger)type {
switch (type) {
case 0:
return [CCAModel new];
break;
case 1:
return [CCBModel new];
break;
default:
return [super init];
break;
}
}
- (NSString *)title {
return @"";
}
- (NSString *)subtitle {
return @"";
}
@end
之后在修改各个model增加CCViewModelProtocol协议即可。必要的时候可以新建一个子类继承原有的数据类型,再添加协议。总之这里的核心就是定义了一个view需要数据源接口的协议,然后通过一个抽象的超类包装所有具体实现的那些model数据类型。
代理形式
NSProxy用来实现消息转发,给了我们一个思路。view对显示数据的获取,从一个代理获取。代理里面做的事情,就是把消息转发给对应的model数据类型。
我们同样需要一个协议,这个协议定义view所需要的数据接口。
@protocol CCViewModelProtocol <NSObject>
- (NSString *)title;
- (NSString *)subtitle;
@end
@interface CCView : UIView
@property (nonatomic,strong) UILabel *titleLabel;
@property (nonatomic,strong) UILabel *subtitleLabel;
@property (nonatomic,strong) id<CCViewModelProtocol> proxy;
@end
差别只是在于中间接口的承载着,用代理来实现消息的转发。把具体实现接口的实例注入到代理中,然后由代理进行消息转发。
@interface CCProxy : NSProxy
- (void)injectObject:(id)object;
@end
@implementation CCProxy {
id _object;
}
- (void)injectObject:(id)object{
_object = object;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
if ([_object respondsToSelector:sel]) {
return [_object methodSignatureForSelector:sel];
}
return [super methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
if ([_object respondsToSelector:invocation.selector]) {
[invocation invokeWithTarget:_object];
return;
}
[super forwardInvocation:invocation];
}
@end
调用时的代码如下
CCAModel *aModel = [[CCAModel alloc] init];
CCProxy *proxy = [CCProxy alloc];
[proxy injectObject:aModel];
view.proxy = (id)proxy;
view.titleLabel.text = [view.proxy title];
view.subtitleLabel.text = [view.proxy subtitle];
这只是一个简单代理的使用方式,同时我们可以用它支持更多协议。只要在注入对象时,同时注入对应的协议。用一个字典来保存协议和对象的对应关系,消息转发时再用respondsToSelector来判断,这样可以代理就会变得更加灵活。
总结一下,view与model解耦的核心在于view获取对应数据内容时抽象出的协议接口。这些接口的实现可以通过一个类簇(其实就是一个抽象工厂)或代理来实现。类簇的方式返回真正执行接口的对象,代理的方式把消息转发给真正实现接口的对象。通过中间这一层将view于model分离做到解耦。