面向協議的MVVM

LieselotteM 8年前發布 | 10K 次閱讀 MVVM模式 iOS開發 Apple Swift開發

來自: http://www.cocoachina.com/swift/20160211/15241.html

蘋果在WWDC2015上介紹了面向協議的編程思想,以及Swift 2.0中可以支持這一編程思想的新特性。

參見 WWDC2015 Session 408 Protocol-Oriented Programming in Swift

然后女中豪杰NatashaTheRobot將其和MVVM結合起來,寫了一篇文章介紹面向協議的MVVM(POMVVM),并在2015年底舊金山Swift峰會上作為一個主題分享。

本文主要基于以上兩個資料,并參考其它一些資料,總結出一套最適合的自己項目的 POMVVM 架構方法。

Protocol-Oriented Programming(POP)

關于面向協議編程的具體內容可以去看Session 408,這里只引用其中一句話:

Don't start with a class.  
Start with a protocol.

Protocol Extentions

Swift 2.0增加了Protocol Extentions這個強大的特性,也是這個特性讓POP成為可能。看看這個特性是怎么運作的:

首先定義一個Animal Protocol和其中的屬性:

protocol Animal {
    var name: String { get }
    var canFly: Bool { get }
    var canSwim: Bool { get }
}

并定義三種具體的動物:

struct Parrot: Animal {
    let name: String
    let canFly = true
    let canSwim = false
}

struct Penguin: Animal { let name: String let canFly = true let canSwim = true }

struct Goldfish: Animal { let name: String let canFly = false let canSwim = true }</pre>

這里對canFly和canSwim兩個屬性,每個具體的動物中都要實現一次,非常的不優雅,但利用Swift 2.0我們有了更好的實現方式。

定義Flyable、Swimable兩個Protocol

protocol Flyable {

}

protocol Swimable {

}</pre>

利用Protocol Extensions給protocol增加默認實現

extension Animal {
    var canFly: Bool { return false }
    var canSwim: Bool { return false }
}

extension Animal where Self: Flyable { var canFly: Bool { return true } }

extension Animal where Self: Swimable { var canSwim: Bool { return true } }</pre>

這里對于符合Flyable協議的Animal,就把canFly返回true

對于符合Swimable的Animal,就把canSwim返回true

改造三個具體動物的實現

struct Parrot: Animal, Flyable {
    let name: String
}

struct Penguin: Animal, Flyable, Swimable { let name: String }

struct Goldfish: Animal, Swimable { let name: String }</pre>

這么寫是不是看上去優雅多了。

如果想讓某個動物可以飛并且具有飛的相關行為,只要讓這個動物類去符合Flyable協議就行了,不需要往這個類里寫任何代碼,如果將來需求變動又不需要飛了,只要把Flyable協議去掉。

可能你說,我把Animal作為一個基類,把默認實現寫在基類里,子類中覆蓋基類的屬性和方法來提供不同的實現。恩,這樣寫也行,這是面向對象編程的基本思路,但這樣顯然沒有POP的方式簡潔優雅,其次,如果在基類中增加了一個fly()的方法,那不會飛的動物也不得不獲得了這個方法,顯得有些冗余,所以對這些會飛的動物再寫一個基類,增加一層繼承,好,這個問題解決了,那么如果一個動物又有fly()方法又有swim()方法呢?

而使用Swift2.0的POP思路就能完美解決這些問題。

用Protocol Extentions來提供默認實現相比基類或抽象類有這些優勢:

  1. 類只能繼承一個類,而一個類型可以符合多個Protocol,可以同時被多個Protocol裝飾上多種默認行為。

  2. Protocol可以被應用到類、結構體和枚舉,而類繼承只能在類中使用。或者說,Protocol Extensions能為值類型提供默認行為而不僅僅是類。

  3. Protocol Extentions不會給類型引進任何額外的狀態,它是高度解耦的。

MVVM

關于MVVM這里就不贅述了,可以參考MVVM核心概念

面向協議的MVVM

