美文网首页开发杂谈值得观看今日看点
iOS 开发中的 MVVM 模式——实用进阶篇(整理)

iOS 开发中的 MVVM 模式——实用进阶篇(整理)

作者: 最Fly的Engine人 | 来源:发表于2016-10-11 13:53 被阅读1033次

这篇文章主要介绍了实际应用 MVVM 的过程中的一些问题和解决方案

MVVM(Model View ViewModel)是一种 MVC(Model View Controller)的一种变型,来解决 MVC 中庞大复杂的 Controller 难以维护的问题。大致上讲 MVVM 有几个要求:

  • Models 不能跟主动对任何对象交流(这点与 MVC 一样)
  • View Model 只能主动对 Models 交流
  • View Controller 不直接与 Models 交流,只和 View Models 和 Views 交流
  • Views 只与 View Controllers 交流,通知他们用户交互的事件(这点也和 MVC 一样)

MVVM 和 MVC 有很多类似的特点,主要的不同有:

  • 有一个 View Model 类
  • View Controller 不能直接接触 Models

另外一点,MVVM 默认 View 和 View Controller 有一个一对一的关系,一般我们把这两个看做一个整体,会以 .swift文件 和 Storyboard 的形式出现。

View Model 的工作是处理所有的展示数据的逻辑。如果一个 model 中有一个 NSDate对象,NSDateFormatter就会在 View Model 中用来设置日期的展示形式。

View Model 不能接触任何用户界面的部分,View Model 文件中不应该 import UIKit,View Controller 会观察 View Model 去了解什么时候显示新的数据(通过 KVO 或者 FRP(Functional Reactive Programming))

MVVM 和 MVC 有一个共同的弱点:没有清楚的定义应该把网络请求部分放在哪里。在实际操作过程中,我会把网络请求放在 View Model 文件里面,但之后我打算把网络请求放在自己独立的一个类中,View Model 文件会拥有这个对象。

下面我们主要谈一谈实际应用 MVVM 过程中一些挑战:

如何构造用户界面

例如你想构造这样一个常用的界面,有一个 segment control 在屏幕顶部,屏幕的其他部分是一个 collection view,选择不同的 segment,就会展示不同样式的 collection view,元素的排列顺序。我们定义了一个 enum 来枚举所有的排列样式:

enum LayoutValues: Int {
    case layout0 = 0
    case layout1
    case layout2
}

那么这个 enum 在 MVVM 模式中应该放在哪里呢?因为这个 enum 决定了数据排列的顺序,每个 cell 中的文字和按钮的 title,这些都属于展示的逻辑,所以这个 enum 看起来应该放在 view model 中。

然而,这些 layout 并不改变要展示的数据,只是决定了要呈现的数据的排列方式和排列顺序,从这个角度上来说 enum 又应该放在 view controller 中。

我的解决方法是把 enum 放在 view model 中,然后在 view model 中加一个对外的Observable或者Signal来表示使用了哪个 layout,基于用户选择的 segment,view model 更新这个值,然后在 view controller 中根据相应的 layout 改变 collection view 的样式,view controller 也可以根据这个值来决定用哪个 cell reuse identifier

如何构造 View Model

iOS 开发者在用 MVVM 和 FRP 写应用的时候最常见的问题可能就是 ViewModel 怎么把数据展现给 ViewController。当 Model 层的数据发生变化更新的时候,ViewController 需要得到通知然后做出相应的 UI 更新,我们一般会用到两种机制:

  1. 在 View Model 使用 property,可以用 KVO 来观察数据的变化 (或者用 FRP 把属性封装在信号或者序列中)
    2.把信号或者序列当做 property 放在 View Model 中,然后可以用相应的第三方库和框架

第一个选项很吸引人,因为可以在 View Controller 中决定怎么选择观察那些 property。然而,我不推荐在 Swift 中使用第一个选项,因为 Swift 在 KVO 中没有类型检查,你需要对 AnyObject 强制转换类型很多次。

第二个选项是比较 Swift 的方式,基于 Swift 的 generics 特性,signals,sequences,observables 可以支持编译过程中的类型检查。

但有时候在 view model 增加这些 Signals 或者 Observables 有些困难。Swift 的初始化方法对于什么时候对 property 赋值有非常明确的规定。Signals 或者 Observables 需要使用 view model 内部的状态,所以它们必须在super.init()之后才能创建,但是另一方面,我们在调用super.init()之前保证所有 property 已经被赋值了,包括那些 Signal/Observable property。

