测试已测试 | ✓ |
Lang语言 | Obj-CObjective C |
许可协议 | MIT |
发布最后发布 | 2015年10月 |
由 Marko Hlebar 构建。
依赖 | |
libextobjc | >= 0 |
NODE_ | >= 0 |
iOS 的数据绑定和 MVVM
NSValueTransformer
和 BNDAsyncValueTransformer
子类进行 数据转换BIND 提供了创建你的 view
- viewModel
和 viewModel
- model
绑定的简化接口。你可以通过从实现 BNDView
和 BNDViewModel
协议的类派生来利用这些绑定。以下示例将 viewModel
的某些属性与相应的 view
绑定在一起。
@implementation MHNameTableCell
BINDINGS(MHPersonNameViewModel,
BINDViewModel(name, ~>, textLabel.text),
BINDViewModel(ID, ~>, detailTextLabel.text),
BINDViewModelCommand(reverseNameCommand, onTouchUpInside),
nil);
...some other code...
@end
这是如何工作的?它会做些什么?实现 BNDView
协议的视图或视图控制器暴露 viewModel
和 bindings
属性。viewModel
更新时(例如,在单元格重用时),bindings
也会更新,从而保证了正确的 viewModel
被绑定到正确的 view
。
同样,你也可以将 viewModel
与 model
绑定。你可以通过创建一个继承自 BNDViewModel
的 viewModel
子类,并按如下方式定义你的绑定来实现这一点。
@implementation MHPersonNameViewModel
BINDINGS(MHPerson,
BINDModel(fullName, <>, name),
BINDModel(hexColorCode, <>, hexColorCode),
BINDModel(ID.stringValue, ~>, ID),
nil);
...some other code...
@end
KVO 看起来很丑陋,如果忘记删除你的观察者,它将导致你的应用崩溃。你可以使用 BIND 来实现与 KVO 相同的目的。假设你有一个 UILabel *nameLabel
和代表 nameLabel
内容的 viewModel
。当 viewModel
由于任何原因更改其 name
值时,这应该自动在标签上可见。
viewModel.name = @"Kim";
BIND(viewModel, name, ~>, nameLabel, text);
//nameLabel.text says Kim at this point.
viewModel.name = @"Hobbit";
//nameLabel.text says Hobbit at this point.
现在假设你只想观察 viewModel
中 name
值的变化。你可以通过使用 observe 动作来实现。
[BINDO(viewModel,name) observe:^(id observable, id value, NSDictionary *observationInfo) {
//fired when viewModel changes it's name property
}];
请注意,在上述示例中,绑定并没有分配给任何实例变量。当观察对象被回收时,绑定会自动被断开。从版本 1.1.0 开始,BIND 会自动处理已绑定对象的解绑。这意味着不再会有以下这样的 KVO 异常:"NSInternalInconsistencyException", "An instance 0xF00B400 of class XYZ was deallocated while key value observers were still registered with it.
然而,你可以通过调用 unbind
方法手动解绑绑定,如下所示:
BNDBinding *binding = BIND(engine, rpm, ~>, car, speed);
...
[binding unbind];
您可以通过使用transform:
操作来改变值的变换方式。让我们以之前的示例为基础,并假设我们需要在标签中显示名称的大写。
...
viewModel.name = @"Kim";
[BIND(viewModel, name, ~>, nameLabel, text) transform:^id(id sender, id value) {
return value.uppercaseString;
}];
//nameLabel.text says @"KIM" at this point.
viewModel.name = @"Hobbit";
//nameLabel.text says @"HOBBIT" at this point.
...
观察操作在之前的示例中已介绍。使用观察操作被 passively 用来观察绑定的值何时发生变化,类似于 KVO。
[BINDSR(viewModel,name,~>,nameLabel) observe:^(id observable, id value, NSDictionary *observationInfo){
//Fired when name changes on the viewModel.
}];
BIND 允许您分配自己子类的 NSValueTransformer
用于从对象到其他对象的转换,以及反向。
使用 NSValueTransformer
子类而不是 block 变换是一个设计决策,取决于您在变换中放入了多少逻辑。使用 NSValueTransformer
子类更容易测试,并且可重用,但对于琐碎的变换,可能最好使用 transform:
操作。
您可以构建您自己的 NSValueTransformer
子类并将它轻松地分配给绑定。在分配一个值转换器时,您可以使用它的类名或转换器名称。如果您传递了一个尚未注册的类名,BIND 将注册那个 NSValueTransformer
,并在后续调用中用类名作为注册的转换器名称,从而节省内存和处理时间。
...
viewModel.name = @"Kim";
//Set the transformer by using BINDT() macro
BINDT(viewModel, name, ~>, nameLabel, text, UppercaseStringTransformer);
//nameLabel.text says @"KIM" at this point.
viewModel.name = @"Hobbit";
//nameLabel.text says @"HOBBIT" at this point.
...
//Trivial transformer implementation
@interface UppercaseStringTransformer : NSValueTransformer
@end
@implementation UppercaseStringTransformer
- (NSString *)transformValue:(NSString *)string {
return string.uppercaseString;
}
- (NSString *)reverseTransformValue:(NSString *)string {
return string.lowercaseString;
}
@end
传递 UppercaseStringTransformer
告诉绑定使用 NSValueTransformer
的 UppercaseStringTransformer
子类来变换值。如果需要,您可以添加一个 !
修饰符以改变变换方向,如下所示 BINDRT(viewModel, name, ~>, nameLabel, text, !, UppercaseStringTransformer)
。
假设您想异步地从网页中获取一个图像,使用从 NSURL
到 UIImage
的变换。您可以通过创建一个 BNDAsyncValueTransformer
子类并实现它的 `transform` 和 `reverseTransform` 方法来实现这一点。以下是一个此类实现的简单示例。
...
BINDT(self,viewModel.imageURL,~>,self,imageView.image,BNDURLToImageTransformer);
...
@implementation BNDURLToImageTransformer
- (void)asyncTransformValue:(NSURL *)value
transformBlock:(BNDAsyncValueTransformBlock)transformBlock {
NSURLRequest *request = [NSURLRequest requestWithURL:value];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue new]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (data) {
UIImage *image = [UIImage imageWithData:data];
transformBlock(response.URL, image);
}
}];
}
+ (BOOL)allowsReverseTransformation {
return NO;
}
@end
请注意,不支持双向异步绑定,并会抛出异常。
观察表达式 name ~> textLabel.text
中的符号 ~>
。BIND 语法允许您配置绑定在绑定对象值上的反 映方式。它提供了六种不同的方向和初始值赋值配置。
name ~> textLabel.text /// left object passes values to right object
name <~ textLabel.text /// right object passes values to left object.
name <> textLabel.text /// binding is bidirectional. Initial value is passed from left to right object.
name !~> textLabel.text /// left object passes values to right object with no initial value assignment.
name <~! textLabel.text /// right object passes values to left object with no initial value assignment.
name <!> textLabel.text /// binding is bidirectional with no initial value assignment.
您可以使用缩写运算符来创建大多数绑定。缩写是为了表达简短而设计的,同时仍然保持绑定属性的清晰性。
UILabel *nameLabel ... //a label instance
UITextField *nameField ... // a textfield instance
id viewModel ... //a viewModel containing property name
UIButton *button ... //a button instance
BINDS(nameField,~>,nameLabel);
nameField.text = @"Kim";
//nameLabel.text says Kim at this point.
BINDSR(viewModel,name,~>,nameLabel);
viewModel.name = @"Hobbit";
//nameLabel.text says Hobbit at this point.
BINDSL(nameField,~>,viewModel,name);
nameField.text = @"Cartman";
//viewModel.name says Cartman at this point.
[BINDOS(button) observe:^(id observable, id value){
//Fired when you press the button.
}];
这些缩写实际上是一些记忆提示,所以您可以将它们理解为
BINDS
绑定缩写BINDSR
绑定缩写右BINDSL
绑定缩写左BINDOS
绑定观察缩写此架构提供了责任分配的明显分布以及业务逻辑和表示层的清晰分割,这使得代码更易于测试和维护。以下图表代表一个建议的 MVVMC 应用架构,我们将在以下内容中对此进行更详细的解释。
数据控制器负责将模型从外部来源(例如,Web服务)转换为视图模型,视图模型与视图元素(视图或视图控制器)形成1:1的关系。BIND提供了一个协议BNDDataController
,它公开了以下方法:
- (void)updateWithContext:(id)context
viewModelsHandler:(BNDViewModelsBlock)viewModelsHandler;
理想情况下,每个数据控制器都应该只向拥有的视图控制器公开此方法。调用此方法应触发一系列事件,例如从Web服务获取模型,然后将该模型转换为与您的视图相对应的视图模型。
视图控制器持有数据控制器的引用,并启动对视图模型的请求数。通常,最好在UIViewController
的viewWillAppear:
回调方法中刷新视图模型。
- (void)viewWillAppear:(BOOL)animated {
__weak typeof(self) weakSelf = self;
[self.dataController updateWithContext:someContext
viewModelsHandler:^(NSArray *viewModels, NSError *error) {
weakSelf.viewModels = viewModels;
//Do stuff with view models here
//like call reloadData on the tableView.
}];
}
为了您的方便,BIND提供了一个抽象类BNDViewController
,它包含一个IBOutlet
属性dataController
。您可以从代码或XIB中分配此属性(想想依赖注入)。
BIND为视图元素提供了以下抽象子类
BNDView
BNDTableViewCell
BNDCollectionViewCell
BNDViewController
这些子类持有对viewModel
的弱引用以及绑定数组的强引用。考虑一个常见的场景,在呈现UITableView
单元格时。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
id <BNDViewModel> viewModel = self.viewModels[indexPath.row];
UITableViewCell <BNDView> *cell = [tableView dequeueReusableCellWithIdentifier:viewModel.identifier];
...
cell.viewModel = viewModel;
return cell;
}
假设您使用的是BNDTableViewCell
子类,当您将带有相应视图模型的引用分配给它时,将自动迭代相关的绑定数组,并刷新视图模型与视图之间的绑定。上述部分中有更详细的绑定说明。
视图模型在您的业务逻辑和视图之间扮演中间人的角色。从数据控制器接收视图模型后,视图控制器应将其分配给其指定的视图,以创建视图与视图模型之间的绑定。视图模型可能包含内嵌的视图模型,但不能创建它。创建视图模型是DataController
的唯一责任。
您常用的PONSO或其他模型,只要您在将其提供给视图之前将其转换为视图模型,BIND就不用担心您正在处理什么。
假设您正在构建一个基于表格视图的应用程序,并希望在单元格中显示不同人员的姓名。假设PersonViewModel
视图模型有一个名为name
的属性,您希望在单元格的textLabel
上显示该属性。
BNDTableViewCell
公开了一个接口用于分配viewModel
,这应该在每tableView:cellForRowAtIndexPath:
调用时发生,以及绑定,它是一个xib集合导出,用于将绑定更新为后续的viewModel
分配。我们将绑定单元格的textLabel.text
关键路径与视图模型的name
关键路径绑定。 easiest way. easiest way to do this is to use the BINDINGS
macro with BINDViewModel
to create your BNDView
bindings, as shown in the following example.
@interface PersonTableViewCell : BNDTableViewCell
@end
@implementation PersonTableViewCell
BINDINGS(PersonViewModel, //you must provide the viewModel's class over here,
BINDViewModel(name, ~>, textLabel.text), //add as many bindings you like,
nil); //and nil terminate the list when done.
...
@end
在下面的例子中,代码将该绑定分配给绑定属性的数组(BNDTableViewCell)。在设置viewModel:
将自动刷新绑定,调用[binding bindLeft:self.viewModel withRight:self llllllMaking sure that your objects are bound to cell reuse.
如果您想在设置 viewModel
时执行一些附加操作,可以选择重写 viewDidUpdateViewModel:
方法。此方法在单元格上调用 setViewModel:
之后被调用,应该用它来替代重写 setViewModel:
。
@implementation PersonTableViewCell
...
- (void)viewDidUpdateViewModel:(id <BNDViewModel> )viewModel {
}
...
@end
BIND 允许您从 XIB 中创建绑定。最简单的方法是使用 BNDTableViewCell
类或创建其子类。 BNDTableViewCell
提供了一个接口用于分配 viewModel
,这应该在每次调用 tableView:cellForRowAtIndexPath:
时发生,以及 bindings
,这是一个绑定的 XIB 集合出口,它将随着后续的每个 viewModel
分配而更新。
在上面的 gif 中,您可以观察到向 BNDTableViewCell
子类单元格添加绑定的一项简单程序。步骤如下
BNDTableViewCell
子类相同的名称Table View Cell
并将其类更改为您创建的子类。Object
并将其类更改为 BNDBinding
String
,将“键路径”更改为 BIND
,并在“值”中键入 BIND 表达式(在上面的示例中,我将视图模型中 name
keypath 连接到单元格的 textLabel.text
)bindings
的出口集合tableView:cellForRowAtIndexPath:
中,您应该将单元格的 viewModel
属性设置为您的视图模型克隆 BIND
,打开 BIND.xcworkspace
并检查 BINDApp
目标。
此库源于我对 iOS 应用不同架构的探索。它从一些类似项目中获取了一些想法,如