我們用一個小DEMO來講述如何實現面向協議的MVVM架構,這個小DEMO就是Xcode提供的Master-Detail模板,里面默認實現了一個TableView列表,點擊右上角的加號可以向列表中添加當前日期,我們在這個項目模板的基礎上刪除無用的代碼,來實踐我們的POMVVM。

MVVM的數據綁定部分我們采用了GitHub上一個開源庫SwiftBond,這個庫的優勢用法簡單方便,支持iOS7,維護者更新較為頻繁,目前已經是v4.2.0版本。另外RxSwift這個庫也是一個很好的選擇。

DEMO源碼下載地址:

https://github.com/liuduoios/POMVVMDemo

建議大家閱讀后面的內容之前先下載源碼跑一下。

POP,一切從Protocol開始思考,用Protocol來提供最高級別的抽象。

MVVMBase

首先我們來抽象MVVM架構層面的東西

抽象出ViewModel

protocol ViewModel {

}</pre>

抽象出可綁定的視圖,這里用了泛型協議

protocol BindableView {
    typealias ViewModelType
    var viewModel: ViewModelType! { get }
    func bindViewModel(viewModel: ViewModelType)
}

抽象出可綁定的TableViewCell,簡單繼承于BindableView,為了增強抽象性和描述性

protocol BindableTableCell: BindableView {

}</pre>

業務抽象

到每個具體界面,首先考慮不是界面的具體實現,而是先抽象出業務邏輯。

對于主界面:

我們首先抽象出兩個Protocol

數據源Protocol:

protocol MasterViewControllerDataSource {
    var items: ObservableArray { get }
    var openSwitchCount: Observable { get }
}

業務邏輯Protocol:

protocol MasterViewControllerBusinessDelegate {
    /// 插入當前日期
    func insertNowDate()
    /// 更新打開開關的個數
    func updateOpenSwitchCount()
}

對于主界面上的Cell:

抽象出它的數據源,并通過Protocol Extension給文字顏色提供一個默認的共享實現:

protocol DateCellDataSource {
    var text: Observable { get }
    var on: Observable { get set }
    var textColor: Observable { get }
}

extension DateCellDataSource { var textColor: Observable { return Observable(.greenColor()) } }</pre>

業務邏輯Protocol:

protocol DateCellBusinessDelegate {
    mutating func openSwitch()
    mutating func closeSwitch()
}

對于Detail界面:

數據源Protocol:

protocol DetailViewControllerDataSource {
    var on: Observable { get set }
    var text: Observable { get set }
}

業務邏輯Protocol:

protocol DetailViewControllerBusinessDelegate {
    func openSwitch()
    func closeSwitch()
}

可以發現Cell和Detail擁有相同的業務邏輯,我們后面會說這個問題。

ViewModel

實現主界面的ViewModel:

struct MasterViewModel: MasterViewControllerDataSource {
    var items: ObservableArray = ObservableArray(Item)
    var openSwitchCount: Observable = Observable(0)
}
extension MasterViewModel: MasterViewControllerBusinessDelegate {
    /// 插入當前日期
    func insertNowDate() {
        let item = Item(text: NSDate().description, on: false)
        items.insert(item, atIndex: 0)
    }

/// 更新打開開關的個數
func updateOpenSwitchCount() {
    openSwitchCount.next(currentOpenSwitchCount())
}

/// 獲取當前打開開關的個數
private func currentOpenSwitchCount() -> Int {
    print(items.array)
    let count = items.array.filter { $0.on.value }.count
    return count
}

} extension MasterViewModel: ViewModel {}</pre>

實現Cell的ViewModel:

這里我們為每一個Cell都創建一個ViewModel,把cell中的業務邏輯放到cell的ViewModel中,有效的減少了ViewController中的代碼數量。

