精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

MVVM With ReactiveCocoa

移動開發
本文將采用理論與實踐相結合的方式,重點介紹一個使用 MVVM 和 RAC 開發的 iOS 開源項目MVVMReactiveCocoa ,目的是希望能為你實踐 MVVM 提供幫助。不過,在正式開始介紹正文之前,請你先思考以下三個問題:MVC 與 MVVM 有什么異同點,MVC 到 MVVM 是怎樣演進的;RAC 在 MVVM 中扮演什么樣的角色,MVVM 是否一定要結合 RAC 使用;如何將一個現有的 MVC 應用轉變成一個 MVVM 應用,有哪些需要注意的地方。

[[164687]]

MVVM 是一種軟件架構模式,它是 Martin Fowler 的 Presentation Model 的一種變體,最先由微軟的架構師 John Gossman 在 2005 年提出,并應用在微軟的 WPF 和 Silverlight 軟件開發中。MVVM 衍生于 MVC ,是對 MVC 的一種演進,它促進了 UI 代碼與業務邏輯的分離。

說明:本文將采用理論與實踐相結合的方式,重點介紹一個使用 MVVM 和 RAC 開發的 iOS 開源項目MVVMReactiveCocoa ,目的是希望能為你實踐 MVVM 提供幫助。不過,在正式開始介紹正文之前,請你先思考以下三個問題:

  • MVC 與 MVVM 有什么異同點,MVC 到 MVVM 是怎樣演進的;
  • RAC 在 MVVM 中扮演什么樣的角色,MVVM 是否一定要結合 RAC 使用;
  • 如何將一個現有的 MVC 應用轉變成一個 MVVM 應用,有哪些需要注意的地方。

帶著以上問題,我們一起進入正文。

名詞解釋:本文中的 RAC 為 ReactiveCocoa 的縮寫。

MVC

MVC 是 iOS 開發中使用最普遍的架構模式,同時也是蘋果官方推薦的架構模式。MVC 代表的是 Model–view–controller ,它們之間的關系如下:

是的,MVC 看上去棒極了,model 代表數據,view 代表 UI ,而 controller 則負責協調它們兩者之間的關系。然而,盡管從技術上看 view 和 controller 是相互獨立的,但事實上它們幾乎總是結對出現,一個 view 只能與一個 controller 進行匹配,反之亦然。既然如此,那我們為何不將它們看作一個整體呢:

因此,M-VC 可能是對 iOS 中的 MVC 模式更為準確的解讀。在一個典型的 MVC 應用中,controller 由于承載了過多的邏輯,往往會變得臃腫不堪,所以 MVC 也經常被人調侃成 Massive View Controller :

iOS architecture, where MVC stands for Massive View Controller.

坦白說,有一部分邏輯確實是屬于 controller 的,但是也有一部分邏輯是不應該被放置在 controller 中的。比如,將 model 中的 NSDate 轉換成 view 可以展示的 NSString 等。在 MVVM 中,我們將這些邏輯統稱為展示邏輯。

MVVM

因此,一種可以很好地解決 Massive View Controller 問題的辦法就是將 controller 中的展示邏輯抽取出來,放置到一個專門的地方,而這個地方就是 viewModel 。其實,我們只要在上圖中的 M-VC 之間放入 VM ,就可以得到 MVVM 模式的結構圖:

從上圖中,我們可以非常清楚地看到 MVVM 中四個組件之間的關系。注:除了 view 、viewModel 和 model 之外,MVVM 中還有一個非常重要的隱含組件 binder :

  • view :由 MVC 中的 view 和 controller 組成,負責 UI 的展示,綁定 viewModel 中的屬性,觸發 viewModel 中的命令;
  • viewModel :從 MVC 的 controller 中抽取出來的展示邏輯,負責從 model 中獲取 view 所需的數據,轉換成 view 可以展示的數據,并暴露公開的屬性和命令供 view 進行綁定;
  • model :與 MVC 中的 model 一致,包括數據模型、訪問數據庫的操作和網絡請求等;
  • binder :在 MVVM 中,聲明式的數據和命令綁定是一個隱含的約定,它可以讓開發者非常方便地實現 view 和 viewModel 的同步,避免編寫大量繁雜的樣板化代碼。在微軟的 MVVM 實現中,使用的是一種被稱為 XAML 的標記語言。

ReactiveCocoa

盡管,在 iOS 開發中,系統并沒有提供類似的框架可以讓我們方便地實現 binder 功能,不過,值得慶幸的是,GitHub 開源的 RAC ,給了我們一個非常不錯的選擇。

RAC 是一個 iOS 中的函數式響應式編程框架,它受 Functional Reactive Programming 的啟發,是 Justin Spahr-Summers 和 Josh Abernathy 在開發 GitHub for Mac 過程中的一個副產品,它提供了一系列用來組合和轉換值流的 API 。如需了解更多關于 RAC 的信息,可以閱讀我的上一篇文章《ReactiveCocoa v2.5 源碼解析之架構總覽》。

在 iOS 的 MVVM 實現中,我們可以使用 RAC 來在 view 和 viewModel 之間充當 binder 的角色,優雅地實現兩者之間的同步。此外,我們還可以把 RAC 用在 model 層,使用 Signal 來代表異步的數據獲取操作,比如讀取文件、訪問數據庫和網絡請求等。說明,RAC 的后一個應用場景是與 MVVM 無關的,也就是說,我們同樣可以在 MVC 的 model 層這么用。

小結

綜上所述,我們只要將 MVC 中的 controller 中的展示邏輯抽取出來,放置到 viewModel 中,然后通過一定的技術手段,比如 RAC 來同步 view 和 viewModel ,就完成了 MVC 到 MVVM 的轉變。

Talk is cheap. Show me the code.

