MVVM與ReactiveCocoa的運(yùn)用(Part1)

本文翻譯自MVVM Tutorial with ReactiveCocoa

  • MVVM和數(shù)據(jù)綁定

MVVM模式依賴于數(shù)據(jù)綁定,能自動(dòng)將對(duì)象屬性和UI controls相聯(lián)系是其框架級(jí)的特性.
舉個(gè)例子,在微軟的WPF框架里,ViewModel將TextField里的Text屬性和Username屬性綁定,如下所示:

<TextField Text=”{DataBinding Path=Username, Mode=TwoWay}”/>

WPF框架將兩個(gè)屬性綁定在一起.
TwoWay綁定確保ViewModel中的Username屬性改變時(shí)會(huì)為T(mén)extField的Text屬性改變做準(zhǔn)備,而且可逆.例如用戶輸入時(shí)ViewModel的變化.
另一個(gè)例子是著名的基于MVVM的網(wǎng)頁(yè)框架Knockout,你可以在動(dòng)作里看到相似的綁定特性:

<input data-bind=”value: username”/>   

上面將HTML元素的一個(gè)屬性和JavaScript模型綁定.
遺憾的是,iOS缺乏數(shù)據(jù)綁定的框架,但這正是ReactiveCocoa所扮演的角色:進(jìn)行ViewModel連接"粘合"工作.
從iOS開(kāi)發(fā)的角度來(lái)看MVVM模式,ViewController和其相關(guān)的UI(無(wú)論是nib、storyboard或者純代碼組成的View):

MVVMReactiveCocoa.png

....通過(guò)ReactiveCocoa將它們綁定在一起.
理論知識(shí)已經(jīng)補(bǔ)的差不多了吧?如果不熟悉,可以回去復(fù)讀一下.如果覺(jué)得還可以,那就開(kāi)始編寫(xiě)ViewModels吧.

  • 開(kāi)始項(xiàng)目架構(gòu)

首先下載一下初始項(xiàng)目:

項(xiàng)目用CocoaPods來(lái)管理依賴庫(kù)(如果你對(duì)CocoaPods不熟,這有你需要的教程].運(yùn)行命令行pod install來(lái)獲取依賴庫(kù),確保你會(huì)看到以下輸出:

$ pod install
Analyzing dependencies
Downloading dependencies
Installing LinqToObjectiveC (2.0.0)
Installing ReactiveCocoa (2.1.8)
Installing SDWebImage (3.6)
Installing objectiveflickr (2.0.4)
Generating Pods project
Integrating client project

你將會(huì)學(xué)到這些庫(kù)的很多用法.
初始項(xiàng)目已經(jīng)用view controllers和nib文件為你準(zhǔn)備好了應(yīng)用所需的視圖.打開(kāi)CocoaPods所生成的RWTFlickrSearch.xcworkspace,運(yùn)行后,你將看到其中的一個(gè)視圖:

first-launch.jpg

花些時(shí)間來(lái)熟悉下項(xiàng)目的結(jié)構(gòu):

EmptyInterface.png

Model和ViewModel group是空的,待會(huì)你將在里面添加文件.View Group包含以下內(nèi)容:

  • RWTFlickSearchViewController:程序的主界面,包含一個(gè)搜索text field和一個(gè)'Go'按鈕.
  • RWTRecentSearchItemTableViewCell:在主界面展示最近搜索結(jié)果的table cell.
  • RWTSearchResultsViewController:展示搜索結(jié)果Flickr圖片的table.
  • RWTSearchResultsTableViewCell:展示單個(gè)Flickr圖片的table cell.

好了,該開(kāi)始編寫(xiě)你的第一個(gè)view model嚕.

  • 你的首個(gè)ViewModel

在ViewModel組里添加一個(gè)名為RWTFlickrSearchModel的NSObject的子類(lèi).
打開(kāi)此文件的頭文件,添加如下屬性:

@interface RWTFlickrSearchViewModel : NSObject
 
@property (strong, nonatomic) NSString *searchText;
@property (strong, nonatomic) NSString *title;
 
@end

SearchText屬性為text field里面輸入的文字,title屬性為navigation bar上的title.

打開(kāi)RWTFlickrSearchViewModel.m添加如下代碼:

@implementation RWTFlickrSearchViewModel
 
- (instancetype)init {
  self = [super init];
  if (self) {
    [self initialize];
  }
  return self;
}
 
