6
//MyTweetCellviewmodel.h
你可能认为这也太像普通”推特”里的数据-模型对象了吧. 为啥要干将其转化成 view-model 的工作?即使类似,view-model 让我们限制信息只暴露给我们需要的地方,提供额外数据转换的属性,或为特定的视图计算数据. (此外,当可以不暴露可变数据-模型对象时也是极好的,因为我们希望 view-model 自己承担起更新它们的任务,而不是靠视图或视图控制器. )
View-Model 从哪来?
那么 view-model 是何时何处被创建的呢?视图控制器创建它们自己的 view-model 么?
View-Model 产生 View-Model
严格来说,你应该为 app delegate 中的顶级视图控制器创建一个 view-model. 当展示一个新的视图控制器时,或很小的视图被 view-model 表现时,你应要求当前的 view-model 为你创建一个子 view-model.
加入我们想要在用户轻拍应用顶部的头像时添加一个资料视图控制器. 我们可以为一级 view-model 添加类似如下方法:
然后在我们的一级视图控制器中这么用它:
9
//MYMainViewController.m
-(IBAction)didTapPrimaryUserAvatar
{
MYTwitterUserProfileViewController*profileViewController=
[[MYTwitterUserProfileViewControlleralloc]initWithviewmodel:userProfileviewmodel];
[self.navigationControllerpushViewController:profileViewControlleranimated:YES];
}
|
在这个例子中我将会展现当前用户的资料视图控制器,但是我的资料视图控制器需要一个 view-model. 我这的主视图控制器不知道(也不该知道)用于创建关联相关用户 view-model 的全部必要数据,所以它请求它自己的 view-model 来干这种创建新 view-model 的苦差事.
View-Model 列表
至于我们的推特 cell,当数据驱动屏幕(在这个例子中或许是通过网络服务调用)聚到一起时,我将会代表性地提前为对应的 cell 创建所有的 view-model. 所以在我们这个方案中,tweets 将会是一个 MYTweetCellviewmodel 对象数组. 在我的表格视图中的 cellForRowAtIndexPath 方法中,我将会在正确的索引上简单地抓取 view-model,并把它赋值给我的 cell 上的 view-model 属性.
Functional Core,Imperative Shell
view-model 这种通往应用设计的方法是一块应用设计之路上的垫脚石,这种被称作“Functional Core,Imperative Shell”的应用设计由Gary Bernhardt创造. (我最近十分有幸去听Andy Matuschak关于这方面的演讲,他为”胖的数值层,瘦的对象层”提出充分理由. 虽然观点相似,但关注于我们怎样移除对象和它们状态的边界影响性质,并用 Swift 中的新数据结构构建更加函数式,可测试的数值层. )
Functional Core
view-model 就是 “functional core”,尽管实际上在 iOS/Objective-C 中达到纯函数水平是很棘手的(Swift 提供了一些附加的函数性,这会让我们更接近). 大意是让我们的 view-model 尽可能少的对剩余的”应用世界”的依赖和影响. 那意味着什么?想起你第一次学编程时可能学到的简单函数吧. 它们可能接受一两个参数并输出一个结果.数据输入,数据输出.这个函数可能是做一些数学运算或是将姓和名结合到一起. 无论应用的其他地方发生啥,这个函数总是对相同的输入产生相同的输出. 这就是函数式方面.
这就是我们为 view-model 谋求的东西. 他们富有逻辑和转换数据并将结果存到属性的功能. 理想上相同的输入(比如网络服务响应)将会导出相同的输出(属性的值). 这意味着尽可能多地消除由”应用世界”剩余部分带来的可能影响输出的因素,比如使用一堆状态. 一个好的第一步就是不要再 view-model 头文件中引入 UIKit.h.(这是个重大原则,但也有些灰色区域. 比如,你可能认为 UIImage 是数据而不是展示信息. PS: 我爱这么干. 既然这样的话就得引入 UIKit. h 以便使用 UIImage 类)UIKit 其性质就是将要影响许多应用世界. 它包含很多”副作用”,凭借改变一个值或调用一个函数将触发很多间接(甚至未知)的改变.
更新:刚刚看了 Andy 在函数式 Swift 会议上给出的另一个超赞的演讲,于是又想到了一些. 要清楚你的 view-model 仍然只是一个对象,而不用维护一些状态(否则它将不会是你视图中非常好用的模型了. )但你仍该努力将尽可能多的逻辑移到无状态的函数”值”中. 再重复一次,Swift在这方面比 Objective-C 更加可行.
Imperative (Declarative?) Shell
命令式外壳 (Imperative Shell) 是我们需要做所有的状态转换,应用世界改变的苦差事的地方,为的是将 view-model 数据转成给用户在屏幕上看到的东西. 这是我们的视图(控制器),实际上我们在这分离 UIKit 的工作. 我仍将特别注意尽可能消除状态并用 ReactiveCocoa 这种陈述性质的东西做这方面工作,而 iOS 和 UIKit 在设计上是命令式的. (表格的 data source 就是个很好的例子,因为它的委托模式强制将状态应用到委托中,为了当请求发生时能够为表格视图提供信息. 实际上委托模式通常强制一大堆状态的使用)
可测试的核心
iOS 的单元测试是个脏,苦,乱的活儿. 至少我去做的时候得出的是这么个结论. 就这方面我还出读过一两本书,但当开始做视图控制器的 mocking 和 swizzling 使其一些逻辑可测试时,我目光呆滞. 我最终把单元测试归入模型和任何同类别模型管理类中. (译者注: mock 是测试常用的手段,而 method swizzling 是基于 Objective-C Runtime 交换方法实现的黑魔法)
这个函数式核心一样的 view-model 的最大优点,除了 bug 数量随着状态数递减之外,就是变得非常能够进行单元测试. 如果你有那种每次输入相同而产生的输出也相同的方法,那就非常适合单元测试的世界. 我们现在将我们的数据用获取/逻辑/转换提取出,避免了视图控制器的复杂性. 那意味着构建棒棒哒测试时不需要用疯狂的 mock 对象,method swizzling,或其他疯癫的变通方法(希望能有).
连接一切
那么当 view-model 的共有属性发生变化时我们如何更新我们的视图呢?
绝大部分时间我们用对应的 view-model 来初始化视图控制器,有点类似我们刚刚在上文见到的:
2
MYTwitterUserProfileViewController*profileViewController=
[[MYTwitterUserProfileViewControlleralloc]initWithviewmodel:userProfileviewmodel];
|
有时你无法在初始化时将 view-model 传入,比如在 storyboard segue 或 cell dequeuing 的情况下. 这时你应该在讨论中的视图(控制器)中暴露一个公有可写的 view-model 属性.
4
MYTwitterUserCell*cell=
[self.tableViewdequeueReusableCellWithIdentifier:@
"MYTwitterUserCell"
forIndexPath:indexPath];
|
有时我们可以在钩子程序调用前传入 view-model,比如 init 和 viewDidLoad,我们可以从view-model 的属性初始化所有 UI 元素的状态.
//dontDoThis1.m
self=[
super
init];
if
(!self)
return
nil;
self;
}
-(void)viewDidLoad{
[
viewDidLoad];
_goButton.enabled=viewmodel.isUsernameValid;
_goButton.alpha=viewmodel.isUsernameValid?1:0.5;
好棒!我们已经配置好了初始值. 当 view-model 上的数据改变时怎么办? 当”go” 按钮在什么时候可用了怎么办?当用户标签和头像在什么时候从网络上下载并填充了怎么办?
我们可以将视图控制器暴露给 view-model,以便于当相关数据变化或类似事件发送时它可以调用一个 “updateUI” 方法. (别这么干. )在 view-model 上将视图控制器作为一个委托?当 view-model 内容有变化时发个通知?(不不不不. )
我们的视图控制器会感知一些变化的发生. 我们可以使用从 UITextfield 得来的委托方法在每当有字符变化时通过检查 view-model 来更新按钮的状态.
8
//dontDoThisEither.m
-(void)textFieldDidChange:(UITextField*)sender{
//updatetheview-model
//checkifthingsarenowvalid
self.goButton.enabled=self.viewmodel.isUsernameValid;
self.goButton.alpha=self.viewmodel.isUsernameValid?1.0:0.5;
这种方法解决的场景是在只有再文本框发生变化时才会影响 view-model 中的 isUsernameValid 值. 假使还有其他变量/动作改变 isUsernameValid 的状态将会怎么样?对于 view-model 中的网络调用会怎么样?或许我们该为 view-model 上的方法加一个完成后回调处理,这样我们此时就可以更新 UI 的一切东西了?使用珍贵而笨重的 KVO 方法怎么样?
我们或许最终使用多种多样我们熟悉的机制将 view-model 和视图控制器所有的接触点都连起来,但你已经知道了标题上不是这么写的. 这样在代码中创建了大量的入口点,仅仅为了简单的更新 UI 就要在代码中完全重新创建应用状态上下文.
进入 ReactiveCocoa
ReactiveCocoa(RAC) 是来拯救我们的,并恰好返回给我们一点理智. 让我们看看如何做到.
思考在一个新的用户页面上控制信息的流动,当表单合法时更新提交按钮的状态. 你现在可能会照下面这么做:
你最后通过使用状态,小心翼翼地代码中许多不同且零碎无关的内容穿到简单的逻辑上. 看看你信息流中所有不同的入口点?(这还只是一个 UI 元素中的一条逻辑线. )我们程序中现在用的抽象概念还不够厉害,不能为我们追踪所有事物的关系,所以我们停止自己去干这蛋疼事儿.
让我们看看陈述版本:
这看起来可能像是为我们应用流程文档中的一张老旧的计算机科学图解. 通过陈述式的编程,我们使用了更高层次的抽象,来让我们实际编程更靠近我们在脑海中设计流程的方式. 我们让电脑为我们做更多工作. 实际的代码更加像这幅图了.
RACSignal
RACSignal (信号)就 RAC 来说是构造单元. 它代表我们最终将要收到的信息. 当你能将未来某时刻收到的消息具体表示出来时,你可以开始预先(陈述性)运用逻辑并构建你的信息流,而不是必须等到事件发生(命令式).
信号会为了控制通过应用的信息流而获得所有这些异步方法(委托,回调 block,通知,KVO,target/action 事件观察,等)并将它们统一到一个接口下.这只是直观理解. 不仅是这些,因为信息会流过你的应用,它还提供给你轻松转换/分解/合并/过滤信息的能力.
那么什么是信号呢?这是一个信号:
信号是一个发送一连串值的物体. 但是我们这儿的信号啥也不干,因为它还没有订阅者. 如果有订阅者监听时(已订阅)信号才会发信息. 它将会向那个订阅者发送0或多个载有数值的”next”事件,后面跟着一个”complete”事件或一个”error”事件. (信号类似于其他语言/工具包中的 “promise”,但更强大,因为它不仅限于向它的订阅者一次只传递一个返回值. )
正如我之前提到的,如果觉得需要的话你可以过滤,转换,分解和合并那些值. 不同的订阅者可能需要使用信号通过不同方式发送的值.
信号发送的值是从哪获得的?
信号是一些等待某事发生的异步代码,然后把结果值发送给它们的订阅者. 你可以用 RACSignal 的类方法 createSignal: 手动创建信号:
9
//networkSignal.m
RACSignal*networkSignal=[RACSignalcreateSignal:^RACDisposable*(idsubscriber){
NetworkOperation*operation=[NetworkOperationgetJSONOperationForURL:@
"http://someurl"
];
[operationsetCompletionBlockWithSuccess:^(NetworkOperation*theOperation,id*result){
[subscribersendNext:result];
[subscribersendCompleted];
}failure:^(NetworkOperation*theOperation,NSError*error){
[subscribersendError:error];
}];
|
我在这用一个具有成功和失败 block (伪造)的网络操作创建了一个信号. (如果我想让信号在被订阅时才让网络请求发生,还可以用 RACSignal 的类方法 defer. )我在成功的 block 里使用提供的 subscriber 对象调用 sendNext: 和 sendCompleted: 方法,或在失败的 block 中调用 sendError:. 现在我可以订阅这个信号并将在响应返回时接收到 json 值或是 error.
幸运的是,RAC 的创造者实际上使用它们自己的库来创建真的事物(捉摸一下),所以对于我们在日常需要什么,他们有很强烈的想法. 他们为我们提供了很多机制,来从我们通常使用的现存的异步模式中拉取信号. 别忘了如果你有一个没有被某个内建信号覆盖到的异步任务,你可以很容易地用 createSignal: 或类似方法来创建信号.
一个被提供的机制就是 RACObserve() 宏. (如果你不喜欢宏,你可以简单地看看罩子下面并用稍微多些冗杂的描述. 这也非常好. 在我们得到Swift 版本的替代之前,这也有在Swift 中使用 RAC 的解决方案. )这个宏是 RAC 中对 KVO 中那些悲惨的 API 的替代. 你只需要传入对象和你想观察的那个对象某属性的 keypath. 给出这些参数后,RACObserve 会创建一个信号,一旦它有了订阅者,它就立刻发送那个属性的当前值,并在发送那个属性在这之后的任何变化.
RACSignal*usernameValidSignal=RACObserve(self.viewmodel,usernameIsValid);
这仅是提供用于创建信号的一个工具. 这里有几个立即可用的方式,来从内置控制流机制中拉取信号:
13
14
15
16
17
18
19
//signals.m
RACSignal*controlUpdate=[myButtonrac_signalForControlEvents:UIControlEventTouchUpInside];
//signalsforUIControleventssendthecontroleventvalue(UITextField,UIButton,UiSlider,etc)
//subscribeNext:^(UIButton*button){NSLog(@"%@",button);//UIButtoninstance}
RACSignal*textChange=[myTextFieldrac_textSignal];
//somespecialmethodsareprovidedforcommonlyneededcontroleventvaluesoffcertaincontrols
//subscribeNext:^(UITextField*textfield){NSLog(@"%@",textfield.text);//"Hello!"}
RACSignal*alertButtonClicked=[myAlertViewrac_buttonClickedSignal];
//signalsforsomedelegatemethodssendthedelegateparamsasthevalue
//e.g.UIAlertView,UIActionSheet,UIImagePickerControl,etc
//(limitedtomethodsthatreturnvoid)
//subscribeNext:^(NSNumber*buttonIndex){NSLog(@"%@",buttonIndex);//"1"}
RACSignal*viewAppeared=[selfrac_signalForSelector:@selector(viewDidAppear:)];
//signalsforarbitraryselectorsthatreturnvoid,sendthemethodparamsasthevalue
//worksforbuiltinoryourownmethods
//subscribeNext:^(NSNumber*animated){NSLog(@"viewDidAppear%@",animated);//"viewDidAppear1"}
|
记住你也能轻松创建自己的信号,包括替代那些没有内建支持的其他委托. 我们现在能够从所有这些不连贯的异步/控制流工具中拉取出信号并将他们合并,试想想这该多酷!这些会成为我们之前看到的陈述性图表中的节点. 真是兴奋.
什么是订阅者?
简言之,订阅者就是一段代码,它等待信号给它发送一些值,然后订阅者就能处理这些值了. (它也可以作用于 “complete” 和 “error” 事件. )
这有一个简单的订阅者,是通过向信号的实例方法 subscribeNext 传入一个 block 来创建的. 我们在这通过 RACObserve() 宏创建信号来观察一个对象上属性的当前值,并把它赋值给一个内部属性.
9
-(void)viewDidLoad{
//...
//createandgetareferencetothesignal
//updatethelocalpropertywhenthisvaluechanges
[usernameValidSignalsubscribeNext:^(NSNumber*isValidNumber){
self.usernameIsValid=isValidNumber.boolValue
}];
注意 :RAC 只处理对象,而不处理像 BOOL 这样的原始值. 不过不用担心,RAC 通常会帮你这些转换.
幸运的是 RAC 的创造者也意识到这种绑定行为的普遍必要性,所以他们提供了另一个宏 RAC(). 与 RACObserve() 相同,你提供想要与即将到来的值绑定的对象和参数,在其内部它所做的是创建一个订阅者并更新其属性的值. 我们的例子现在看起来像这样:
4
RAC(self,usernameIsValid)=RACObserve(self.viewmodel,isUsernameValid);
考虑下我们的目标,这么干有点傻啊. 我们不需要将信号发送的值存到属性中(这会创建状态),我们真正要做的是用从那个值获取到信息来更新 UI.
转换数据流
现在我们进入 RAC 为我们提供的用于转换数值流的方法. 我们将会利用 RACSignal 的实例方法 map.
@H_ 502_978@
10
//transformingStreams.m
-(void)viewDidLoad{
//...
RACSignal*usernameIsValidSignal=RACObserve(self.viewmodel,monospace!important; font-size:1em!important; min-height:auto!important">RAC(self.goButton,enabled)=usernameIsValidSignal;
map:^id(NSNumber*usernameIsValid){
usernameIsValid.boolValue?@1.0:@0.5;
}];
这样现在我们将 view-model 上的 isUsernameValid 发生的变化直接绑定到 goButton 的 enabled 属性上. 酷吧?对 alpha 的绑定更酷,因为我们正在使用 map 方法将值转换成与 alpha 属性相关的值. (注意在这里我们返回的是一个 NSNumber 对象而不是原始float值. 这基本上是唯一的污点: 你需要负责为 RAC 将原始值转化为对象,因为它不能帮你导出来.
多个订阅者,副作用,昂贵的操作
订阅信号链时要明白重要的一件事是每当一个新值通过信号链被发送出去时,实际上会给每个订阅者都发送一次. 直到意识到这就我们而言是有意义的,信号发出的值不存储在任何地方(除了 RAC 在内部实现中). 当信号需要发送一个新的值时,它会遍历所有的订阅者并给每个订阅者发送那个值. (这是对信号链实际工作的简化说明,但基本想法是对的)
这为什么重要?这意味着信号链某处存在的任何副作用,任何影响应用世界的转变,将会发生多次. 这对新接触 RAC 的用户来说是意想不到的. (这也违反了函数式构建的理念-数据输入,数据输出).
一个做作的例子可能是: 信号链某处的信号在每次按钮被按下时更新 self 中的一个计数器属性. 如果信号链有多个订阅者,计数器的增长将会比你想的还要多. 你需要努力从信号链中尽可能剔除副作用. 当副作用不可避免时,你可以使用一些恰当的预防机制. 我将会在另一篇文章中探索.
除副作用之外,你需要注意带有昂贵操作和可变数据的信号链. 网络请求就是一个三者兼得的例子:
-
网络请求影响了应用的网络层(副作用).
-
网络请求为信号链引入了可变数据. (两个完全一样请求可能返回了不同的数据. )
-
网络请求反应慢啊.
例如,你可能有个信号在每次按钮按下时发送一个值,而你想将这个值转换成网络请求的结果. 如果有多个订阅者要这个处理信号链上返回的这个值,你将发起多个网络请求.
网络请求明显是经常需要的. 正如你所期望,RAC 提供这些情况的解决方案,也就是 RACCommand 和多点广播. 我将会在下一篇文章中更深入地分析.
Tweetboat Plus
既然简短的介绍(嗯哼)扯远了,让我们着眼于如何用 ReactiveCocoa 将 view-model 与视图控制器连接起来.//
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//ViewController
-(void)viewDidLoad{
viewDidLoad];
RAC(self.viewmodel,username)=[myTextfieldrac_textSignal];
map:^(NSNumber*valid){
valid.boolValue?@1:@0.5;
}];
RAC(self.avatarImageView,image)=RACObserve(self.viewmodel,userAvatarImage);
RAC(self.userNameLabel,text)=RACObserve(self.viewmodel,userFullName);
@weakify(self);
[[[RACSignalmerge:@[RACObserve(self.viewmodel,tweets),
RACObserve(self.viewmodel,allTweetsLoaded)]]
bufferWithTime:0onScheduler:[RACSchedulermainThreadScheduler]]
subscribeNext:^(idvalue){
@strongify(self);
[self.tableViewreloadData];
}];
[[self.goButtonrac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(idvalue){
@strongify(self);
[self.viewmodelgetTweetsForCurrentUsername];
}];
}
-(UITableViewCell*)tableView:(UITableView*)tableViewcellForRowAtIndexPath:(NSIndexPath*)indexPath{
//iftablesectionisthetweetssection
(indexPath.section==0){
MYTwitterUserCell*cell=
forIndexPath:indexPath];
cell;
}
else
{
//elseifthesectionisourloadingcell
MYLoadingCell*cell=
"MYLoadingCell"
forIndexPath:indexPath];
cell;
}
}
-(void)awakeFromNib{
awakeFromNib];
viewmodel.tweetAuthorAvatarImage);
viewmodel.tweetAuthorFullName);
RAC(self.tweetTextLabel,viewmodel.tweetContent);
让我们过一遍这个例子.
在这我们用 RAC 库中的方法从 UITextField 拉取一个信号. 这行代码将 view-model 上的可读写属性 username 绑定到文本框上的用户输入的任何更新.
map:^(NSNumber*valid){
valid.boolValue?@1:@0.5;
}];
在这我们用 RACObserve 方法在 view-model 的 usernameValid 属性上创建了一个信号 usernameIsValidSignal. 无论何时属性发生变化,它将会沿着管道发送一个新的 @YES 或 @NO. 我们拿到那个值并将其绑定到 goButton 的两个属性上. 首先我们将 alpha 分别对应 YES 或 NO 更新到1或0. 5(记着在这必须返回 NSNumber). 然后我们直接将信号绑定到 enabled 属性,因为 YES 和 NO 在这无需转换就能完美地运作.
2
下面我们为表头的图像视图和用户标签创建绑定,再次在 view-model 上对应的属性上用 RACObserve 宏创建信号.
@weakify(self);
bufferWithTime:0onScheduler:[RACSchedulermainThreadScheduler]]
subscribeNext:^(idvalue){
@strongify(self);
[self.tableViewreloadData];
这货看上去有点诡异,所以我们在这上多花点时间. 我们想在 view-model 上 tweets 数组或 allTweetsLoaded 属性发生变化时更新表格视图. (在这个例子中,我们要用一个简单的方法来重新加载整张表. )所以我们将这两个属性被观察后创建的两个信号合并成一个更大的信号,当两个属性中有一个发生变化,这个信号就会发送值. (你一贯认为信号的值是同类型的,不会像这个信号有一样混杂的值. 这很可能在 Swift 版本的 RAC 中强制要求,但在这我们不关心发出的真实值,我们只是用它来触发表格式图的重新加载. )
那么这儿看起来最吓人的部分可能是信号链中的 bufferWithTime: onScheduler: 方法. 需要它来围绕 UIKit 中的一个问题进行变通. tweets 和 allTweetsLoaded 这两个属性我们都需要追踪,万一 tweets 变化和 allTweetsLoaded 为否(不管怎样我们都得重新加载表格). 有时两个属性都将在同一准确的时间发生变化,意味着合并后的大信号中的两个信号都会发送一个值,那么 reloadData 方法将会在同一个运行循环中被调用两次. UIKit 不喜欢这样. bufferWithTime: 在给明的时间内抓取所有下一个到来的值,当给定的时间过后将所有值合在一起发给订阅者. 通过传入0作为时间,bufferWithTime: 将会抓取那个合并信号在特定的运行循环中发出的全部值,并将他们一起发送出去. (NSTimer 以同样的方式工作,这不是巧合,因为 bufferWithTime: 是用 NSTimer 构建的. )暂时不用担心 scheduler,试把它想做指明这些值必须在主线程上被发送. 现在我们确保 reloadData 每次运行循环只被调用一次.
注意我在这用 @weakify/@strongify 宏切换 strong 和 weak. 这在创建所有这些 block 时非常重要. 在 RAC 的 block 中使用 self 时self 将会被捕获为强引用并得到保留环,除非你尤其意识到要破除保留环
5
[[self.goButtonrac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(idvalue){
@strongify(self);
[self.viewmodelgetTweetsForCurrentUsername];
我将会在下一篇文章中在这里引入 RACCommand,但目前我们只是当按钮被触碰时手动调用 view-model 的 getTweetsForCurrentUsername 方法.
我们已经搞定了 cellForRowAtIndexPath 的第一部分,那么我在这将只说下 loading cell:
4
MYLoadingCell*cell=
forIndexPath:indexPath];
cell;
|
这是另一块我们以后将利用到 RACCommand 的地方,但目前我们只是调用 view-model 的 loadMoreTweets 方法. 我们将只是信任如果 cell 显示或隐藏多次的话 view-model 会避免多次内部调用.
6
-(void)awakeFromNib{
awakeFromNib];
viewmodel.tweetAuthorAvatarImage);
viewmodel.tweetAuthorFullName);
viewmodel.tweetContent);
这段现在应该非常直接了,除此之外我想指出一点. 我们正在将图片和文字绑定到 UI 上对应的属性,但注意 viewmodel 出现在 RACObserve 宏中逗号右边. 这些 cell 终将被重用,新的 view-models 将会被赋值. 如果我们不将 viewmodel 放在逗号右边,那就会监听 viewmodel 属性的变化然后每次都要重新设置绑定;如果放在逗号右边,RACObserve 将会为我们负责这些事儿. 因此我们只需要设定一次绑定并让 Reactive Cocoa 做剩余的部分. 这是在绑定表格 cell 时为了性能需要记住的好东西. 我在实践中即使是有很多表格 cell 依然没有出过问题.
福利-消除更多的状态
有时候你可以在 view-model 中暴露 RACSignal 对象来替代像字符串和图像这样的属性,这能在 view-model 上消除更多的状态. 然后视图控制器就不需要自己用 RACObserve 创建信号了,并只是直接影响这些信号. 要意识到如果你的信号在被 UI 订阅/绑定到 UI 之前发出过一个值,那么你将不会收到那个”初始”的值.
结论
本文篇幅略长,但别被吓着. 这还有好多没讲的,而且是干货儿,是舒展你大脑的好方法. 这毫无疑问是不同的编程风格. 花一会儿功夫停止机械地试图用命令式方案去解决问题. 即使你一开始不是经常用这种编程风格,我认为这有助于理解和提醒我们有截然不同的途径来解决我们程序员的困惑.
下一次我将稍微深入 view-model 内部中本文没提到的内容,并介绍下 RACCommand(希望篇幅能短很多). 然后我们将投入到一个真实案例中,那是我的一个叫做Three Cents的 app 中的一个相当复杂的页面,它混合了网络调用,CoreData,多重 UI 状态,等等!
拓展阅读
Introduction to MVVMby Ash Furrow
Functional Reactive Programming on iOSby Ash Furrow
A sample app by Ash Furrow
MVC,MVVM,FRP,And Building Bridgesby Jonathan Penn
MVVM Tutorial with ReactiveCocoaby Colin Eberhardt on the Ray Wenderlich site.
Basic MVVM with ReactiveCocoaby Colin Wheeler
On MVVM,and Architecture Questionsby Chris Trott 原文链接:https://www.f2er.com/react/304145.html
| | | | | | | | |