美文网首页
MVVM + ReactiveCoocoa 5

MVVM + ReactiveCoocoa 5

作者: chdo002 | 来源:发表于2017-04-01 01:48 被阅读127次

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

相关文章

网友评论

      本文标题:MVVM + ReactiveCoocoa 5

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