- (void)initialize {
  self.searchText = @"search text";
  self.title = @"Flickr Search";
}
 
@end

以上代碼用來(lái)進(jìn)行ViewModel的初始化.
接下來(lái)將ViewModel和View連接.要記得View擁有對(duì)ViewModel的引用.當(dāng)前給出的是相應(yīng)ViewModel模型的View的初始化.
在RWTFlickrSearchViewController.h里導(dǎo)入ViewModel的頭文件:

#import "RWTFlickrSearchViewModel.h"

接著添加初始化方法:

@interface RWTFlickrSearchViewController : UIViewController
 - (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel;
 @end

在RWTFlickrSearchViewController.m里面添加一個(gè)私有屬性來(lái)控制UI outlets:

@property (weak, nonatomic) RWTFlickrSearchViewModel *viewModel;

繼而添加初始化方法:

- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel {
  self = [super init];
  if (self) {
    _viewModel = viewModel;
  }
  return self;
}

這將存儲(chǔ)一個(gè)View對(duì)ViewModel的引用.
在viewDidLoad的底部增加:

[self bindViewModel];

然后實(shí)現(xiàn)以下方法:

- (void)bindViewModel {
  self.title = self.viewModel.title;
  self.searchTextField.text = self.viewModel.searchText;
}

上面的代碼當(dāng)UI初始化和ViewModel狀態(tài)給View是調(diào)用.
最后是創(chuàng)建個(gè)ViewModel的實(shí)例以供View使用.
RWTAppDelegate.m增加如下導(dǎo)入:

#import "RWTFlickrSearchViewModel.h"

添加一個(gè)私有屬性:

@property (strong, nonatomic) RWTFlickrSearchViewModel *viewModel;

你會(huì)發(fā)現(xiàn)這個(gè)類(lèi)已經(jīng)有個(gè)方法createInitialViewController,更新它的實(shí)現(xiàn)代碼:

- (UIViewController *)createInitialViewController {
  self.viewModel = [RWTFlickrSearchViewModel new];
  return [[RWTFlickrSearchViewController alloc] initWithViewModel:self.viewModel];
}

以上代碼用來(lái)創(chuàng)建一個(gè)ViewModel的新實(shí)例,繼而構(gòu)建和返回View.作用為初始化應(yīng)用的navigation controller.
運(yùn)行程序,你將看到View有一些狀態(tài)!

ViewWithState-333x500.png

恭喜哦,這是你的第一個(gè)ViewModel.請(qǐng)克制你的興奮,還有許多要學(xué);]
也許你覺(jué)察到還木有用到ReactiveCocoa.當(dāng)前,如果你在搜索text field輸入內(nèi)容將不會(huì)傳遞到ViewModel.

  • 檢測(cè)有效的搜索狀態(tài)

本章節(jié),你將使用ReactiveCocoa綁定ViewModel和View以使搜索text field和按鈕能和ViewModel連接.
RWTFlickrSearchViewController.m更新bindViewModel;

- (void)bindViewModel {
  self.title = self.viewModel.title;
  RAC(self.viewModel, searchText) = self.searchTextField.rac_textSignal;
}

通過(guò)ReactiveCocoa的一個(gè)category給UITextField的類(lèi)添加了rac_textSignal屬性.這是個(gè)text field當(dāng)前文本內(nèi)容更新的信號(hào)量.
RAC宏是個(gè)綁定操作,上面的代碼通過(guò)rac_textSignal來(lái)監(jiān)測(cè)輸入的狀態(tài)實(shí)時(shí)更新viewModel中的searchText屬性.
簡(jiǎn)言之,就是確保seachText屬性能實(shí)時(shí)反映出當(dāng)前的UI狀態(tài).如果你覺(jué)得難以理解,那么最好去讀讀之前的兩篇ReactiveCocoa tutorials!
搜索按鈕只有在用戶輸入合法的文本時(shí)才能使用.我們?cè)O(shè)定只有在輸入超過(guò)三個(gè)字符時(shí)才能進(jìn)行搜索.
在RWTFlickrSearchViewModel.m里增加如下導(dǎo)入:

#import <ReactiveCocoa/ReactiveCocoa.h>

更新初始化方法里的內(nèi)容:

- (void)initialize {
  self.title = @"Flickr Search";
 
  RACSignal *validSearchSignal =
    [[RACObserve(self, searchText)
      map:^id(NSString *text) {
         return @(text.length > 3);
      }]
      distinctUntilChanged];
 
  [validSearchSignal subscribeNext:^(id x) {
    NSLog(@"search text is valid %@", x);
  }];
}

