本文由Colin Eberhardt
发表于raywenderlich
,原文可查看MVVM Tutorial with ReactiveCocoa: Part 2/2
在第一部分中,我们介绍了MVVM
,可以看到ReactiveCocoa
如何将ViewModel
绑定到各自对应的View
上。
下图是我们程序实现的Flickr
搜索功能
在这一部分中,我们来看看如何在程序的ViewModel
中驱动视图间的导航操作。
目前我们的程序允许使用简单的搜索字符串来搜索Flickr
。我们可以在这里下载程序。Model
层使用ReactiveCocoa
来提供搜索结果,ViewModel
只是简单地记录响应。
现在,我们来看看如何在结果页中进行导航。
实现ViewModel导航
当一个Flickr
成功返回需要的结果时,程序导航到一个新的视图控制器来显示搜索结果。当前的程序只有一个ViewModel
,即RWTFlickrSearchViewModel
类。为了实现需要的功能,我们将添加一个新的ViewModel
来返回到搜索结果视图。添加新的继承自NSObject
的RWTSearchResultsViewModel
类到ViewModel
分组中,并更新其头文件:
|
|
上述代码添加了描述视图的两个属性,及一个初始化方法。打开RWTSearchResultsViewModel.m
并实现初始化方法:
|
|
回想一下第一部分,ViewModel
在View
驱动程序之前就已经生成了。下一步就是将View
连接到对应的ViewModel
上。
打开RWTSearchResultsViewController.h
,导入ViewModel
,并添加以下初始化方法:
|
|
打开RWTSearchResultsViewController.m
,在类的扩展中添加以下私有属性:
|
|
在同一个文件下面,实现初始化方法:
|
|
在这一步中,我们将重点关注导航如何工作,回到视图控制器中将ViewModel
绑定到UI
中。
现在程序有两个ViewModel
,但是现在将面临一个难题。如何从一个ViewModel
导航到另一个ViewModel
中,也就是在对应的视图控制器中导航。ViewModel
不能直接引用视图,所示我们应该怎么做呢?
答案已经在RWTViewModelServices
协议中给出来了。它获取了一个Model
层的引用,我们将使用这个协议来允许ViewModel
来初始化导航。打开RWTViewModelServices.h
并添加以下方法来协议中:
|
|
理论上讲,是ViewModel
层驱动程序,这一层中的逻辑决定了在View
中显示什么,及何时进行导航。这个方法允许ViewModel
层push
一个ViewModel
,该方式与UINavigationController
方式类似。在更新协议实现前,我们将在ViewModel
层先让这个机制工作。
打开RWTFlickrSearchViewModel.m
并导入以下头文件
|
同时在同一文件中更新executeSearchSignal
的实现:
|
|
上面的代码添加一个addNext
操作到搜索命令执行时创建的信号。doNext
块创建一个新的ViewModel
来显示搜索结果,然后通过ViewModel
服务将它push
进来。现在是时候更新协议的实现代码了。为了满足这个需求,代码需要一个导航控制器的引用。
打开RWTViewModelServicesImpl.h
并添加以下的初始化方法
|
|
打开RWTViewModelServicesImpl.m
并导入以下头文件:
|
然后添加一个私有属性:
|
|
接下来实现初始化方法:
|
|
这简单地更新了初始化方法来存储传入的导航控制器的引用。最后,添加以下方法:
|
|
上面的方法使用提供的ViewModel
的类型来确定需要哪个视图。在上面的例子中,只有一个ViewModel-View
对,不过我确信你可以看到如何扩展这个模式。导航控制器push
了结果视图。
最后,打开RWTAppDelegate.m
,定位到createInitialViewController
方法的RWTViewModelServicesImpl
实例创建的地方,用下面的代码替换创建操作:
|
|
运行后,点击”GO
“可以看到程序切换到新的ViewModel/View
:
现在还是空的。别急,我们一步一步来。不过我们的程序现在有多个ViewModel
,其中导航控制器通过ViewModel
层来进行控制。我们先回来UI
绑定上来。
渲染结果页
搜索结果的视图对应的nib
文件中有一个UITableView
。接下来,我们需要在这个table
中渲染ViewModel
的内容。打开RWTSearchResultsViewController.m
并定位到类扩展。更新它以实现UITableViewDataSource
协议:
|
|
重写viewDidLoad
的代码:
|
|
这段代码执行table view
的初始化并将其绑定到view model
。先忘记硬编码的cell
标识常量,我们会在后面将其移除。
继续在下面添加bindViewModel
代码:
|
|
ViewModel
有两个属性:上述代码处理的的标题,及渲染到table
中的searchResults
数组。那么我们该怎么样将数组绑定到table view
呢?实际上,我们做不了。ReactiveCocoa
可以绑定一些简单的UI控件,但是不能处理这种针对table view
的复杂交互。但不需要担心,还有其它方法。卷起袖子开始做吧。
在同一文件中,添加以下两个数据源方法:
|
|
这个就不用说了吧。运行后,效果如下:
更好的TableView绑定方法
table view
绑定的缺失会很快导致视图控制器代码的增加。而手动绑定看上去又不太优雅。从概念上讲,在ViewModel
的searchResults
数组中的每一项是一个ViewMode
,每个cell
是对应一个ViewModel
实例。在这篇博客中我创建了一个绑定帮助类CETableViewBindingHelper
,允许我们定义用于子ViewModel
的View
,帮助类负责实现数据源协议。我们可以在当前工程的Util
分组中找到这个帮助类。
CETableViewBindingHelper
的构造方法如下:
|
|
为了将数组绑定到视图中,我们简单创建一个帮助类的实例。它的参数是:
- 渲染
ViewModel
数组的table view
- 处理数组变化的信号
- 可选的当某行被选中时的命令
cell
视图的nib
文件
nib
文件定义的cell
必须实现CEReactiveView
协议。工程已经包含了一个table view cell
,我们可以用它来渲染搜索结果。打开RWTSearchResultsTableViewCell.h
并导入协议:
|
采用协议:
|
|
下一步是实现协议。打开RWTSearchResultsTableViewCell.m
并添加头文件
|
添加以下方法:
|
|
RWTSearchResultsViewModel
的searchResults
属性包含RWTFlickrPhoto
实例的数组。它们被直接绑定到View
,而不是在ViewModel
中包装这些Model
对象。
bindViewModel
方法使用了SDWebImage
第三方库,它在后台线程下载并解码图片数据,大大提高了scroll
的性能。
最后一步是使用绑定帮助类来渲染table
。
打开RWTSearchResultsViewController.m
并导入头文件:
|
在该文件下面的代码中移除对UITableDataSource
协议的实现,同时移除实现的方法。接下来,添加以下私有属性:
|
|
在viewDidLoad
方法中移除table view
的配置代码,回归来方法的最初形式:
|
|
然后我们在[self bindViewModel]
后面添加以下代码:
|
|
这从nib
文件中创建了一个UINib
实例并构建了一个绑定帮助类实例,sourceSignal
是通过观察ViewModel
的searchResults
属性改变而创建的。
运行后,得到新的UI
:
一些UI特效
到目前为止,本指南主要关注于根据MVVM模式来构建程序。接下来,我们做点别的吧:添加特效。
iOS7
已经发布一年多了,“运动设计(motion design
)”获取了更多的青睐,很多设计者现在都喜欢用这种微妙的对话和流体行为。
在这一步中,我们将添加一个图片滑动的特效,很不错的。
打开RWTSearchResultsTableViewCell.h
并添加以下方法:
|
|
table view
将使用这个方法来为每个cell
提供视差补偿。
打开RWTSearchResultsTableViewCell.m
并实现这个方法:
|
|
很不错,这只是个简单的变换。
打开RWTSearchResultsViewController.m
并导入以下头文件:
|
然后在类扩展中采用UITableViewDelegate
协议:
|
|
我们只是添加一个绑定辅助类来将将它自己设置为table view
的代理,以便其可以响应行的选择。然而,它也转发代理方法调用到它所有的代理属性,这样我们仍然可以添加自定义行为。
在bindViewModel
方法中,设置绑定辅助类代理:
|
|
在同一文件下面,添加scrollViewDidScroll
的实现:
|
|
table view
每次滚动时,调用这个方法。它迭代所有的可见cell
,计算用于视差效果的偏移值。这个偏移值依赖于cell
在table view
中可见部分的位置。
运行后,可得到以下效果
现在我们回到业务的View
和ViewModel
。
查询评论及收藏计数
我们应该在列表界面中每幅图片的右下方显示评论的数量和收藏的数量。当前我们只在nib
文件中显示一个假数据’123
‘。我们在使用真值来替换这些值前,需要在Model
层添加这些功能。添加表示查询Flickr
API
结果的Model
对象的步骤跟前面一样。
在Model
分组中添加RWTFlickrPhotoMetadata
类,打开RWTFlickrPhotoMetadata.h
并添加以下属性:
|
|
打开RWTFlickrPhotoMetadata.m
并添加description
的实现
|
|
接下来打开RWTFlickrSearch.h
并添加以下方法:
|
|
ViewModel
将使用这个方法来请求给定图片的元数据,如评论和收藏。
接下来打开RWTFlickrSearchImpl.m
并添加以下头文件:
|
接下来实现flickrImageMetadata
方法。不幸的是,这里有些小问题:为了获取图片相关的评论数,我们需要调用flickr.photos.getinfo
方法;为了获取收藏数,需要调用flickr.photos.getFavorites
方法。这让事件变得有点复杂,因为flickrImageMetadata
方法需要调用两个接口请求以获取需要的数据。不过,ReactiveCocoa
已经为我们解决了这个问题。
添加以下实现:
|
|
上面的代码使用signalFromAPIMethod:arguments:transform:
来从底层的基于ObjectiveFLickr
的接口创建信号。上面的代码创建了一个信号对,一个用于获取收藏的数量,一个用于获取评论的数量。
一旦创建了两个信号,combineLatest:reduce:
方法生成一个新的信号来组合两者。
这个方法等待源信号的一个next
事件。reduce
块使用它们的内容来调用,其结果变成联合信号的next
事件。
简单明了吧!
不过在庆祝前,我们回到signalFromAPIMethod:arguments:transform:
方法来修复之前提到的一个错误。你注意到了么?这个方法为每个请求创建一个新的OFFlickrAPIRequest
实例。然后,每个请求的结果是通过代理对象来返回的,而这种情况下,其代理是它自己。结果是,在并发请求的情况下,没有办法指明哪个flickrAPIRequest:didCompleteWithResponse:
调用用来响应哪个请求。不过,ObjectiveFlickr
代理方法签名在第一个参数中包含了相应请求,所以这个问题很好解决。
在signalFromAPIMethod:arguments:transform:
中,使用下面的代码来替换处理successSignal
的管道:
|
|
这只是简单地添加一个filter
操作来移除任何与请求相关的代理方法调用,而不是生成当前的信号。
最后一步是在ViewModel
层中使用信号。
打开RWTSearchResultsViewModel.m
并导入以下头文件:
|
在同一文件中的初始化的末尾添加以下代码:
|
|
这段代码测试了新添加的方法,该方法从返回的结果中的第一幅图片获取图片元数据。运行程序后,会在控制台输出以下信息:
|
|
获取可见cell的元数据
我们可以扩展当前代码来获取所有搜索结果的元数据。然而,如果我们有100
条结果,则需要立即发起200
个请求,每幅图片2
个请求。大多数API
都有些限制,这种调用方式会阻塞我们的请求调用,至少是临时的。
在一个table
中,我们只需要获取当前显示的单元格所对象的结果的元数据。所以,如何实现这个行为呢?当然,我们需要一个ViewModel
来表示这些数据。当前RWTSearchResultsViewModel
暴露了一个绑定到View
的RWTFlickrPhoto
实例的数组,它们的暴露给View
的Model
层对象。为了添加这种可见性,我们将给ViewModel
中的model
对象添加view-centric
状态。
在ViewModel
分组中添加RWTSearchResultsItemViewModel
类,打开头文件并各以下代码更新:
|
|
看看初始化方法,这个ViewModel
封装了一个RWTFlickrPhoto
模型对象的实例。这个ViewModel
包含以下几类属性:
- 表示底层
Model
属性的属性(title, url)
- 当获取到元数据时动态更新的属性
(favorites, comments)
isVisible
,用于表示ViewModel
是否可见
打开RWTSearchResultsItemViewModel.m
并导入以下头文件:
|
接下来添加几个私有属性:
|
|
然后实现初始化方法:
|
|
这基于Model
对象的title
和url
属性,然后通过私有属性来存储服务和图片的引用。
接下来添加initialize
方法。准备好,这里有些有趣的事情会发生。
|
|
这个方法的第一部分通过监听isVisible
属性和过滤true
值来创建一个名为fetchMetadata
的信号。结果,信号在isVisible
属性设置为true
时发出next
事件。第二部分订阅这个信号以初始化到flickrImageMetadata
方法的请求。当这个嵌套的信号发送next
事件时,favorite
和comment
属性使用这个结果来更新值。
总的来说,当isVisible
设置为true
时,发送Flickr API
请求,并在将来某个时刻更新comments
和favorites
属性。
为了使用新的ViewModel
,打开RWTSearchResultsViewModel.m
并导入头文件:
|
在初始化方法中,移除当前设置_searchResults
的代码,并使用以下代码:
|
|
这只是简单地使用一个ViewModel
来包装每一个Model
对象。
最后一步是通过视图来设置isVisible
对象,并使用这些新的属性。
打开RWTSearchResultsTableViewCell.m
并导入以下头文件:
|
然后在下面的bindViewModel
方法的第一行添加以下代码:
|
|
并在访方法中添加以下代码:
|
|
这个代码监听了新的comments
和favorites
属性,当它们更新lable
和image
时会更新。最后,ModelView
的isVisible
属性被设置成YES
。table view
绑定辅助类只绑定可见的单元格,所以只有少部分ViewModel
去请求元数据。
运行后,以看到以下效果:
是不是很酷?
节流
慢着,还有一个问题没有解决。当我们快速地滚动滑动栏,如果不做特殊,会同时加载大量的元数据和图片,这将明显地降低我们程序的性能。为了解决这个问题,程序应该只在照片显示在界面上的的时候去初始化元数据请求。现在ViewModel
的isVisible
属性被设置为YES
,但不会被设置成NO
。我们现在来处理这个问题。
打开RWTSearchResultsTableViewCell.m
,然后修改刚才添加到bindViewModel:
的代码,以设置isVisible
属性:
|
|
当ViewModel
绑定到View
时,isVisible
属性会被设置成YES
。但是当cell
被移出table view
进行重用时会被设置成NO
。我们通过rac_prepareForReuseSignal
信号来实现这步操作。
返回到RWTSearchResultsItemViewModel
中。ViewModel
需要监听isVisible
属性的修改,当属性被设置成YES
后一秒钟,将发送一个元数据的请求。
在RWTSearchResultsItemViewModel.m
中,更新initialize
方法,移除fetchMetadata
信号的创建。使用以下代码来替换:
|
|
你可以想像一下如果没有ReactiveCocoa
,这会有多复杂。
运行程序,现在我们和滑动显示平滑多了。
错误处理
当前搜索Flickr
的代码只处理了OFFlickrAPIRequestDelegate
协议中的flickrAPIRequest:didCompleteWithResponse:
方法。不过,这样网络请求由于多种原因会出错。一个好的应用程序必须处理这些错误,以给用户一个良好的用户体验。代理同时定义了flickrAPIRequest:didFailWithError:
方法,这个方法在请求出错时调用。我们将用这个方法来处理错误并显示一个提示框给用户。
我们之前讲过信号会发出next
,completed
和错误事件。其结果是,我们并不需要做太多的事情。
打开RWTFlickrSearchImpl.m
,并定位到signalFromAPIMethod:arguments:transform:
方法。在这个方法中,在创建successSignal
变量前添加以下代码:
|
|
上面的代码从代理方法中创建了一个信号,订阅了该信号,如果发生错误则发送一个错误。传递给subscribeNext
块的元组包含传递给flickrAPIRequest:didFailWithError:
方法的变量。结果是,tuple.second
获取源错误并使用它来为错误事件服务。这是一个很好的解决方案,你觉得呢?不是所有的API
请求都有内建的错误处理。接下来我们使用它。
RWTFlickrSearchViewModel
不直接暴露信号给视图。相反它暴露一个状态和一个命令。我们需要扩展接口来提供错误报告。
打开RWTFlickrSearchViewModel.h
并添加以下属性:
|
|
打开RWTFlickrSearchViewModel.m
并添加以下代码到initialize
实现的最后:
|
|
executeSearch
属性是一个ReactiveCococa
框架的RACCommand
对象。RACCommand
类有一个errors
属性,用于发送命令执行时产生的任何错误。
为了处理这些错误,打开RWTFlickrSearchViewController.m
并添加以下的代码到initWithViewModel:
方法中:
|
|
运行后,处理错误的效果如下:
想知道为什么获取收藏和评论的请求不报告错误么?这是由设计决定的,主要是这些不会影响程序的可用性。
添加最近搜索列表
用户可能会回去查看一些重复的图片。所以,我们可以做些简化操作。回想一下本文的开头,最后的程序在搜索输入框下面有一个显示最近搜索结果的列表。
现在我们只需要添加上这个功能,这次我要向你发起一个挑战了。我将这一部分的实现留给读者您来处理,来练习练习MVVM
技能吧。
在开始之前,我在这些做些总结:
- 我将创建一个
ViewModel
来表示每个先前的搜索,它包含一些属性,这些属性包括搜索文本,匹配的数量和第一个匹配的图片 - 我将修改
RWTFlickrSearchViewModel
来暴露这些新的ViewModel
对象的数组做为一个属性。 - 使用
CETableViewBindingHelper
可以非常简单地渲染ViewModel
的数组,我已经添加了一个合适的cell(RWTRecentSearchItemTableViewCell
)到工程中。
接下来何去何从?
在这里可以下载最终的程序。这两部分的内容已经包含了很多内容,这里我们可以好好回顾一下主要点:
MVVM
是MVC
模式的一个变种,它正逐渐流行起来MVVM
模式让View
层代码变得更清晰,更易于测试- 严格遵守
View=>ViewModel=>Model
这样一个引用层次,然后通过绑定来将ViewModel
的更新反映到View
层上。 ViewModel
层决不应该维护View
的引用ViewModel
层可以看作是视图的模型(model-of-the-view
),它暴露属性,以直接反映视图的状态,以及执行用户交互相关的命令。Model
层暴露服务。- 针对
MVVM
程序的测试可以在没有UI
的情况下运行。 ReactiveCocoa
框架提供强大的机制来将ViewModel
绑定到View
。它同时也广泛地使用在ViewModel
和Model
层中。
怎么样,下次创建程序的时候,是不是试试MVVM
?试试吧。