[译]基于ReactiveCocoa的MVVM开发模式教程:Part1/2

此文是翻译作品,原文见:http://www.raywenderlich.com/74106/mvvm-tutorial-with-reactivecocoa-part-1

此文是我学习过程中遇到的很好的文章,因为搜不到翻译版本,因此自己翻译了,希望能帮到大家。同时翻译的时候我也好好精进了一下我的markdown语法

你可能在Twitter上听过这样的笑话:

“iOS框架,大量View Controller的产生地” by Colin Campbell。

这在iOS开发者心中是个轻松地“戳”,但是我确信你已经在练习中遇到过这些问题了——臃肿的,难以管理的View Controller。

这个MVVM的开发教程用一个不同的模式来构建一个app,Model-View-View-ViewModel,或者简称MVVM,这个模式因为ReactiveCocoa的诞生更加方便,带来了一个完美的MVC模式的替换模式,和一个轻便的,易于管理的View Controller!

通过这个MVVM教程,你要去建立一个简单的搜索app叫做Flicker search,像下面的图片一样:

[译]基于ReactiveCocoa的MVVM开发模式教程:Part1/2_第1张图片
1.png

注意:这个教程是使用Objective-C开发的,如果你要看我用swift开发的教程,点击这里,在我的博客里面可以看到。

在你开始写代码前,是时候讲一些理论知识了!

一个对ReactiveCocoa的简单介绍

这个教程主要是关于MVVM的,并且假设你对ReactiveCocoa有一定的了解,如果你没有用过ReactiveCocoa,我强烈建议你看我早一些的教程,这个教程会教给你很多。
ReactiveCocoa最核心的东西无疑是 signals,在RACSignal 这个类里面。signals给事件发出一个流,这个流(stream)有三种类型: nextcompletederror

运用这些简单的模式,ReactiveCocoa 可以用来替代代理模式(delegate pattern),观察者模式(KVO)和 target-action pattern,以及更多。

用signal的API编写出来的代码更加均匀,因此更加容易阅读,但是ReactiveCocoa真正的强大的地方在于是你对signals的高级操作,这些操作允许你进行复杂的过滤(filter),转化(transformation)以及用简单的方式协调(coordination)。

在MVVM的环境下,ReactiveCocoa扮演了极其重要的角色,它提供了强大的粘合力在View和ViewModel中间,这些对你还有一点点的超前。

MVVM开发模式的介绍

MVVM——Model-View-ViewModel,在通常的理解中是一个设计的模式,他是MV家族的一个成员,这个家族包括MVC、MVP等等。

每一个MV家族中的模式开始关心如何将UI和业务逻辑分开,因为这样更便利于开发和测试。

注意:如果想要深入了解开发设计模式,我推荐Eli’s和Ash Furrow’s的文章。

了解MVVM的起源有助于你更加了解这个模式。

MVC是第一个用户界面设计模式( UI design pattern),可以追溯到1970年代的Smalltalk language。下面这个图说明了MVC的主要运作模式:

[译]基于ReactiveCocoa的MVVM开发模式教程:Part1/2_第2张图片
MVC.png

这个模式将用户界面分为三种:

  • Model,用来呈现应用状态。
  • View,由视图控制器组成。
  • Controller,处理用户交互并且更新model。

MVC的一个重大问题令人十分困扰,这个概念很好很完美,但是当经常人们开始实现MVC的时候,Model-View-Controller看似圆形的关系,反过来,他们合并成了一个可怕的巨大的麻烦。

不久之前Martin Fowler 向我们介绍了一个由MVC衍生出来的表现模式,并被微软接受并流行开来。

MVVM.png

这个模式的核心是ViewModel,是一种特殊的Model,用来展示应用中UI的状态。

它包含了每一个UI控制器(Controller)的详细状态和属性,例如,一个TextFeild当前的文字,或者一个按钮的可否点击的状态,它也展现了当前视图的一系列动作,例如按钮点击或者手势操作。

将VIewModel理解成为View的Model(model of the view)可以更好地帮你去思考ViewModel。

MVVM遵循以下规则

  • 1.View用来展现VIewModel,但是VIewModel不能展现View。
  • 2.VIewModel用来展现Model,但是也不可反过来。

如果你打破了任何这个规则,你的MVVM就错了!

这种规则的优势如下:

  • 1.更加轻量级的VIew层,所有业务逻辑都被移到ViewModel中。
  • 2.更易于测试,你可以在没有View的情况下启动你的应用,大大提高了可测试性。

注意:测试视图是众所周知的困难,因为测试运行的小的包含的代码块。通常,控制器会在依赖于其他应用程序状态的场景中添加和配置视图。这意味着,意义上的小测试,可以成为一个脆弱而繁琐的命题。

因此,你可能会想提出一个问题,如果只是View可以展现VIewModel,而ViewModel不能反过来展现View的话,那么ViewModel如何更新View呢?啊哈!!这就是MVVM的秘诀了!

MVVM和数据绑定(Data Binding)

MVVM模式依赖于数据绑定,一个框架级的功能,自动连接对象属性的用户界面控件。

有一个例子,在微软的WPF框架,下面一个例子将TextField的文本和ViewModel的Username绑定。

  

WPF的框架将这两个成员变量“绑定”。

这个双向的绑定确保了ViewModel的Username的改变同时TextFeild的文本也改变,反之亦然,用户的输入也将改变ViewModel中的参数值。

另一个例子,基于web的流行的一个MVVM框架Knockout, 你可以发现两个框架中数据绑定的相同的特点。