下面,我們直接上代碼,一起來看一個 MVC 模式轉換成 MVVM 模式的示例。首先是 model 層的代碼 Person :

  1. @interface Person : NSObject 
  2.   
  3. - (instancetype)initwithSalutation:(NSString *)salutation firstName:(NSString *)firstName lastName:(NSString *)lastName birthdate:(NSDate *)birthdate; 
  4.   
  5. @property (nonatomic, copy, readonly) NSString *salutation; 
  6. @property (nonatomic, copy, readonly) NSString *firstName; 
  7. @property (nonatomic, copy, readonly) NSString *lastName; 
  8. @property (nonatomic, copy, readonly) NSDate *birthdate; 
  9.   
  10. @end 

然后是 view 層的代碼 PersonViewController ,在 viewDidLoad 方法中,我們將 Person 中的屬性進行一定的轉換后,賦值給相應的 view 進行展示:

  1. - (void)viewDidLoad { 
  2.     [super viewDidLoad]; 
  3.   
  4.     if (self.model.salutation.length > 0) { 
  5.         self.nameLabel.text = [NSString stringWithFormat:@"%@ %@ %@", self.model.salutation, self.model.firstName, self.model.lastName]; 
  6.     } else { 
  7.         self.nameLabel.text = [NSString stringWithFormat:@"%@ %@", self.model.firstName, self.model.lastName]; 
  8.     } 
  9.   
  10.     NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; 
  11.     [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"]; 
  12.     self.birthdateLabel.text = [dateFormatter stringFromDate:model.birthdate]; 

接下來,我們引入一個 viewModel ,將 PersonViewController 中的展示邏輯抽取到這個 PersonViewModel 中:

  1. @interface PersonViewModel : NSObject 
  2.   
  3. - (instancetype)initWithPerson:(Person *)person; 
  4.   
  5. @property (nonatomic, strong, readonly) Person *person; 
  6. @property (nonatomic, copy, readonly) NSString *nameText; 
  7. @property (nonatomic, copy, readonly) NSString *birthdateText; 
  8.   
  9. @end 
  10.   
  11. @implementation PersonViewModel 
  12.   
  13. - (instancetype)initWithPerson:(Person *)person { 
  14.     self = [super init]; 
  15.     if (self) { 
  16.        _person = person; 
  17.     
  18.       if (person.salutation.length > 0) { 
  19.           _nameText = [NSString stringWithFormat:@"%@ %@ %@", self.person.salutation, self.person.firstName, self.person.lastName]; 
  20.       } else { 
  21.           _nameText = [NSString stringWithFormat:@"%@ %@", self.person.firstName, self.person.lastName]; 
  22.       } 
  23.     
  24.       NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; 
  25.       [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"]; 
  26.       _birthdateText = [dateFormatter stringFromDate:person.birthdate]; 
  27.     } 
  28.     return self; 
  29.   
  30. @end 

最終,PersonViewController 將會變得非常輕量級:

  1. - (void)viewDidLoad { 
  2.     [super viewDidLoad]; 
  3.       
  4.     self.nameLabel.text = self.viewModel.nameText; 
  5.     self.birthdateLabel.text = self.viewModel.birthdateText; 

怎么樣?其實 MVVM 并沒有想像中的那么難吧,而且更重要的是它也沒有破壞 MVC 的現有結構,只不過是移動了一些代碼,僅此而已。好了,說了這么多,那 MVVM 相比 MVC 到底有哪些好處呢?我想,主要可以歸納為以下三點:

  • 由于展示邏輯被抽取到了 viewModel 中,所以 view 中的代碼將會變得非常輕量級;
  • 由于 viewModel 中的代碼是與 UI 無關的,所以它具有良好的可測試性;
  • 對于一個封裝了大量業務邏輯的 model 來說,改變它可能會比較困難,并且存在一定的風險。在這種場景下,viewModel 可以作為 model 的適配器使用,從而避免對 model 進行較大的改動。

通過前面的示例,我們對第一點已經有了一定的感觸;至于第三點,可能對于一個復雜的大型應用來說,才會比較明顯;下面,我們還是使用前面的示例,來直觀地感受下第二點好處:

  1. SpecBegin(Person) 
  2.     NSString *salutation = @"Dr."
  3.     NSString *firstName = @"first"
  4.     NSString *lastName = @"last"
  5.     NSDate *birthdate = [NSDate dateWithTimeIntervalSince1970:0]; 
  6.   
  7.     it (@"should use the salutation available. ", ^{ 
  8.         Person *person = [[Person alloc] initWithSalutation:salutation firstName:firstName lastName:lastName birthdate:birthdate]; 
  9.         PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person]; 
  10.         expect(viewModel.nameText).to.equal(@"Dr. first last"); 
  11.     }); 
  12.   
  13.     it (@"should not use an unavailable salutation. ", ^{ 
  14.         Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate]; 
  15.         PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person]; 
  16.         expect(viewModel.nameText).to.equal(@"first last"); 
  17.     }); 
  18.   
  19.     it (@"should use the correct date format. ", ^{ 
  20.         Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate]; 
  21.         PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person]; 
  22.         expect(viewModel.birthdateText).to.equal(@"Thursday January 1, 1970"); 
  23.     }); 
  24. SpecEnd 

對于 MVVM 來說,我們可以把 view 看作是 viewModel 的可視化形式,viewModel 提供了 view 所需的數據和命令。因此,viewModel 的可測試性可以幫助我們極大地提高應用的質量。

MVVMReactiveCocoa

接下來,我們進入本文的第二部分,重點介紹一個使用 MVVM 和 RAC 開發的開源項目 MVVMReactiveCocoa 。說明,本文將主要介紹這個應用的架構和設計思路,希望可以為你實踐 MVVM 提供一個真實的參考案例,有些架構并非是 MVVM 所必須的,而是我們為了更順暢地使用 MVVM 而引入的,特別是 ViewModel-Based Navigation 。所以,請你在實踐的過程中能夠結合自身應用的實際情況做出相應的取舍,靈活處理。最后,我們將以登錄界面為例,一起探討下 MVVM 的實踐思路。

說明,以下內容均基于 MVVMReactiveCocoa 的 v2.1.1 標簽進行展開,并且對部分無關代碼做了刪減。