struct DateCellViewModel: DateCellDataSource {

// -------------------------
// MARK: - Public Properties
// -------------------------

private(set) var text: Observable = Observable(nil)
var on: Observable = Observable(false)

// ------------------------
// MARK: - Underlying Model
// ------------------------

private var item: Item {
    didSet {
        configureBinding()
    }
}

private func configureBinding() {
    item.text.bidirectionalBindTo(text)
    item.on.bidirectionalBindTo(on)
}

// -----------------
// MARK: - Lifecycle
// -----------------

init(item: Item) {
    self.item = item
    configureBinding()
}

} extension DateCellViewModel: ViewModel {}</pre>

View/ViewController

主界面中的實現:

主界面MasterViewController,我們讓它符合協議BindableView,然后來實現BindableView中的屬性和方法。

首先對于泛型類型ViewModelType,把它實現成

typealias ViewModelType = protocol

這樣就不用關心它具體是哪個類型,只要是符合這些協議的類型都可以。

借助SwiftBond強大的數據綁定功能,對于UITableView和UICollectionView,我們不用再去實現繁瑣的UITableViewDataSource中的方法,只要做一下綁定操作后,只要修改了數據(插入、修改、刪除),TableView的界面會立刻隨之更新。

var viewModel: ViewModelType!
func bindViewModel(viewModel: ViewModelType) {
    viewModel.dates.lift().bindTo(tableView) { indexPath, dataSource, tableView in
        let cell = tableView.dequeueReusableCellWithIdentifier("DateCell", forIndexPath: indexPath) as! DateCell
        if let cellViewModel = self.viewModel.cellViewModelAtIndex(indexPath.row) {
            cell.bindViewModel(cellViewModel)
        }
        return cell
    }
}
func insertNewObject(sender: AnyObject) {
    viewModel.insertNowDate()
}

可以看到這里我們都是用ViewModelType這個類型來調用接口,并不關心具體實現。

Cell中的實現:

class DateCell: UITableViewCell, BindableTableCell {
    typealias ViewModelType = DateCellViewModel

var viewModel: ViewModelType?

func bindViewModel(viewModel: ViewModelType) {
    self.viewModel = viewModel
    viewModel.date.bindTo(textLabel!.bnd_text)
    textLabel?.textColor = viewModel.textColor.value
}

}</pre>

現在基本的實現已經寫好了,我們點擊主界面右上角的加號時,就會往主界面的MasterViewModel中的items數組中添加一條當前日期的數據,隨之界面會自動更新來展現出這條數據。

這里我們會遇到一個問題,如果每個Cell中都有一個開關,在改變開關的時候會去更新CellViewModel,那么這個更新如何同步到整個列表的ViewModel的Items數組中呢?如果是用Objective-C實現,那么可能這個問題不是一個問題,但是我們用的是Swift實現,總所周知,Swift中的數組是值類型的,就是說,如果我們這樣操作:

let array = ["a", "b", "c"]
var item = array[1]
item = "new"

從數組里通過下標去取一個值,然后改變這個值,這個改變是不會作用到原數組中的。

我們可以借助SwiftBond庫提供的雙向綁定功能來解決這個問題

// 把數據綁定到TableView上
viewModel.items.lift().bindTo(tableView) { indexPath, dataSource, tableView in
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! DateCell
    let item = dataSource[indexPath.section][indexPath.row]

// 為每個cell綁定cellViewModel
let cellViewModel = DateCellViewModel(item: item)
cell.bindViewModel(cellViewModel)

return cell

}</pre>

這里通過下標從dataSource中取出了item,按道理說對item的屬性進行賦值是不會改變viewModel的items數組中對應的item的值的。但是Swift對值類型的拷貝時機是有優化的,一般來說,拷貝操作會盡量推遲到真正需要拷貝的時候。所以我們可以在真正的拷貝發生前對其進行雙向綁定,就能解決這個問題了。

這里在cell.bindViewModel中去進行雙向綁定:

func bindViewModel(var viewModel: ViewModelType) {
    self.viewModel = viewModel

bnd_bag.dispose()

// ViewModel中關于text的屬性單向綁定到label的相關屬性上
viewModel.text.bindTo(label.bnd_text)
viewModel.textColor.bindTo(label.bnd_textColor)

// ViewModel的on和UISwitch的on雙向綁定
viewModel.on.bidirectionalBindTo(cellSwitch.bnd_on)

cellSwitch.bnd_on
    .distinct()
    .observeNew { switchOn in
        if switchOn {
            viewModel.openSwitch()
        } else {
            viewModel.closeSwitch()
        }
    }.disposeIn(bnd_bag)

}</pre>

我們可以在cell中的開關切換狀態時,去讀取一下items數組中on為true的項目的個數,并且顯示在tableHeaderView中,經測試是沒有問題的。

然后,用同樣的方法,在點擊cell后進入詳情時,同樣也是從items數組中取出一個item,并用其創建一個DetailViewModel,傳給DetailViewController,代碼如下:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "showDetail" {
        if let indexPath = self.tableView.indexPathForSelectedRow {
            let controller = segue.destinationViewController as! DetailViewController

        // 取出viewModel.items中對應的Item,創建一個DetailViewModel
        let detailViewModel = DetailViewModel(item: viewModel.items[indexPath.row])
        controller.viewModel = detailViewModel
    }
}

}</pre>

在DetailViewController中進行綁定操作:

func bindViewModel(viewModel: ViewModelType) {
    // ViewModel的text和TextField的text雙向綁定
    viewModel.text.bidirectionalBindTo(detailTextField.bnd_text)
    // ViewModel的text單項綁定到Label上
    viewModel.text.bindTo(detailDescriptionLabel.bnd_text)

// ViewModel的on和UISwitch的on雙向綁定
viewModel.on.bidirectionalBindTo(detailSwitch.bnd_on)

detailSwitch.bnd_on.observeNew { switchOn in
    if switchOn {
        viewModel.openSwitch()
    } else {
        viewModel.closeSwitch()
    }
}

}</pre>

進入詳情界面后,會發現對Switch進行開關,或者修改TextField中的內容,這些變化都會反饋到主列表界面中。

抽取公共邏輯

現在我們開始用上Protocol Extentions的特性。

可以看到在列表的cell和詳情界面中,都有Switch關閉的操作,我們用這個來代表某個相同的業務邏輯,因此他們有著相同的實現。

通過Protocol Extensions,我們就可以不必在每個ViewModel里都分別去實現這個邏輯,只要對符合某個協議的Protocol進行擴展即可。

首先創建Switchable協議,符合這個協議的結構就是可以支持開關切換的。它的實現如下:

protocol Switchable {
    var on: Observable { get set }
    mutating func openSwitch()
    mutating func closeSwitch()
}
extension ViewModel where Self: Switchable {
    func openSwitch() {
        print("I have opened the switch.")
    }

func closeSwitch() {
    print("I have closed the switch.")
}

}</pre>

我們對ViewModel這個Protocol進行了擴展,凡是實現了Protocol協議,并且也實現了Switchable協議的結構,會自動獲得openSwitch()和closeSwitch()這兩個默認的實現。

然后把DateCellViewModel和DetailViewModel都去符合一下Switchable協議,這兩個ViewModel就都獲得了默認實現,不需要分別各自實現了。

DateCellViewModel:

extension DateCellViewModel: DateCellBusinessDelegate, Switchable {}

DetailViewModel:

extension DetailViewModel: DetailViewControllerBusinessDelegate, Switchable {}

這樣POMVVM的結構就基本完成了。

總結

可以看到它相比普通的MVVM:

  • 能提供更高的抽象。

  • 面向接口編程實現解耦。

  • 更符合Swift的寫法,更加優雅。

  • 可以用Protocol Extentions抽取公共的業務邏輯。

  • 利用Protocol Extentions給類型添加/去除功能而不引進額外狀態。

參考資料

本文源代碼下載地址: https://github.com/liuduoios/POMVVMDemo

參考資料:

</div>

 本文由用戶 LieselotteM 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!