美文网首页
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