運(yùn)行程序,在text field里持續(xù)輸入內(nèi)容.你將從日志中看到text在合法和不合法狀態(tài)間改變:

2014-05-27 18:03:26.299 RWTFlickrSearch[13392:70b] search text is valid 0
2014-05-27 18:03:28.379 RWTFlickrSearch[13392:70b] search text is valid 1
2014-05-27 18:03:29.811 RWTFlickrSearch[13392:70b] search text is valid 0

上面的代碼使用RACObserve宏在ViewModel中的searchText屬性創(chuàng)建一個(gè)信號(hào)量(這就是ReactiveCocoa對(duì)KVO的包裝).一個(gè)map操作將text轉(zhuǎn)換為真假值.
最后distinctUnitlChanges使信號(hào)量只有在值狀態(tài)改變時(shí)發(fā)出.
截止現(xiàn)在你看到的是用ReactiveCocoa將View綁定到ViewModel,使之得到同步.另外,ReactiveCocoa經(jīng)常在ViewModel里來(lái)監(jiān)測(cè)它本身狀態(tài)來(lái)進(jìn)行其它操作.
這種類(lèi)型貫穿本MVVM教程.ReactiveCocoa用來(lái)綁定View和ViewModel,但在應(yīng)用其它layers里也會(huì)有用.

  • 增加一個(gè)搜索指令

本章節(jié),你將使用validSearchSignal做更多事情:創(chuàng)建一個(gè)綁定View的指令.
RWTFlickrSearchViewModel.h增加如下導(dǎo)入:

#import <ReactiveCocoa/ReactiveCocoa.h>

添加屬性:

@property (strong, nonatomic) RACCommand *executeSearch;

RACCommand是ReactiveCocoa中呈現(xiàn)UI動(dòng)作的組件.它包含一個(gè)來(lái)表示UI動(dòng)作結(jié)果、當(dāng)前狀態(tài)、標(biāo)明動(dòng)作是否被執(zhí)行的信號(hào)量.
在RWTFlickrSearchViewModel.m里的initialize方法的尾部增加:

self.executeSearch =
  [[RACCommand alloc] initWithEnabled:validSearchSignal
    signalBlock:^RACSignal *(id input) {
      return  [self executeSearchSignal];
    }];

當(dāng)validSearchSignal為真時(shí)創(chuàng)建的指令為可用.
接著添加如下方法來(lái)提供創(chuàng)建執(zhí)行指令的信號(hào)量:

- (RACSignal *)executeSearchSignal {
  return [[[[RACSignal empty]
           logAll]
           delay:2.0]
           logAll];
}

在這個(gè)方法中將執(zhí)行一些業(yè)務(wù)邏輯作為執(zhí)行命令的結(jié)果,并會(huì)通過(guò)信號(hào)異步地返回結(jié)果.
目前只完成了一個(gè)虛擬的執(zhí)行情況;空信號(hào)立即完成.延遲操作增加了完成事件返回后的兩秒延遲.用來(lái)使代碼看起來(lái)更加真實(shí).
最后一步為將這個(gè)指令添加到View中.打開(kāi)RWTFlickrSearchViewController.m在bindViewModel方法的尾部添加:

self.searchButton.rac_command = self.viewModel.executeSearch;

上面的rac_command屬性為ReactiveCocoa給UIButton添加的擴(kuò)展.用來(lái)確保button點(diǎn)擊后指定的命令執(zhí)行,并且按鈕的可用狀態(tài)反應(yīng)了命令的可用狀態(tài).
運(yùn)行,輸入一些文本,點(diǎn)擊Go:

GoButtonEnabled-333x500.png

你會(huì)看到按鈕只有在輸入文本大于三個(gè)字符時(shí)才可用.而且當(dāng)年你點(diǎn)擊按鈕后兩秒內(nèi)不可用,當(dāng)執(zhí)行完信號(hào)量的時(shí)候才又變得可用了. 在console里,你將看到空信號(hào)量立即完成,兩秒后延遲操作執(zhí)行:

09:31:25.728 RWTFlickrSearch ... name: +empty completed
09:31:27.730 RWTFlickrSearch ... name: [+empty] -delay: 2.0 completed

是不是超酷?

Girl學(xué)iOS100天 第17天

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容