類圖

為了方便我們從宏觀上了解 MVVMReactiveCocoa 的整體結構,我們先來看看它的類圖:

MVVMReactiveCocoa-v2.1.1

從上圖中,我們可以看到,在 MVVMReactiveCocoa 中主要有兩大繼承體系:

  • 用藍色標識出來的 viewModel 的繼承體系,基類為 MRCViewModel ;
  • 用紅色標識出來的 view 的繼承體系,基類為 MRCViewController 。

除了提供與系統基類 UIViewController 相對應的基類 MRCViewModel/MRCViewController 外,還提供了與系統基類 UITableViewController 和 UITabBarController 相對應的基類 MRCTableViewModel/MRCTableViewController 和 MRCTabBarViewModel/MRCTabBarController ,其中基類 MRCTableViewModel/MRCTableViewController 的使用最為普遍。

說明,之所以通過基類的方式來組織 MVVMReactiveCocoa ,一方面是因為主要開發者只有我一個人,這個方案非常容易實施;另一方面是因為通過基類的方式可以盡可能簡單地實現代碼重用,提高開發效率。

服務總線

經過前面的探討,我們已經知道了 MVVM 中的 viewModel 的主要職責就是從 model 層獲取 view 所需的數據,并且將這些數據轉換成 view 能夠展示的形式。因此,為了方便 viewModel 層調用 model 層中的所有服務,并且統一管理這些服務的創建,我使用抽象工廠模式將 model 層的所有服務集中管理了起來,結構圖如下:

從上圖中,我們可以看出,在服務總線類 MRCViewModelServices/MRCViewModelServicesImpl 中,主要包括以下三個方面的內容:

  • 應用自有的服務類,用柚黃色進行了標識,包括 MRCAppStoreService/MRCAppStoreServiceImpl 和 MRCRepositoryService/MRCRepositoryServiceImpl 兩個服務類;
  • 第三方 GitHub 提供的 API 框架,用天藍色進行了標識,主要包括 OCTClient 服務類;
  • 應用的導航服務,用藻綠色進行了標識,包括 MRCNavigationProtocol 協議和實現類 MRCViewModelServicesImpl 等。

其中,前兩者都是以信號的形式對 viewModel 層提供服務,代表異步的網絡請求等數據獲取操作,而我們在 viewModel 層則可以通過訂閱信號的形式獲取到所需的數據。此外,服務總線還實現了 MRCNavigationProtocol 協議,它的內容如下:

  1. @protocol MRCNavigationProtocol [NSObject](因識別問題,這里用方括號代替尖括號) 
  2.   
  3. - (void)pushViewModel:(MRCViewModel *)viewModel animated:(BOOL)animated; 
  4.   
  5. - (void)popViewModelAnimated:(BOOL)animated; 
  6.   
  7. - (void)popToRootViewModelAnimated:(BOOL)animated; 
  8.   
  9. - (void)presentViewModel:(MRCViewModel *)viewModel animated:(BOOL)animated completion:(VoidBlock)completion; 
  10.   
  11. - (void)dismissViewModelAnimated:(BOOL)animated completion:(VoidBlock)completion; 
  12.   
  13. - (void)resetRootViewModel:(MRCViewModel *)viewModel; 
  14.   
  15. @end 

看上去是不是有點眼熟?是的,MRCNavigationProtocol 協議其實就是參照系統的導航操作定義出來的,用來實現 ViewModel-Based 的導航服務。注意,服務總線類 MRCViewModelServicesImpl 其實并沒有真正實現 MRCNavigationProtocol 協議中聲明的操作,只不過是實現了一些空操作而已:

  1. - (void)pushViewModel:(MRCViewModel *)viewModel animated:(BOOL)animated {} 
  2.   
  3. - (void)popViewModelAnimated:(BOOL)animated {} 
  4.   
  5. - (void)popToRootViewModelAnimated:(BOOL)animated {} 
  6.   
  7. - (void)presentViewModel:(MRCViewModel *)viewModel animated:(BOOL)animated completion:(VoidBlock)completion {} 
  8.   
  9. - (void)dismissViewModelAnimated:(BOOL)animated completion:(VoidBlock)completion {} 
  10.   
  11. - (void)resetRootViewModel:(MRCViewModel *)viewModel {} 

那么,我們是怎么實現 ViewModel-Based 的導航操作的呢?用 MRCViewModelServicesImpl 來實現這些空操作到底有什么用意?為什么要這么做,目的是為了什么?兄臺,莫急,請接著看下一小節的內容。

ViewModel-Based Navigation

我們先來思考一個問題,就是我們為什么要實現 ViewModel-Based 的導航操作呢?直接在 view 層使用系統的 push/present 等操作來完成導航不就好了么?我總結了一下這么做的理由,主要有以下三點:

  • 從理論上來說,MVVM 模式的應用應該是以 viewModel 為驅動來運轉的;

  • 根據我們前面對 MVVM 的探討,viewModel 提供了 view 所需的數據和命令。因此,我們往往可以直接在命令執行成功后使用 doNext 順帶就把導航操作給做了,一氣呵成;

  • 這樣可以使 view 更加輕量級,只需要綁定 viewModel 提供的數據和命令即可。

既然如此,那我們究竟要如何實現 ViewModel-Based 的導航操作呢?我們都知道 iOS 中的導航操作無外乎兩種,push/pop 和 present/dismiss ,前者是 UINavigationController 特有的功能,而后者是所有 UIViewController 都具備的功能。注意,UINavigationController 也是 UIViewController 的子類,所以它也同樣具備 present/dismiss 的功能。因此,從本質上來說,不管我們要實現什么樣的導航操作,最終都是離不開 push/pop 和 present/dismiss 的。

目前,MVVMReactiveCocoa 的做法是在 view 層維護一個 NavigationController 的堆棧 MRCNavigationControllerStack ,不管是 push/pop 還是 present/dismiss ,都使用棧頂的 NavigationController 來執行導航操作,并且保證 present 出來的是一個 NavigationController 。

