MVVM + ReactiveCoocoa 5

MVVM + ReactiveCoocoa 5

三个月前我写了自己的第一个应用,Memori, 同时,我也开始使用MVC的架构模式,但是随着功能的增加,C层越来越臃肿。

MVC的缺陷


总得来说就是业务代码放在viewcontroller中,会导致C层太大,同时,view层有和C层太紧密,导致我们要考虑太多的视图相关的问题。Viewcontroller还要管理起自己的生命周期,有时还要管理其他的控制器的,所以,MVC又被戏称"Massive View Controller".

MVC 的另一个选择 MVVM

除了MVC外,其实还有很多的架构可以选择,包括MVVM。在MVVM里UIViewcontroller现在是View中的一部分,然后,又引入了一个概念叫做ViewModel来填补View和Model之间的空隙。


MVVM 和 ReactiveCoocoa配合会产生非常好的效果,当也有其他的选择,包括SwiftBond,RxSwift。他们都非常的类似。下面的代码用的是RAC:版本5.

下面主要通过一些简单的例子来讲MVVM

例子

在Memori的第一屏中有个label,负责显示用户当前的信息填写的完成程度,现在的需求是,当用户把collectionview中的某个item删除时,label的值也应该随之改变。

struct Book {  
   let name: String
   let cardCount: Int
   let progress: Float
   ...
}

class BookStore {  
   let books = MutableProperty([Book]())
   ...
}

MutableProperty来自ReactiveSwift,你可以把它视为一个可以监听或者绑定其他属性的地方。

ViewModel

class ViewModel {

   let currentProgress = MutableProperty("")

   init(withBookStore bookStore: BookStore) {
      // Each time 'books' is updated on the store,  'currentProgress' is updated with the computed value
      currentProgress <~ bookStore.books.map { books in
         let progress = computeCurrentProgress(fromBooks: books)
         return "\(progress*100)% KNOWN"
      }
   }

   ...
}

<~是RAC中的一个用来绑定target和signal的重载符。MutableProperty既是target又是signal,所以我们可以把它绑定到BookStore.booksViewModel.currentProgressmap(transform)方法可以帮我们把boos列表转化成字符串。

View

class ViewController: UIViewController {

   // Injected
   var viewModel: ViewModel!

   // Injected
   @IBOutlet var progressLabel: UILabel!

   override func viewDidLoad() {
      super.viewDidLoad()

      // Uses ReactiveCocoa extensions to bind the text
      // of the UILabel to the Property in the ViewModel
      progressLabel.reactive.text <~ viewModel.currentProgress
   }

   ...
}

RAC 在绝大数UIView中添加了reactive部分,来让我们直接把文本绑定到UILabel上去。
代码写完
就这样,通过更改在model中的books属性,它就会自动更改view上的label了。

例如,用户删除了tableview中的某一行,view就会通知viewmodel,然后viewmodel通知model,修改了books的属性。


在用MVVM的时候,有几条规则:

  1. viewmodel绝不使用UIKit。这条规则是非常重要的,它把viewmodel从view中分离了出来。如果你谨遵这条规则,你甚至可以在iOS 和 macOS的应用里用相同的viewmodel。
  2. view也和model不直接产生关系。 viewmodel负责把model的数据暴露给view,view不应该关心数据怎样产生。
  3. model和app剩余的部分完全没有联系。

在写代码时,这些规则应该被强制实施,尤其是在大型的项目中。

Collections

按照上面思路使用UIViewController还好,但是使用UITableView或者UICollectionVie时就会产生问题了,该谁来负责做DataSource?由于cell是懒加载的,并且是复用的,那应该怎么把viewmodel绑定到cell上呢?

datasource应该是由ViewModel提供,在这种情况下的最好的处理方式是:由viewmodel给每一个cell提供一个单独的viemodel,这样的话,我们又有了两个新的规则:

  1. 只有view才可以创建view。
  2. 只有viewmodel才可以创建viewmodel(在app启动时除外)



    这样的分离概念是非常好的:viewmodel部分没有关联到view,他只知道怎么创建一个cell的viemodel,你可以不改任何代码的在tableview和collectionview之间切换。
    相关代码如下:

ViewModel

class ViewModel {  
   private var books: [Book]
   func getBookCount() -> Int { 
      return books.count 
   }
   func createCellViewModel(forIndex index: Int) -> CellViewModel {
      return CellViewModel(withBook: books[index]!)
   }
}

struct CellViewModel {  
   let name: String
   let cardCount: String
   init(withBook book: Book) {
      self.name = book.name
      self.cardCount = String(book.cardCount)
   }
}

这里没有使用MutableProperty,因为绑定会带来不必要的性能损耗。

View

class ViewController: UIViewController, UITableViewDataSource {

   ...

   func tableView(...numberOfRowsInSection section: Int) -> Int {
      return viewModel.getBookCount()
   }

   func tableView(...cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      // The View creates the View
      let cell = tableView.dequeueReusableCell(withIdentifier: "BookCell")
         as! TableViewCell
      // The ViewModel creates the ViewModel
      cell.viewModel = viewModel.getBookViewModel(atIndex: indexPath.row)
      return cell
    }
}

class TableViewCell: UITableViewCell {

   @IBOutlet var nameLabel: UILabel!
   @IBOutlet var cardCountLabel: UILabel!

   var viewModel: CellViewModel! {
      didSet {
         nameLabel.text = viewModel.name
         nameLabel.cardCount = viewModel.cardCount
      }
   }
}

基本上完成了,现在还有一个问题,view怎么知道什么时候reload呢?最基本的方法就是在viewmodel中创建一个Signal,当books 更新时,信号就会发出信息,然后view就知道要reload了。
然而,这样的信息并不足够,在MVVM里,view并不可以获取model,所以他亦不知道数据的增删改查,解决这个问题,我使用了一个第三方库Changeset,他可以算出两数据的差别,同时也做了对tableview和collectionview的扩展,这样可以直接使用。

理想的状态是无论何时viewmodel更改了books的属性,Changeset就会计算出数据的变化(edits),并发送到view上。然后可以产生合适的动画。

// ViewModel 
class ViewModel {

   let booksChangeset = MutableProperty([Edit]())

   private var books: [Book] {
      didSet {
         booksChangeset.value = Changeset.edits(
            from: oldValue,
            to: books)
      }
   }

   func deleteBook(at index: Int) {
      books.remove(at: index)
   }
}

// View
class ViewController: UIViewController {  
   override func viewDidLoad() {
      ...
      viewModel.booksChangeset.producer
         .startWithValues { edits in
            self.tableView.update(with: edits)
         }
      }
   }
}

如你所见,这很简单,并且是个非常强大的模式:viewmodel与view解耦,但是books属性变化时,tableview又可以自动通过合适的动画变化,你也可以轻松的进行单元测试。

导航

导航部分的话我们一般这么做:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {  
   if segue.identifier == "AddBook",
      let viewController = segue.destination as? AddBookNameViewController {
      viewController.viewModel = viewModel.getAddBookViewModel()
   }
}

但是有个关于解耦view和viewmodel的有趣的地方。下面的例子是一个关于创建book的流程:


我们需要两个UIViewController来对应两个不同的场景。但是添加一个book一个流程,所以我们可以把一个viewmodelAddBoolViewModel用在两个地方。在第一个vc中设置viewmodel的name,然后把viewmodel传递到第二个vc。

来源: @JoanZap

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

推荐阅读更多精彩内容