这是个先有鸡还是先有蛋的问题。

我采用比较简单的解决方法:定义成var的隐式可选类型,这样就可以在super.init()之后才给 property 赋值。这不是一个完美的解决办法。我们可以用lazy var property 的闭包赋值来代替上面的方法。在 Swift 不断完善和更新的过程中,大家也可以探索其他更好的办法。

如何处理用户交互

举一个很常用的例子,用户点击 collection view 中的一个 cell,跳转到详情页面。用户点击的操作应该在 view controller 中处理,具体内容是展现一个新的详情页面。但是 view controller 不能直接接触 models,我们要如何用 MVVM 模式实现这样的用户交互呢?

我的解决方案是利用 Swift 的闭包。首先在 view model 中定义一个闭包:

typealias ShowDetailsClosure = (Item) -> Void

然后在 view model 中添加一个 property:

class ViewModel {
  let showDetails: ShowDetailsClosure
  init(...
       showDetails: ShowDetailsClosure,
       ...
}

接着我需要调用闭包,在 view model 中定义一个view controller 可以调用的函数,这个函数的参数是可以决定使用什么数据,一般情况下常用 index path:

func showDetailsForItemAtIndexPath(indexPath: NSIndexPath) { 
    showDetails(Items[indexPath.item])
}

现在当用户选中一个 cell,会调用 view model 中的这个函数,并且传入 index path 参数,view model 决定使用哪个数据,并调用在 view controller 中定义的闭包,例如:

func showDetailsForItem(item: Item) {
    performSegueWithIdentifier(SegueIdentifier.ShowItemDetails.rawValue, sender: item)
}

最后一个问题是怎么创建这个 view model。我们需要传递一个闭包给view model 的初始化函数,然后用 lazy loading 来调用 view model 的初始化函数。

lazy var viewModel = {
    return ViewModel(..., showDetails: self.showDetailsForItem, ...)
}

相关文章

网友评论

  • b7ac9371e916:我们现在对于一个Cell都会bind一个CellViewModel,用脚趾头想这就是和MVVM设计思想不对的,甚至还不如MVC,导致现在嵌套特别复杂。。。但是如果按照MVVM的思想走,每个Cell分配一个Model(或在Vc中给Cell控件挨个赋值),然后Cell中Button的操作又通过delegate(或RacSubject的变种)回到Vc中处理,那这样又是经典的开发模式。我想知道你们怎么处理这种情况,还是我太迷信MVVM,而期待他对经典的设计模式都能有新的实现方式!
  • b7ac9371e916:文中说 “默认一个view对应一个VC”。很显然实际情况不会这么简单。不仅会有table,而且还会有headerView(内容还很丰富),甚至有时还有一个内容丰富的浮层。。。这种情况我想知道你们是怎么处理的。如果按照MVVM的设计思路,我可能回这么做:VC owner 多个View,Vc owner 多个ViewModel(和View一一对应),这样View 和 ViewModel 都只被Vc引用;Vc控制View和ViewModel 的更新和observe。但是这样Vc又会非常的复杂。想看看你们的方案。
  • 仰望星空之Rocky:大神,有demo吗?
  • Homer1ynn:博主,我是OC用RAC实现的MVVM,我的想法是这样的.在VM中创建RACSubject信号(用于选中cell返回数据),然后在Controller中订阅信号,从而就能在C中拿到数据,我感觉这样写起来好像更统一一些.虽然其实都是一样的,但是,作为一种参考,我还是想了解一下,博主在者两种方式中会选择哪一种,并且阐述一下想法.
    最Fly的Engine人:@HomerIce 谢谢你的留言。个人觉得用户交互在 MVVM 中的主要问题就是容易把 model 暴露在 view controller 中,文中的做法也不是完美的做法,还是会让 view controller 接触到 model,但是因为文章中的方法是让 view model 查询可用的 model 和让 view model 调用 closure,我觉得可以最大程度的减少 controller 和 model 之间的耦合。按我的理解,你的方法是让 controller 查询可用的 model,让 controller 调用方法,可能会增加 controller 和 model 之间的耦合。这是我的理解。

本文标题:iOS 开发中的 MVVM 模式——实用进阶篇(整理)

本文链接:https://www.haomeiwen.com/subject/aaejyttx.html