接下來,我們一起來看看 MVVMReactiveCocoa 在執行了 push/pop 或 present/dismiss 操作后視圖層次結構的變化過程。首先,我們來看看用戶在登錄成功后進入到首頁時應用的視圖層次結構圖:

此時,應用展示的界面是 NewsViewController 。在 MRCNavigationControllerStack 堆棧中只有 NavigationController0 一個元素;而 NavigationController1 并沒有在 MRCNavigationControllerStack 堆棧中,這是因為需要支持 TabBarController 的滑動切換而設計的視圖層次結構,是首頁比較特殊的一個地方。更多信息可以查看 GitHub 開源庫 WXTabBarController ,在這里,我們不用太過于關心這個問題,只需要理解原理就好了。

接下來,當用戶在 NewsViewController 界面,點擊了某一個 cell ,通過 push 的方式,進入到倉庫詳情界面時,應用的視圖層次結構圖如下:

應用通過 MRCNavigationControllerStack 棧頂的元素 NavigationController0 ,將倉庫詳情界面 push 到了自身的堆棧中。此時,應用展示的界面是被 push 進來的倉庫詳情界面 RepoDetailViewController 。最后,當用戶在倉庫詳情界面,點擊左下角的切換分支按鈕,通過 present 的方式,彈出分支選擇界面時,應用的視圖層次結構圖如下:

應用通過 MRCNavigationControllerStack 棧頂的元素 NavigationController0 ,將 NavigationController5 以 present 的方式彈出來。此時,應用展示的是 NavigationController5 的根視圖 SelectBranchOrTagViewController 。說明,由于 pop 和 dismiss 與 push 和 present 互為逆操作,所以只要按照從下到上的順序看上面的視圖層次結構圖即可,這里不再贅述。

等等,如果我沒有記錯的話,MRCNavigationControllerStack 堆棧是在 view 層,而服務總線類 MRCViewModelServicesImpl 是在 viewModel 層的。據我所知,viewModel 層是不能引入 view 層的任何東西的,更嚴格的說,是不能引入任何 UIKit 中的東西的,否則就違背了 MVVM 的基本原則,并且也會散失 viewModel 的可測試性。在這個前提下,你要如何讓這兩者產生關聯呢?