上面的绑定将HTML的元素和JavaScript的模型绑定。

不幸的是,iOS缺少一个数据绑定框架,但是这就是ReactiveCocoa所充当的“胶水”作用。

具体从iOS开发的角度去看MVVM,ViewController和它相关的UI——不论是xib、storyboard或者是代码组成的视图(View):

[译]基于ReactiveCocoa的MVVM开发模式教程:Part1/2_第3张图片
MVVMReactiveCocoa.png

ReactiveCocoa将两者绑定起来。

注意:对于UI的各种实现方式,我高度推荐Martin Fowler的GUI Architectures article。

你学到了足够的理论知识了吗?如果没有,请回头去再看一遍。当然,如果你学得够好了,那么现在是时候开始创造你自己的ViewModel了。

开始项目准备

首先下载这个开始工程

  • FlickrSearchStarterProject.zip

这个项目使用CocoaPods去管理依赖库(如果你不知道CocoaPods,我们这里有个教程),运行pod install去安装依赖库,确认你看到了一下输出:

  $ 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

你会学到每个依赖库是干吗的。

本教程的开始工程包含了一个View,通过xib实现,打开RWTFlickrSearch.xcworkspace,并且运行,然后你会看到以下页面:

[译]基于ReactiveCocoa的MVVM开发模式教程:Part1/2_第4张图片
first-launch.jpg

花一点时间去熟悉这个项目结构:


[译]基于ReactiveCocoa的MVVM开发模式教程:Part1/2_第5张图片
EmptyInterface.png

Model和VIewModel的groups都是空的,你要为这两个group添加文件,项目已含有的文件是做这些的:

  • RWTFlickSearchViewController:项目的主页面,包含了一个搜索框,和一个“GO”按钮。
  • RWTRecentSearchItemTableViewCell:一个cell显示来自Flicker的第三方图片

是时候开始写你的第一个view model 了!

你的第一个ViewModel

在ViewModel这个group里添加一个新的类,将之命名为RWTFlickrSearchViewModel并且使他继承NSObject。

打开它并在头文件添加下面的声明:

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

searchText提供一个字符串显示在textfield上,成员变量title提供在navigation bar上显示的标题。

注意 :为了更容易的理解项目结构,View和ViewModel用了相同的名字和不同的后缀,例如:RWTFlickrSearch-ViewModel
和 RWTFlickrSearch-ViewController。

打开 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

这段代码初始化了这个ViewModel。

下一步是讲如何将ViewModel和View关联到一起,记住View和ViewModel的关联,因此就需要在View中给对应的ViewModel添加一个相关的实例化方法。

注意:在这个教程管我们的Controller叫做”Views“,这笔“View”在MVVM更多语义。和UIKit使用的默认名不同。

打开 RWTFlickrSearchViewController.h 并声明ViewModel的头文件。
#import "RWTFlickrSearchViewModel.h"
然后加入下面的实例化方法

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

RWTFlickrSearchViewController.m中添加一个私有变量

@property (weak, nonatomic) RWTFlickrSearchViewModel *viewModel;

接下来实现init方法

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

注意:这是个弱引用(弱指针),View引用了ViewModel,但没有拥有它。

在 viewDidLoad的最后加上下面代码

[self bindViewModel]; 

下面是这个方法的实现

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

上面的代码将会在UI初始化和ViewModel状态在VIew上应用的时候运行。
最后一步是实例化ViewModel,并在View中应用。
viewDidLoad中添加以下

#import "RWTFlickrSearchViewModel.h"

加一个私有变量(在文件顶部的类扩展名内)。

@property (strong, nonatomic) RWTFlickrSearchViewModel *viewModel;

你会发现已经有了一个createInitialViewController方法,更新他的实现方法:

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

这将创建一个新的ViewModel实例,然后它返回View。这是应用程序的导航控制器的初始视图。

[译]基于ReactiveCocoa的MVVM开发模式教程:Part1/2_第6张图片

恭喜你,这是你的第一个ViewModel。我得请你控制住你的兴奋!这里还有很多要学习。
你可能已经注意到了你没有使用任何ReactiveCocoa呢。在其目前的形式,任何用户进入搜索文本字段将不会反映在ViewModel。

检测有效搜索状态

在这一部分中,你将使用ReactiveCocoa绑定ViewModel和View的搜索框和按钮在一起,更新RWTFlickrSearchViewController.m中的bindViewModel方法如下

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

我们来加一个ReactiveCocoa 中UITextFeild的分类的方法rac_textSignal,它是一个信号,每次文本字段更新时,将发出包含当前文本的一个事件,RAC宏是一个绑定,上面代码更新了ViewModel中的searchText对象的值,它随着rac_textSignal响应。
总之,上面的代码保证了searchText的值总是UI中的最新的值。如果上面的写法让你感到陌生,你真的应该重新学习一下ReactiveCocoa tutorials这个教程!

如果用户输入的文本是有效的,则只能启用搜索按钮。这里的输入规则是,他们必须输入超过三个字符,然后才能执行搜索。
在 RWTFlickrSearchViewModel.m加入下面代码

#import 

更新方法initialize:

- (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);
   }];
 }

编译,运行并在TextFeild输入一些文字。每次文本在有效或无效状态之间转换时,都会看到日志消息:

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的信号。map操作将文本流转换为真值和假值。最后, distinctuntilchanges是用来确保该信号只在状态变化的时候传递值。

你可能感兴趣的:([译]基于ReactiveCocoa的MVVM开发模式教程:Part1/2)