MVC架构编程模式中,Controller负责将Model模型数据更新到View视图,同时当用户对视图View数据做了修改后,还需要Controller将变换的数据更新到Model模型中。
模型Model到视图View,视图View到模型Model,这种双向的数据更新涉及到大量繁琐的数据转换和赋值操作。因此OSX Cocoa从系统层面设计了Cocoa数据绑定机制,用以简化MVC编程中这种双向更新操作。
Cocoa数据绑定机制的实现依赖KVC,KVO,KVB 三种技术,下面分别来介绍说明。
KVC是Key-value coding 键值编码的简称,提供了通过类的属性字符名称来读写属性值的方法。你不需要通过调用类的不同的方法去实现不同的属性读写修改,而只需要通过不同的属性名称,通过统一一致的接口实现读写。
KVC的好处是明显的,可以实现循环遍历读写所有的属性值,也可以支持属性路径链式调用,即嵌套对象的访问。
Cocoa在基类NSObject上实现了KVC的接口方法,因此所有类天然支持KVC方式属性读写。
我们定义Person和Phone 2个类便于后面说明KVC的属性访问方式。
Person对象类,有3个属性:名字,年龄,电话。
@interface Person : NSObject
@property(nonatomic,strong)NSString *name;
@property(nonatomic,assign)int age;
@property(nonatomic,strong)Phone *phone;
@end
电话对象类,有3个属性:办公电话,家庭电话,手机。
@interface Phone : NSObject
@property(nonatomic,strong)NSString *office;
@property(nonatomic,strong)NSString *family;
@property(nonatomic,strong)NSString *mobile;
@end
定义Person和Phone实例
Person *person= [[Person alloc]init];
person.name = @"john";
person.age = 20;
Phone *phone = [[Phone alloc]init];
phone.office = @"010-67854545";
person.phone = phone;
1.通过名字访问对象属性值
-(id)valueForKey:(NSString *)key;
2.修改属性名为key的值为value
-(void)setValue:(id)value forKey:(NSString *)key;
//使用KVC方法获取属性
NSString *name = [person valueForKey:@"name"];
//使用KVC修改属性
[person setValue:@"Habo" forKey:@"name"];
1.使用keyPath路径去获取属性
-(id)valueForKeyPath:(NSString *)keyPath;
2.更新keyPath路径对应的属性值为value
-(void)setValue:(id)value forKeyPath:(NSString *)keyPath;
//使用path获取属性
NSString *officePhone = [person valueForKeyPath:@"phone.office"];
//修改属性
[person setValue:@"010-678545466" forKeyPath:@"phone.office"];
1.访问不存在的属性,会调用此方法,如果此方法没有实现,会抛出异常。
-(id)valueForUndefinedKey:(NSString *)key;
2.修改不存在的属性值,会调用此方法,如果此方法没有实现,会抛出异常。
-(void)setValue:(id)value forUndefinedKey:(NSString *)key;
3.修改属性值为nil,会调用此方法,如果此方法没有实现,会抛出异常
-(void)setNilValueForKey:(NSString *)key;
下面是用使用setValue方法对hidden属性修改为nil,默认设置为YES
- (void)setNilValueForKey:(NSString *)theKey {
if ([theKey isEqualToString:@"hidden"]) {
[self setValue:@YES forKey:@"hidden"];
} else {
[super setNilValueForKey:theKey];
}
}
4.修改key对应的value值,对value进行有效性验证。
对每个Key单独实现validate方法
-(BOOL)validate
下面是对Person对象的age年龄进行validate的例子:
- (BOOL)validateAge:(id *)ioValue error:(NSError * __autoreleasing *)outError
if ([*ioValue integerValue] <= 0) {
if (outError != NULL) {
NSString *errorString = NSLocalizedStringFromTable(@"Age must be greater than zero", @"Person",
@"validation: zero age error");
NSDictionary *userInfoDict = @{ NSLocalizedDescriptionKey : errorString };
NSError *error = [[NSError alloc] initWithDomain:@"" code:10005 userInfo:userInfoDict];
*outError = error;
};
return NO;
}
return YES;
}
对所有Key统一实现validate方法
-(BOOL)validateValue:(inout id )ioValue forKey:(NSString *)inKey error:(NSError **)outError;
-(BOOL)validateValue:(inout id )ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
在-set
1.访问多个key对应的属性,返回key-value形式的字典对象
-(NSDictionary
2.以字典对应的key-value去更新对象对应的属性。
-(void)setValuesForKeysWithDictionary:(NSDictionary
使用这个方法在网络处理JSON对象解析中可以方便通过字典对象来更新模型对象。
@interface Phone : NSObject
@property(nonatomic,strong)NSString *office;
@property(nonatomic,strong)NSString *family;
@property(nonatomic,strong)NSString *mobile;
-(id)initWithDictionary:(NSDictionary*)attributes;
@end
#import "Phone.h"
@implementation Phone
-(id)initWithDictionary:(NSDictionary*)attributes {
self = [super init];
if(self){
[self setValuesForKeysWithDictionary:attributes];
}
return self;
}
@end
Observer观察者模式(也称为发布-订阅模式)应该可以说是应用非常广泛的设计模式之一,它定义了一种一对多的依赖关系。当一个对象的状态/属性发生变化时,会通知所有的观察者及时更新。
KVO是Key-value observing 简称,它是Cocoa系统实现的对象属性变化的通知机制,也可以理解为观察者模式的系统实现。
1.注册观察者对象
对象使用addObserver方法来增加观察者
-(void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
observer:观察者
keyPath: 对象的keyPath
options参数为配置项,一般为下面2个参数逻辑或组合
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
context:附加的上下文参数,为对象指针或其他标识值
将teacher实例注册为student实例的观察者,当student的address有变化时,会得到通知。
Teacher *teacher = [[Teacher alloc]init];
Student *student = [[Student alloc]init];
[student addObserver:teacher forKeyPath:@"address" options:NSKeyValueObservingOptionNew context:nil];
2.接收变化通知
观察者类通过实现observeValueForKeyPath接口实现接收变化通知
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
change字典中是变化的数据,有3个key,分别为kind,old,new。
kind指示是修改,插入,删除,替换变化的类型。
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
old代表修改前的旧值,new为修改后的新值。
3.删除观察者
通过removeObserver方法删除观察者对象
-(void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
NSObject本身提供了自动化的通知机制,有些情况下我们需要人工触发通知,就需要做额外的处理。
使用automaticallyNotifiesObserversForKey方法来约定key的通知方式,默认情况下所有的key都是系统自动通知,返回NO表示需要手工通知变化。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"address"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
重写key属性的set方法,通过willChangeValueForKey 和 didChangeValueForKey完成变化通知。
- (void)setAddress:(NSString*)address {
[self willChangeValueForKey:@"address"];
_address = address;
[self didChangeValueForKey:@"address"];
}
有依赖关系的属性,当依赖有变化时,希望收到这个属性变化的通知。
比如数据库路径是有文件根目录和名字合成,有getter方法如下:
- (NSString *)dbPath {
return [NSString stringWithFormat:@"%@ %@",path, fileName];
}
当文件目录或名字属性变化时,数据库的路径实际上也发生了变化。
有2种方式实现了依赖变化通知,实现下面任意一种类方法即可。
1.通用的类方法
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"dbPath"]) {
NSArray *affectingKeys = @[@"path", @"fileName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
2.针对key的类方法
+ (NSSet *)keyPathsForValuesAffectingDbPath {
return [NSSet setWithObjects:@"path", @"fileName", nil];
}
使用KVO观察者模式,学生和老师对象之间当student的address有变化时,老师得到通知。
1.对象模型类
老师 Teacher模型类
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
@interface Teacher : NSObject
@property(nonatomic,strong)NSString *school;
@property(nonatomic,strong)NSString *address;
@end
学生Studentr模型类
#import <Foundation/Foundation.h>
@interface Student : NSObject
@property(nonatomic,strong)NSString *name;
@property(nonatomic,strong)NSString *address;
@end
2.注册观察者
@interface AppDelegate ()
@property (weak) IBOutlet NSWindow *window;
@property(nonatomic,strong)Teacher *teacher;
@property(nonatomic,strong)Student *student;
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
Teacher *teacher = [[Teacher alloc]init];
Student *student = [[Student alloc]init];
student.address = @"Beijing";
self.teacher =teacher;
self.student =student;
//注册观察者
[student addObserver:teacher forKeyPath:@"address" options:NSKeyValueObservingOptionNew context:nil];
student.address = @"Nanjing";
}
3.接收通知
Teacher类接收到Student属性变化的处理
@implementation Teacher
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
NSLog(@"change %@",change);
}
@end
MVC编程模式中,模型和视图之间数据同步更新,需要编写很多繁琐的代码。Cocoa绑定技术提供了简单优雅的实现,是系统级的模型和视图之间双向数据同步机制。模型数据的修改能实时更新到视图上,反之视图的修改也能更新到模型上。
传统的编程方法中,假如我们要修改员工Employee模型对应的数据,流程如下:
保存Employee模型类到数据库。
其中第2步需要完成模型到视图的数据同步,在视图类的界面初始化中,你需要写大致如下的代码
self.idTextFiled.string = self.employee.id;
self.nameTextFiled.string = self.employee.name;
self.addressTextFiled.string = self.employee.address;
self.ageTextFiled.string = self.employee.age;
第4步需要完成界面视图上变化的数据到模型的数据同步,代码如下
self.employee.address = self.addressTextFiled.string;
可以看到,这种模型到视图,视图到模型的代码编写是相当重复繁琐和无趣的。
我们创建一个工程BindSimpleObject,使用bind技术来实现模型<->视图双向更新的例子。
@interface Employee : NSObject
@property(nonatomic,assign)NSInteger id;
@property(nonatomic,strong)NSString *name;
@property(nonatomic,assign)NSInteger age;
@property(nonatomic,strong)NSString *address;
@end
点击MainMenu.xib,从控件工具箱拖放一个Object Controller到左侧xib导航面板区
点击xib导航面板区Window,在window界面增加ID,name,address,age 4个文本label和4个NSTextField控件,完成界面设计。
注意一下这里ID和age不是string类型,如果使用普通的NSTextFiled控件,需要NSInteger到NSString的双向转换,比较麻烦,这里我们使用带Transformatter的NSTextFiled,它能自动完成这个转换。
在增加一个NSButton到界面上,绑定事件函数为okAction。
在AppDelegate.m 文件中增加一个employee属性变量,一个IBOutlet变量objectController。
@interface AppDelegate ()
@property (weak) IBOutlet NSWindow *window;
@property(nonatomic,strong)Employee *employee;
@property (strong) IBOutlet NSObjectController *objectController;
@end
从MainMenu.xib面板导航区点击Object Controller,从Connections面板将其绑定到IBOutlet变量objectController。
点击ID对应的NSTextField控件,切换到inspector的Bindings面板, Value部分勾选Bind to,下拉框中选择Object Controller。Controller Key默认是selection,Model Key Path部分输入模型类Employee中的对应的id属性。
其他3个文件输入控件类似设置完成绑定。
点击左边Object Controller,切换到inspector的Bindings面板, Controller Content部分勾选Bind to,下拉框选择Delegate(表示AppDelegate,Model Key Path:输入employee属性变量,完成了 NSObjectController绑定到Employee模型的绑定。
最终的View<->NSObjectController<->Model 3个对象之间绑定关系如下图:
AppDelege.m的applicationDidFinishLaunching中输入下面代码,完成employee变量初始化。
-(void)applicationDidFinishLaunching:(NSNotification *)aNotification {
Employee *em = [[Employee alloc]init];
em.id = 123213123;
em.name = @"John";
em.address = @"China Beijing";
em.age = 25;
self.employee = em;
}
在按钮事件函数中输入下面代码,打印输出属性变量employee的各个字段值。
-(IBAction)okAction:(id)sender {
NSLog(@"employee = %@",[self.employee objectAsDictionary]);
}
运行工程,看到界面上4个输入框都有了对应于self.employee属性变量的值。这样已经自动完成了模型到视图的数据更新。
在输入修改任意一个文本框里面的内容,点击ok按钮,查看打印输出。看到打印出来模型的字段值也相应的发生了变化。这里自动完成了视图数据到模型数据的同步更新。
Cocoa 绑定依赖几个关键技术,key-value coding (KVC),key-value observing (KVO),key-value binding (KVB), NSEditor/NSEditorRegistration。
KVC和KVO我们在前面章节已经做了介绍,下面我们主要讨论下其他相关技术。
1)建立绑定
-(void)bind:(NSString *)binding toObject:(id)observable withKeyPath:(NSString *)keyPath options:(NSDictionary*)options;
binding:绑定的名称
observable:绑定的对象
keyPath:绑定的对象的keyPath
options:参数设置,可以为空
上面例子中的NSObjectController到Employee模型的绑定可以这样编码实现:
[self.objectController bind:@"content"
toObject:self
withKeyPath:@"employee"
options:0];
2)解除绑定
-(void)unbind:(NSString *)binding;
手工编码实现的绑定必须在dealloc方法或其他合适的时机去除绑定,否则绑定对象被retain不能及时释放内容资源。
xib中建立的绑定当视图对象释放时会自动完成解除绑定。
NSObjectController的父类NSController实现了NSEditor/NSEditorRegistration协议方法。
NSEditor/NSEditorRegistration协议提供了视图对象到NSObjectController对象的通信接口,当用户编辑界面内容开始时,发送objectDidBeginEditing消息。结束编辑发送objectDidEndEditing。当用户关闭窗口前使用discardEditing,commitEditing通知NSObjectController丢弃数据或提交保存数据。
NSEditorRegistration协议
-(void)objectDidBeginEditing:(id)editor;
-(void)objectDidEndEditing:(id)editor;
NSEditor协议
-(void)discardEditing;
-(BOOL)commitEditing;
NSController为抽象类,它有4个子类:
NSObjectController,NSUserDefaultsController,NSArrayController,NSTreeController 。
NSObjectController管理单一的对象。
NSUserDefaultsController管理系统配置NSUserDefaults对象。
NSArrayController,NSTreeController 用来管理集合类对象,分别用在NSTableView和NSOutlineView视图的管理中。
NSObjectController对象除了实现了NSEditor/NSEditorRegistration协议外,当绑定对象为空时提供placeholder;对于集合类对象可以方便的管理当前选中的对象,同时提供对象删除/增加等管理方法实现。
只要模型类满足KVC,KVO兼容,实际上是可以直接绑定视图控件到模型,而不需要通过中间的NSController来桥接。但这样对处理集合类对象要增加很多额外的编码工作。
NSController类几个关键属性:
我们以前面例子中 NSTextField <-> NSObjectController <-> Employee 3个对象之间绑定处理过程来说明。
1.建立对象之间绑定
NSObjectController调用bind方法发送绑定消息
[objectController bind:@contentObject
toObject:appDelegate withKeyPath:@employee
options:nil];
NSTextField调用bind方法发送绑定消息
[textFiled bind:@value
toObject:objectController withKeyPath:@selection.id
options:nil];
2.注册KVO
NSObjectController注册KVO,增加NSTextField对象为自己的观察者对象
[objectController addObserver:NSTextField forKeyPath:@selection.id
options:0 context:context];
Employee注册KVO,增加NSObjectController对象为自己的观察者对象
[employee addObserver:objectController forKeyPath:@id
options:0 context:context];
3.用户输入数据
当用户输入数据修改了内容后,NSTextField通过KVC更新NSObjectController的内容
[objectController setValue:4 forKeyPath:@selection.id
];
NSObjectController通过KVC更新Employee的内容
[employee setValue:4 forKeyPath:@id
];
4.修改模型Employee数据
由于NSObjectController是Employee的观察者,因此发生KVO通知到NSObjectController,NSObjectController更新数据。
NSObjectController的变化同样由于KVO会通知到NSTextField,最终更新了界面上的数据。
创建工程BindArrayObject,模型类使用之前例子中定义的Employee。
打开MainMenu.xib,添加NSArrayController到xib导航区。window界面增加4个Label和4个NSTextField控件,添加NSTableView控件,完成设计设计如下图。
ArrayController的Attributes属性面板配置Class Name为Employee,Keys列表增加4个属性key。
AppDelegate.m 接口部分定义NSArrayController类型arrayController变量,建立xib中NSArrayController对象到IBOutlet变量关联。
@property(strong) IBOutlet NSArrayController *arrayController;
定义数组变量
@property(nonatomic,strong)NSMutableArray *anArray;
代码完成arrayController到数组anArray的绑定
NSDictionary *options = @{ NSAllowsEditingMultipleValuesSelectionBindingOption:@YES };
[self.arrayController bind:@"contentArray"
toObject:self
withKeyPath:@"anArray"
options:options];
4个文本输入框依次完成绑定,Model Key Path对应Emploee中的属性字段
NSTableView中的4个NSTableColumn 依次完成绑定,Model Key Path对应Employee中的属性字段
Add,Remove Button分别绑定事件响应函数,Array Controller的add: remove:方法即可。
运行工程修改编辑4个文本框的内容,表格内容会同步更新。修改表格中单元内容也会更新到上面的对应的文本输入框,点击增加,删除都可以正常工作。
创建Xcode新工程BindTreeObject。
MainMenu.xib在window界面上拖放一个NSOutlineView控件,添加5个增删相关按钮如下图。
从控件工具箱拖放一个Tree Controller到左边xib结构导航区。
对于树形结构的视图,每个节点都要包括名称和子节点信息。我们可以用一个TreeNode模型来来描述节点信息。
@interface TreeNode : NSObject
@property(nonatomic,strong)NSString *nodeName;//名称
@property(nonatomic,assign)NSInteger count;//子节点个数
@property(nonatomic,assign)BOOL isLeaf;//是否叶子节点
@property(nonatomic,strong)NSArray *children;//子节点
@end
bind数据在处理过程中都是通过KVC的keyPath访问数据的,因此我们也可以等价的使用Cocoa的字典NSDictionary来直接描述节点信息。
NSDictionary *node = @{ @nodeName
: @Group
,
@children
: @[
@{@name
: @m1
,},
@{@name
: @m2
}
]
};
其中子节点个数和是否是子节点都不需要了,count(子节点个数),isLeaf(是否子节点)的值都可以通过children 子节点数组推算出来,如果children的count为0,则子节点个数个数count为0,isLeaf为YES。
NSTreeController.h中几个重要的属性方法定义
@property (copy) NSString *childrenKeyPath; // key used to find the children of a model object.
@property (copy) NSString *countKeyPath; // optional for performance
@property (copy) NSString *leafKeyPath;
上面这3个属性对应于xib中NSTreeController 对象属性页面上的Key Paths里面的3个设置项:
Children,Count,Leaf。根据之前的分析 只要设置了Children就可以了。
这里我们在NSTreeController Attributes面板配置Key Paths下的Children为上面字典NSDictionary中定义的key children,并在下面 Class Name中输入NSMutableDictionary,在keys列表中增加name,children 2个key。
NSTreeController定义实现了增加删除节点的方法
-(void)add:(id)sender; // 增加新节点
-(void)remove:(id)sender; //删除选择的节点
-(void)addChild:(id)sender; // 在当前选中的节点增加一个子节点
-(void)insert:(id)sender; // 插入一个节点在当前选中的节点之前
-(void)insertChild:(id)sender; // inserts a new first child into the children array of the first selected node
将上图中5个按钮Add,Add Child,Insert,Insert Child,Remove分别绑定到NSTreeController到上面5个方法,这样就可以管理树的节点数据了。
NSOutlineView数据源Content绑定
从xib界面上选中NSOutlineView,右侧inspector面板上切换到bindings绑定面板。从Outline View Content部分中Content勾选Bind to,下拉列表选择Tree Controller,Selection index Paths 勾选Bind to,从下拉列表选择Tree Controller。完成NSOutlineView到NSTreeController数据绑定。
NSOutlineView列单元cell数据绑定
点击NSOutlineView列上面的Table View Cell部分,实际上Table View Cell是一个NSTextField控件,将其Value绑定到Table Cell View,勾选Bind to 选择即可。Model Key Path绑定到 ObjectValue.name。 ObjectValue实际上代表的是当前节点对应的模型对象。name为其名字的key。
AppDelegate.m的接口中增加treeNodes属性变量
@property(nonatomic,strong) NSMutableArray *treeNodes;
Tree Controller绑定到Delegate对象,Model Key Path为treeNodes。
AppDelegate.m实现init方法完成数据初始化。
- (id)init {
self = [super init];
if (self) {
NSMutableDictionary *mNode = [self newObject];
self.treeNodes = [NSMutableArray array];
[self.treeNodes addObject:mNode];
}
return self;
}
newObject为创建一个节点数据的方法。
- (NSMutableDictionary*)newObject {
NSMutableDictionary *node = [NSMutableDictionary dictionary];
node[@"name"]= @"Group";
NSMutableArray *children=[NSMutableArray array];;
node[@"children"]= children;
NSMutableDictionary *m1 = [NSMutableDictionary dictionary];
m1[@"name"]= @"m1";
[children addObject:m1];
NSMutableDictionary *m2 = [NSMutableDictionary dictionary];
m2[@"name"]= @"m1";
[children addObject:m2];
return node;
}
运行App看到初始的数据已经正常显示出来,点击5个功能按钮可以正常管理增加删除节点了。