沒錯,這就是 MRCViewModelServicesImpl 中之所以實現那些空操作的目的所在了。viewModel 通過調用 MRCViewModelServicesImpl 中的空操作來表明需要執行相應的導航操作,而 MRCNavigationControllerStack 則通過 Hook 來捕獲這些空操作,然后使用棧頂的 NavigationController 來執行真正的導航操作:

  1. - (void)registerNavigationHooks { 
  2.     @weakify(self) 
  3.     [[(NSObject *)self.services 
  4.         rac_signalForSelector:@selector(pushViewModel:animated:)] 
  5.         subscribeNext:^(RACTuple *tuple) { 
  6.             @strongify(self) 
  7.             UIViewController *viewController = (UIViewController *)[MRCRouter.sharedInstance viewControllerForViewModel:tuple.first]; 
  8.             [self.navigationControllers.lastObject pushViewController:viewController animated:[tuple.second boolValue]]; 
  9.         }]; 
  10.   
  11.     [[(NSObject *)self.services 
  12.         rac_signalForSelector:@selector(popViewModelAnimated:)] 
  13.         subscribeNext:^(RACTuple *tuple) { 
  14.           @strongify(self) 
  15.             [self.navigationControllers.lastObject popViewControllerAnimated:[tuple.first boolValue]]; 
  16.         }]; 
  17.   
  18.     [[(NSObject *)self.services 
  19.         rac_signalForSelector:@selector(popToRootViewModelAnimated:)] 
  20.         subscribeNext:^(RACTuple *tuple) { 
  21.             @strongify(self) 
  22.             [self.navigationControllers.lastObject popToRootViewControllerAnimated:[tuple.first boolValue]]; 
  23.         }]; 
  24.   
  25.     [[(NSObject *)self.services 
  26.         rac_signalForSelector:@selector(presentViewModel:animated:completion:)] 
  27.         subscribeNext:^(RACTuple *tuple) { 
  28.           @strongify(self) 
  29.             UIViewController *viewController = (UIViewController *)[MRCRouter.sharedInstance viewControllerForViewModel:tuple.first]; 
  30.   
  31.             UINavigationController *presentingViewController = self.navigationControllers.lastObject; 
  32.             if (![viewController isKindOfClass:UINavigationController.class]) { 
  33.                 viewController = [[MRCNavigationController alloc] initWithRootViewController:viewController]; 
  34.             } 
  35.             [self pushNavigationController:(UINavigationController *)viewController]; 
  36.             [presentingViewController presentViewController:viewController animated:[tuple.second boolValue] completion:tuple.third]; 
  37.         }]; 
  38.   
  39.     [[(NSObject *)self.services 
  40.         rac_signalForSelector:@selector(dismissViewModelAnimated:completion:)] 
  41.         subscribeNext:^(RACTuple *tuple) { 
  42.             @strongify(self) 
  43.             [self popNavigationController]; 
  44.             [self.navigationControllers.lastObject dismissViewControllerAnimated:[tuple.first boolValue] completion:tuple.second]; 
  45.         }]; 
  46.   
  47.     [[(NSObject *)self.services 
  48.         rac_signalForSelector:@selector(resetRootViewModel:)] 
  49.         subscribeNext:^(RACTuple *tuple) { 
  50.             @strongify(self) 
  51.             [self.navigationControllers removeAllObjects]; 
  52.   
  53.             UIViewController *viewController = (UIViewController *)[MRCRouter.sharedInstance viewControllerForViewModel:tuple.first]; 
  54.             if (![viewController isKindOfClass:[UINavigationController class]]) { 
  55.                 viewController = [[MRCNavigationController alloc] initWithRootViewController:viewController]; 
  56.                 ((UINavigationController *)viewController).delegate = self; 
  57.                 [self pushNavigationController:(UINavigationController *)viewController]; 
  58.             } 
  59.   
  60.             MRCSharedAppDelegate.window.rootViewController = viewController; 
  61.         }]; 

通過 Hook 的方式,我們最終實現了 ViewModel-Based 的導航操作,并且在 viewModel 層中也沒有引入 view 層的任意東西,實現了解耦合。

Router

還有一點值得一提的是,我們在 viewModel 中調用導航操作的時候,只傳入了 viewModel 的實例作為參數,那么我們在 MRCNavigationControllerStack 中執行真正的導航操作時,怎么才能知道要跳轉到哪個界面呢?為此,我們配置了一個從 viewModel 到 view 的映射,并且約定了一個統一的初始化 view 的方法 initWithViewModel: :

  1. - (MRCViewController *)viewControllerForViewModel:(MRCViewModel *)viewModel { 
  2.     NSString *viewController = self.viewModelViewMappings[NSStringFromClass(viewModel.class)]; 
  3.   
  4.     NSParameterAssert([NSClassFromString(viewController) isSubclassOfClass:[MRCViewController class]]); 
  5.     NSParameterAssert([NSClassFromString(viewController) instancesRespondToSelector:@selector(initWithViewModel:)]); 
  6.   
  7.     return [[NSClassFromString(viewController) alloc] initWithViewModel:viewModel]; 
  8.   
  9. - (NSDictionary *)viewModelViewMappings { 
  10.     return @{ 
  11.       @"MRCLoginViewModel": @"MRCLoginViewController"
  12.         @"MRCHomepageViewModel": @"MRCHomepageViewController"
  13.         @"MRCRepoDetailViewModel": @"MRCRepoDetailViewController"
  14.         ... 
  15.     }; 

登錄界面

最后,我們一起來看看登錄界面中 viewModel 和 view 的部分關鍵代碼,探討一下 MVVM 的具體實踐過程。說明,我們將會盡可能地回避具體的業務邏輯,重點關注 MVVM 的實踐思路。下面是登錄界面的截圖:

其中,主要的界面元素有:

  • 一個用于展示用戶頭像的按鈕 avatarButton ;
  • 用于輸入賬號和密碼的輸入框 usernameTextField 和 passwordTextField ;
  • 一個直接登錄的按鈕 loginButton 和一個跳轉到瀏覽器授權登錄的按鈕 browserLoginButton 。

分析:根據我們前面對 MVVM 的探討,viewModel 需要提供 view 所需的數據和命令。因此,MRCLoginViewModel.h 頭文件的內容大致如下:

  1. @interface MRCLoginViewModel : MRCViewModel 
  2.   
  3. @property (nonatomic, copy, readonly) NSURL *avatarURL; 
  4. @property (nonatomic, copy) NSString *username; 
  5. @property (nonatomic, copy) NSString *password; 
  6.   
  7. @property (nonatomic, strong, readonly) RACSignal *validLoginSignal; 
  8. @property (nonatomic, strong, readonly) RACCommand *loginCommand; 
  9. @property (nonatomic, strong, readonly) RACCommand *browserLoginCommand; 
  10.   
  11. @end 

非常直觀,其中需要特別說明的是 validLoginSignal 屬性代表的是登錄按鈕是否可用,它將會與 view 中登錄按鈕的 enabled 屬性進行綁定。接著,我們來看看 MRCLoginViewModel.m 的實現文件中的部分關鍵代碼:

  1. @implementation MRCLoginViewController 
  2.   
  3. - (void)bindViewModel { 
  4.     [super bindViewModel]; 
  5.   
  6.     @weakify(self) 
  7.     [RACObserve(self.viewModel, avatarURL) subscribeNext:^(NSURL *avatarURL) { 
  8.       @strongify(self) 
  9.         [self.avatarButton sd_setImageWithURL:avatarURL forState:UIControlStateNormal placeholderImage:[UIImage imageNamed:@"default-avatar"]]; 
  10.     }]; 
  11.   
  12.     RAC(self.viewModel, username)  = self.usernameTextField.rac_textSignal; 
  13.     RAC(self.viewModel, password)  = self.passwordTextField.rac_textSignal; 
  14.     RAC(self.loginButton, enabled) = self.viewModel.validLoginSignal; 
  15.   
  16.     [[self.loginButton 
  17.         rac_signalForControlEvents:UIControlEventTouchUpInside] 
  18.         subscribeNext:^(id x) { 
  19.             @strongify(self) 
  20.             [self.viewModel.loginCommand execute:nil]; 
  21.         }]; 
  22.   
  23.     [[self.browserLoginButton 
  24.         rac_signalForControlEvents:UIControlEventTouchUpInside] 
  25.         subscribeNext:^(id x) { 
  26.             @strongify(self) 
  27.             NSString *message = [NSString stringWithFormat:@"“%@” wants to open “Safari”", MRC_APP_NAME]; 
  28.   
  29.             UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil 
  30.                                                                                      message:message 
  31.                                                                               preferredStyle:UIAlertControllerStyleAlert]; 
  32.   
  33.             [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:NULL]]; 
  34.             [alertController addAction:[UIAlertAction actionWithTitle:@"Open" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 
  35.                 @strongify(self) 
  36.                 [self.viewModel.browserLoginCommand execute:nil]; 
  37.             }]]; 
  38.   
  39.             [self presentViewController:alertController animated:YES completion:NULL]; 
  40.         }]; 
  41.   
  42. @end 
  • 當用戶輸入的用戶名發生變化時,調用 model 層的方法查詢本地數據庫中緩存的用戶數據,并返回 avatarURL 屬性;
  • 當用戶輸入的用戶名或密碼發生變化時,判斷用戶名和密碼的長度是否均大于 0 ,如果是則登錄按鈕可用,否則不可用;
  • 當 loginCommand 或 browserLoginCommand 命令執行成功時,調用 doNext 代碼塊,使用服務總線中的方法 resetRootViewModel: 進入首頁。

接下來,我們來看看 MRCLoginViewController 中的部分關鍵代碼:

  1. @implementation MRCLoginViewController 
  2. 9 
  3. - (void)bindViewModel { 
  4.     [super bindViewModel]; 
  5. 9 
  6.     @weakify(self) 
  7.     [RACObserve(self.viewModel, avatarURL) subscribeNext:^(NSURL *avatarURL) { 
  8.       @strongify(self) 
  9.         [self.avatarButton sd_setImageWithURL:avatarURL forState:UIControlStateNormal placeholderImage:[UIImage imageNamed:@"default-avatar"]]; 
  10.     }]; 
  11. 9 
  12.     RAC(self.viewModel, username)  = self.usernameTextField.rac_textSignal; 
  13.     RAC(self.viewModel, password)  = self.passwordTextField.rac_textSignal; 
  14.     RAC(self.loginButton, enabled) = self.viewModel.validLoginSignal; 
  15. 9 
  16.     [[self.loginButton 
  17.         rac_signalForControlEvents:UIControlEventTouchUpInside] 
  18.         subscribeNext:^(id x) { 
  19.             @strongify(self) 
  20.             [self.viewModel.loginCommand execute:nil]; 
  21.         }]; 
  22. 9 
  23.     [[self.browserLoginButton 
  24.         rac_signalForControlEvents:UIControlEventTouchUpInside] 
  25.         subscribeNext:^(id x) { 
  26.             @strongify(self) 
  27.             NSString *message = [NSString stringWithFormat:@"“%@” wants to open “Safari”", MRC_APP_NAME]; 
  28. 9 
  29.             UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil 
  30.                                                                                      message:message 
  31. 9                                                                              preferredStyle:UIAlertControllerStyleAlert]; 
  32. 9 
  33.             [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:NULL]]; 
  34.             [alertController addAction:[UIAlertAction actionWithTitle:@"Open" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 
  35.                 @strongify(self) 
  36.                 [self.viewModel.browserLoginCommand execute:nil]; 
  37.             }]]; 
  38.             [self presentViewController:alertController animated:YES completion:NULL]; 
  39.         }]; 
  40. 9 

