BIND 1.4.7

BIND 1.4.7

测试已测试
Lang语言 Obj-CObjective C
许可协议 MIT
发布最后发布2015年10月

Marko Hlebar 构建。



 
依赖
libextobjc>= 0
NODE_>= 0
 

BIND 1.4.7

iOS 的数据绑定和 MVVM

特性

  1. 使用 BIND DSL 超简单的 UI 绑定
  2. 使用 NSValueTransformerBNDAsyncValueTransformer 子类进行 数据转换
  3. 自动解绑 - 不会有 dealloc 时的 KVO 异常
  4. MVVM 套件内置

UI 绑定

BIND 提供了创建你的 view - viewModelviewModel - model 绑定的简化接口。你可以通过从实现 BNDViewBNDViewModel 协议的类派生来利用这些绑定。以下示例将 viewModel 的某些属性与相应的 view 绑定在一起。

@implementation MHNameTableCell
BINDINGS(MHPersonNameViewModel,
         BINDViewModel(name, ~>, textLabel.text),
         BINDViewModel(ID, ~>, detailTextLabel.text),
         BINDViewModelCommand(reverseNameCommand, onTouchUpInside),
         nil);

...some other code...         
@end

这是如何工作的?它会做些什么?实现 BNDView 协议的视图或视图控制器暴露 viewModelbindings 属性。viewModel 更新时(例如,在单元格重用时),bindings 也会更新,从而保证了正确的 viewModel 被绑定到正确的 view

同样,你也可以将 viewModelmodel 绑定。你可以通过创建一个继承自 BNDViewModelviewModel 子类,并按如下方式定义你的绑定来实现这一点。

@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. 

现在假设你只想观察 viewModelname 值的变化。你可以通过使用 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 告诉绑定使用 NSValueTransformerUppercaseStringTransformer 子类来变换值。如果需要,您可以添加一个 ! 修饰符以改变变换方向,如下所示 BINDRT(viewModel, name, ~>, nameLabel, text, !, UppercaseStringTransformer)

异步转换器

假设您想异步地从网页中获取一个图像,使用从 NSURLUIImage 的变换。您可以通过创建一个 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 架构

此架构提供了责任分配的明显分布以及业务逻辑和表示层的清晰分割,这使得代码更易于测试和维护。以下图表代表一个建议的 MVVMC 应用架构,我们将在以下内容中对此进行更详细的解释。

数据控制器

数据控制器负责将模型从外部来源(例如,Web服务)转换为视图模型,视图模型与视图元素(视图或视图控制器)形成1:1的关系。BIND提供了一个协议BNDDataController,它公开了以下方法:

- (void)updateWithContext:(id)context
        viewModelsHandler:(BNDViewModelsBlock)viewModelsHandler;

理想情况下,每个数据控制器都应该只向拥有的视图控制器公开此方法。调用此方法应触发一系列事件,例如从Web服务获取模型,然后将该模型转换为与您的视图相对应的视图模型。

视图控制器

视图控制器持有数据控制器的引用,并启动对视图模型的请求数。通常,最好在UIViewControllerviewWillAppear:回调方法中刷新视图模型。

- (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就不用担心您正在处理什么。

MVVMC和绑定

假设您正在构建一个基于表格视图的应用程序,并希望在单元格中显示不同人员的姓名。假设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

从 XIB 绑定

BIND 允许您从 XIB 中创建绑定。最简单的方法是使用 BNDTableViewCell 类或创建其子类。 BNDTableViewCell 提供了一个接口用于分配 viewModel,这应该在每次调用 tableView:cellForRowAtIndexPath: 时发生,以及 bindings,这是一个绑定的 XIB 集合出口,它将随着后续的每个 viewModel 分配而更新。

在上面的 gif 中,您可以观察到向 BNDTableViewCell 子类单元格添加绑定的一项简单程序。步骤如下

  • 创建一个空的 XIB 并将其命名为与您的 BNDTableViewCell 子类相同的名称
  • 从对象库中拖入一个 Table View Cell 并将其类更改为您创建的子类。
  • 接下来,从对象库中拖入一个 Object 并将其类更改为 BNDBinding
  • 添加一个 keypath,将其“类型”更改为 String,将“键路径”更改为 BIND,并在“值”中键入 BIND 表达式(在上面的示例中,我将视图模型中 name keypath 连接到单元格的 textLabel.text
  • 右键单击您的表格视图单元格,并找到名为 bindings 的出口集合
  • 将先前创建的绑定与出口集合连接起来。
  • 在您的表格视图代理的 tableView:cellForRowAtIndexPath: 中,您应该将单元格的 viewModel 属性设置为您的视图模型

示例项目

克隆 BIND,打开 BIND.xcworkspace 并检查 BINDApp 目标。

集成

致谢

此库源于我对 iOS 应用不同架构的探索。它从一些类似项目中获取了一些想法,如