面向協議的MVVM
來自: 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來提供默認實現相比基類或抽象類有這些優勢:
類只能繼承一個類,而一個類型可以符合多個Protocol,可以同時被多個Protocol裝飾上多種默認行為。
Protocol可以被應用到類、結構體和枚舉,而類繼承只能在類中使用。或者說,Protocol Extensions能為值類型提供默認行為而不僅僅是類。
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 = DateCellViewModelvar 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 = viewModelbnd_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