@end

  • 觀察 viewModel 中 avatarURL 屬性的變化,然后設置 avatarButton 中的圖片;
  • 將 viewModel 中的 username 和 password 屬性分別與 usernameTextField 和 passwordTextField 輸入框中的內容進行綁定;
  • 將 loginButton 的 enabled 屬性與 viewModel 的 validLoginSignal 屬性進行綁定;
  • 在 loginButton 和 browserLoginButton 按鈕被點擊時分別執行 loginCommand 和 browserLoginCommand 命令。

綜上所述,我們將 MRCLoginViewController 中的展示邏輯抽取到 MRCLoginViewModel 中后,使得 MRCLoginViewController 中的代碼更加簡潔和清晰。實踐 MVVM 的關鍵點在于,我們要能夠分析清楚 viewModel 需要暴露給 view 的數據和命令,這些數據和命令能夠代表 view 當前的狀態。

總結

首先,我們從理論出發介紹了 MVC 和 MVVM 各自的概念以及從 MVC 到 MVVM 的演進過程;接著,介紹了 RAC 在 MVVM 中的兩個使用場景;最后,我們從實踐的角度,重點介紹了一個使用 MVVM 和 RAC 開發的開源項目 MVVMReactiveCocoa 。總的來說,我認為 iOS 中的 MVVM 可以分為以下三種不同的實踐程度,它們分別對應不同的適用場景:

  • MVVM + KVO ,適用于現有的 MVC 項目,想轉換成 MVVM 但是不打算引入 RAC 作為 binder 的團隊;
  • MVVM + RAC ,適用于現有的 MVC 項目,想轉換成 MVVM 并且打算引入 RAC 作為 binder 的團隊;
  • MVVM + RAC + ViewModel-Based Navigation ,適用于全新的項目,想實踐 MVVM 并且打算引入 RAC 作為 binder ,然后也想實踐 ViewModel-Based Navigation 的團隊。

寫在最后,希望這篇文章能夠打消你對 MVVM 模式的顧慮,趕快行動起來吧。

參考鏈接

責任編輯:倪明 來源: 雷純鋒的博客
相關推薦

2015-10-20 15:57:48

ReactiveCociOS

2015-07-02 09:56:48

ReactiveCociOS

2016-08-22 08:36:14

ReactiveCoc內存泄漏GitHub

2016-07-29 10:21:06

IOSAPIReactiveCoc

2015-08-17 09:44:30

reactivecocios框架實用

2024-04-28 10:22:08

.NETMVVM應用工具包

2021-01-21 05:50:28

MVVM模式Wpf

2017-03-31 20:45:41

MVCMVPMVVM

2017-04-01 08:30:00

MVCMVPMVVM

2009-12-24 14:30:19

WPF MVVM

2012-04-05 11:35:07

.NET

2015-05-05 10:32:15

iOS-MVVM框架

2017-02-24 10:02:04

AndroidMVVM應用框架

2017-07-17 15:19:10

MVVM模式iOS開發MVP

2013-07-31 13:13:50

Windows PhoMVVM模式

2017-07-20 11:18:22

Vue.jsMVVMMVC

2017-03-02 11:10:39

AndroidMVVM應用程序

2017-02-21 13:24:41

iOSMVVM架構

2016-05-18 10:20:15

GitHubswiftReactiveCoc

2018-03-21 16:19:40

MVCMVPMVVM
點贊
收藏

51CTO技術棧公眾號

