[译]基于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,像下面的图片一样:

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’sAsh Furrow’s的文章。

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

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

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绑定。

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

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

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

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

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

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

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

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

MVVMReactiveCocoa.png

ReactiveCocoa将两者绑定起来。

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

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

开始项目准备

首先下载这个开始工程

这个项目使用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,并且运行,然后你会看到以下页面:

first-launch.jpg

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


EmptyInterface.png

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

是时候开始写你的第一个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。这是应用程序的导航控制器的初始视图。

恭喜你,这是你的第一个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 <ReactiveCocoa/ReactiveCocoa.h>

更新方法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是用来确保该信号只在状态变化的时候传递值。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258

推荐阅读更多精彩内容