国产性生活毛片| 国产乱子伦精品视频| 国产精品成人无码| 久久久久久久久99精品大| 欧美一二三区在线观看| 波多野结衣综合网| av在线免费观看网| 国产精品亚洲第一 | av福利在线播放| 国产真实精品久久二三区| 久久免费视频网站| 又色又爽的视频| 久久中文资源| 欧美一区永久视频免费观看| 自慰无码一区二区三区| 黄色网页在线看| 久久先锋影音av鲁色资源网| 亚洲综合在线做性| 天堂网免费视频| 国产伊人精品| 色阁综合伊人av| 人妻大战黑人白浆狂泄| 日韩区欧美区| 欧美欧美欧美欧美| 任你操这里只有精品| 国产精品一区hongkong| 亚洲免费三区一区二区| 日本一区美女| 天堂a√在线| 成人一区二区视频| 91网站在线免费观看| 乱码一区二区三区| 中文字幕在线天堂| 欧美精品日韩| 久久久999成人| 四季av中文字幕| 综合亚洲色图| 日韩av综合网站| 娇妻高潮浓精白浆xxⅹ| 久久久精品区| 精品视频资源站| 国产97色在线 | 日韩| 午夜不卡影院| 欧美日韩免费区域视频在线观看| 国产一级不卡视频| 午夜av在线免费观看| 国产精品久久久久毛片软件| 日本一区视频在线| 国产福利免费在线观看| 国产日韩av一区二区| 精品伊人久久大线蕉色首页| 视频二区在线观看| www.欧美日韩国产在线| 久久av一区二区| 无码精品人妻一区二区| 91丨九色丨黑人外教| 久久精品国产一区二区三区不卡| 婷婷丁香一区二区三区| 99精品视频中文字幕| 精品麻豆av| 欧洲毛片在线| 久久久国产一区二区三区四区小说 | 久久免费小视频| 欧美天堂亚洲电影院在线观看| 欧美另类69精品久久久久9999| 裸体武打性艳史| 亚洲小说欧美另类社区| 97热精品视频官网| 久久久免费高清视频| 日韩和欧美的一区| 国产日韩精品综合网站| av资源免费看| av亚洲精华国产精华精华| 女同一区二区| 91短视频版在线观看www免费| 中文字幕一区二| 黄色一级大片免费| 黄视频网站在线观看| 日韩欧美国产成人| 国产又黄又猛的视频| 免费欧美网站| 亚洲美女av黄| 黑人狂躁日本娇小| 在线日韩电影| 国产97人人超碰caoprom| 中文字幕有码无码人妻av蜜桃| 久久 天天综合| 国产精品手机视频| fc2在线中文字幕| 亚洲另类中文字| 日日鲁鲁鲁夜夜爽爽狠狠视频97| 成人看片在线观看| 欧美mv日韩mv国产网站app| 免费看黄色aaaaaa 片| 日韩系列欧美系列| 欧美劲爆第一页| 伊人22222| 成人福利视频网站| 亚洲精品一卡二卡三卡四卡| 男人天堂亚洲| 欧美熟乱第一页| 欧美激情一区二区三区p站| 欧美日韩国产一区二区三区不卡| 美女国内精品自产拍在线播放| 日本少妇裸体做爰| 久久丁香综合五月国产三级网站 | 亚洲性视频大全| 日韩中文字幕免费看| 天天插天天操天天干| 国产真实精品久久二三区| 蜜桃麻豆www久久国产精品| 国产日产一区二区| 91久久精品一区二区三区| 91丨porny丨九色| 日韩精品91| 欧美在线精品免播放器视频| 国产欧美第一页| 国产网站一区二区| 国产精品333| 日本精品国产| www.精品av.com| 成人黄色片在线观看| 91视频免费播放| 国产毛片久久久久久国产毛片| 日本黄色成人| 在线观看欧美日韩国产| 四虎成人在线观看| 成人短视频下载| 国产精品videossex国产高清| 成人黄色免费网站| 亚洲欧洲在线看| 好吊操这里只有精品| 成人免费三级在线| av一区二区三区免费观看| 国产精品国产三级在线观看| 北条麻妃久久精品| 中文字幕久久久久| 国产嫩草影院久久久久| 日韩有码免费视频| 国产99亚洲| 日本精品免费一区二区三区| 亚洲人妻一区二区三区| 五月天亚洲婷婷| 免费黄色a级片| 国产一区二区中文| 91在线网站视频| 超碰在线网址| 日韩欧美一级二级| 精品无码久久久久久久| 成人小视频免费在线观看| 黄色一级大片免费| 老牛影视av一区二区在线观看| 欧美激情精品久久久久久大尺度| 丰满少妇一级片| 亚洲国产日韩av| 亚洲av成人无码一二三在线观看| 一本色道精品久久一区二区三区| 精品伦理一区二区三区| 欧美xo影院| 综合激情国产一区| 国产又粗又猛又爽又黄视频 | 亚洲欧美日韩精品| 男人天堂视频网| 欧美极品xxx| 911av视频| 国产精品theporn| 久久av二区| 自拍偷自拍亚洲精品被多人伦好爽| 亚洲人成网站777色婷婷| 一级片在线免费播放| 亚洲人成网站影音先锋播放| 四虎成人免费视频| 久久综合图片| 亚洲资源视频| 成人香蕉社区| 国产99久久精品一区二区 夜夜躁日日躁 | 外国电影一区二区| 久久这里只有精品99| 午夜精品久久久久久久99| 同产精品九九九| 免费一级做a爰片久久毛片潮| 久久成人麻豆午夜电影| 成人免费网站入口| 欧美少妇性xxxx| 999国内精品视频在线| 无遮挡爽大片在线观看视频 | 美女视频黄 久久| 黄色录像特级片| 欧美人与拘性视交免费看| 成人久久一区二区三区| 999福利在线视频| 最新国产精品拍自在线播放| 欧美熟妇交换久久久久久分类| 色爱区综合激月婷婷| 久久久香蕉视频| 国产精品久久久久久久久晋中| 精品人妻伦一二三区久| 久久精品国产99| 久久国产乱子伦免费精品| 欧美日韩国产一区精品一区| 日本不卡免费新一二三区| 亚州一区二区| 国产一区二区视频在线观看| a国产在线视频| 美女av一区二区| 国产在线一二| 亚洲精品97久久| 99热在线只有精品| 欧美日精品一区视频| 日本一级一片免费视频| 亚洲图片你懂的| 亚洲精品一区二区三区影院忠贞| 成人免费视频一区二区| 女人高潮一级片| 久久不射网站| www污在线观看| 女人色偷偷aa久久天堂| 亚洲一区三区| 精品国产一区二区三区久久久樱花 | 亚洲图片小说视频| 黑人巨大精品欧美一区二区| 久久艹精品视频| 综合自拍亚洲综合图不卡区| 69精品无码成人久久久久久| 91蜜桃免费观看视频| 极品人妻一区二区| 蜜臀久久久99精品久久久久久| 少妇高潮毛片色欲ava片| 欧美日韩久久| 9191国产视频| 影音先锋成人在线电影| 亚洲欧洲日本国产| 波多野结衣在线观看一区二区| 欧美日韩在线一二三| 色吊丝一区二区| 久久久久九九九| 老司机在线精品视频| 国产精品初高中精品久久| 亚洲国产欧美在线观看| 1卡2卡3卡精品视频| 日本在线一区二区三区| 亚洲精品日韩激情在线电影| 亚洲免费一区| 96国产粉嫩美女| 欧美精品三级在线| 99re国产| 精品av导航| 久草热久草热线频97精品| 精品深夜福利视频| 久久久久久亚洲精品不卡4k岛国 | 日韩精品在线看| 日本福利片在线| 国产一区二区三区毛片| 超碰国产在线| 久久精品亚洲94久久精品| 黄色网在线看| 久久久久久午夜| 国产粉嫩在线观看| 欧美一级大胆视频| 日本国产欧美| 成人妇女免费播放久久久| 欧美视频三区| 国产原创精品| 精品国产一区二区三区小蝌蚪| 亚洲欧美日韩另类精品一区二区三区 | 国产一区二区三区免费在线观看| 午夜免费视频网站| 9i在线看片成人免费| 蜜桃传媒一区二区亚洲| 国产精品国产三级国产aⅴ入口| 永久久久久久久| 亚洲第一福利视频在线| 日韩精品一区不卡| 制服丝袜亚洲播放| 欧美 日韩 中文字幕| 亚洲片av在线| av黄在线观看| 日本欧美一二三区| 青青伊人久久| 国产亚洲福利社区| 欧美一级精品| 少妇大叫太大太粗太爽了a片小说| 免费欧美在线| 欧美激情第一区| 97久久精品人人做人人爽50路| 亚洲色图第四色| 亚洲国产日韩在线一区模特| 成人午夜精品视频| 日韩欧美成人激情| 国产一级免费在线观看| 欧美成人午夜激情视频| 欧美7777| 高清av免费一区中文字幕| 国产精品美女久久久久久不卡 | 日本亚洲天堂网| 免费观看污网站| 中文字幕免费一区| 成人精品在线看| 5月丁香婷婷综合| 日产精品久久久久久久性色| 久久天堂av综合合色| 欧美性xxx| 国产精品jizz视频| 97视频精品| 日本男人操女人| 成人av在线看| 婷婷久久综合网| 欧美在线观看视频一区二区 | 国产精品欧美久久久久一区二区| 久久久久无码国产精品| 欧美日韩一区精品| 日本一级在线观看| 久久久噜久噜久久综合| 二区三区精品| 日韩精品一区二区三区外面| 一本一本久久| 人妻换人妻a片爽麻豆| 亚洲欧美韩国综合色| 亚洲熟女乱色一区二区三区久久久| 亚洲精品电影网在线观看| 欧美高清另类hdvideosexjaⅴ| 国产美女直播视频一区| 精品国产一区二区三区久久久樱花| 日韩欧美国产免费| 成人免费视频国产在线观看| 久久高清无码视频| 日韩欧美一区二区免费| 日本成人网址| 国产狼人综合免费视频| 国产亚洲一区二区三区不卡| 欧美a在线视频| 99久免费精品视频在线观看| 久久久久久国产精品免费播放| 91精品国产入口在线| 国产最新在线| 国产日韩欧美一二三区| 日韩影院二区| 在线能看的av网站| 中文字幕中文字幕一区| 一区二区www| xxx一区二区| www.成人| 六月婷婷激情综合| 国产大陆精品国产| 久久一级黄色片| 精品国产乱码久久久久久久| 91资源在线观看| 精品欧美一区二区久久久伦| 夜夜夜久久久| 国产又爽又黄无码无遮挡在线观看| 欧美日韩在线一区| 香蕉国产在线视频| 国产精品69av| 日韩视频在线观看| 一本之道在线视频| 亚洲国产一区在线观看| 天天爱天天干天天操| 国产91精品不卡视频| 你懂的一区二区三区| 孩娇小videos精品| 亚洲三级小视频| 三级小视频在线观看| 4k岛国日韩精品**专区| 国产剧情一区| 国产福利精品一区二区三区| 亚洲天堂免费在线观看视频| 精品久久人妻av中文字幕| 久久免费高清视频| 嫩草一区二区三区| 视频免费1区二区三区| 一区二区三区日韩欧美精品| 婷婷久久久久久| 国产精品看片资源| 欧美精品自拍| 精品人妻无码一区二区三区 | 亚洲综合一区二区精品导航| 亚洲人妻一区二区三区| 国产精品丝袜久久久久久不卡| 一个色综合网| 成人免费无码大片a毛片| 91黄色在线观看| 黄色片网站在线观看| 九色91在线视频| 精品一区二区在线视频| 日本网站在线播放| 色婷婷综合成人av| 国产精品18hdxxxⅹ在线| 国产精品无码av无码| 亚洲精品中文字幕在线观看| 性插视频在线观看| 成人欧美在线视频| 久久国产精品亚洲77777| 免费成人深夜夜行网站| 精品一区二区亚洲| 国产精品高清一区二区| 欧在线一二三四区| 亚洲一区中文在线| 色大18成网站www在线观看| 久久久久久久久久码影片| 国产一区二区毛片|