對於Massive View Controller,現在流行的解決方案是MVVM架構,把業務邏輯移入ViewModel來減少ViewController中的代碼。
這幾天又看到另一種方案,在此介紹一下。
例子
我們通過例子來說明,這裡舉的例子是一個常見的基於TableView的界面——一個通訊錄用戶信息列表。
我們要實現的業務流程如下
App啟動後首先讀取本地Core Data中的數據,並展現出來,然後調用Web API來獲取到用戶數據列表,然後更新本地Core Data數據庫,只要數據更新了,UI上的展現也隨之變化。
用戶也可以在本地添加用戶數據,然後這些數據會同步到服務端。
1. 聲明協議
我們不會把所有的業務邏輯都寫到ViewController裡,而是首先聲明兩個protocol:
PeopleListDataProviderProtocol
定義了數據源對象要實現的屬性和方法
public protocol PeopleListDataProviderProtocol: UITableViewDataSource { var managedObjectContext: NSManagedObjectContext? { get set } weak var tableView: UITableView! { get set } func addPerson(personInfo: PersonInfo) func fetch() }
APICommunicatorProtocol
定義了API請求者要實現的屬性和方法
public protocol APICommunicatorProtocol { func getPeople() -> (NSError?, [PersonInfo]?) func postPerson(personInfo: PersonInfo) -> NSError? }
2. 編寫ViewController
我們的ViewController叫做PeopleListViewController,在其中聲明兩個屬性:
public var dataProvider: PeopleListDataProviderProtocol? public var communicator: APICommunicatorProtocol = APICommunicator()
實現ViewDidLoad
override public func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. self.navigationItem.leftBarButtonItem = self.editButtonItem() let addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: "addPerson") self.navigationItem.rightBarButtonItem = addButton // ViewController繼承於UITableViewController assert(dataProvider != nil, "dataProvider is not allowed to be nil at this point") tableView.dataSource = dataProvider dataProvider?.tableView = tableView }
添加按鈕的事件響應方法和回調:
func addPerson() { let picker = ABPeoplePickerNavigationController() picker.peoplePickerDelegate = self presentViewController(picker, animated: true, completion: nil) } extension PeopleListViewController: ABPeoplePickerNavigationControllerDelegate { public func peoplePickerNavigationController(peoplePicker: ABPeoplePickerNavigationController, didSelectPerson person: ABRecord) { let personInfo = PersonInfo(abRecord: person) dataProvider?.addPerson(personInfo) } }
然後再添加兩個方法來請求和同步數據:
public func fetchPeopleFromAPI() { let allPersonInfos = communicator.getPeople().1 if let allPersonInfos = allPersonInfos { for personInfo in allPersonInfos { dataProvider?.addPerson(personInfo) } } } public func sendPersonToAPI(personInfo: PersonInfo) { communicator.postPerson(personInfo) }
到此,我們的ViewController已經全部完成了,只有60行代碼,是不是很開森。
那Web API調用、Core Data操作,業務邏輯的代碼都去哪兒了呢?
OK,我們可以開始編寫實現那兩個協議的類了。
3. 實現Protocol
首先是實現了APICommunicatorProtocol的APICommunicator類:
public struct APICommunicator: APICommunicatorProtocol { public func getPeople() -> (NSError?, [PersonInfo]?) { return (nil, nil) } public func postPerson(personInfo: PersonInfo) -> NSError? { return nil } }
與服務端的交互這裡就先省略了,就簡單實現一下。
然後再看實現了PeopleListDataProviderProtocol的PeopleListDataProvider類:
主要是以下幾個部分:
對Core Data操作的實現:
public func fetch() { let sortKey = NSUserDefaults.standardUserDefaults().integerForKey("sort") == 0 ? "lastName" : "firstName" let sortDescriptor = NSSortDescriptor(key: sortKey, ascending: true) let sortDescriptors = [sortDescriptor] fetchedResultsController.fetchRequest.sortDescriptors = sortDescriptors var error: NSError? = nil do { try fetchedResultsController.performFetch() } catch let error1 as NSError { error = error1 print("error: \(error)") } tableView.reloadData() }
對TableViewDataSource的實現:
public func numberOfSectionsInTableView(tableView: UITableView) -> Int { return self.fetchedResultsController.sections?.count ?? 0 } public func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) self.configureCell(cell, atIndexPath: indexPath) return cell } func configureCell(cell: UITableViewCell, atIndexPath indexPath: NSIndexPath) { let person = self.fetchedResultsController.objectAtIndexPath(indexPath) as! Person cell.textLabel!.text = person.fullname cell.detailTextLabel!.text = dateFormatter.stringFromDate(person.birthday) }
對NSFetchedResultsControllerDelegate的實現:
public func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) { switch type { case .Insert: tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade) case .Delete: tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade) case .Update: self.configureCell(tableView.cellForRowAtIndexPath(indexPath!)!, atIndexPath: indexPath!) case .Move: tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade) tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade) } }
(未列出所有代碼)
可以看到,我們把業務邏輯都放入了PeopleListDataProviderProtocol和APICommunicatorProtocol這兩個協議的實現中。在ViewController中通過屬性來引用這兩個協議的實現類,並且調用協議中定義的方法。
優勢
ViewController中的代碼就變的短小而清晰。
同MVVM一樣也實現了界面和業務邏輯的分離。
相對與MVVM,學習成本較低。
可以方便的創建Mock對象。
Mock對象
例如這個APICommunicator
public var communicator: APICommunicatorProtocol = APICommunicator()
在開發過程中或者單元測試時都可以用一個Mock對象MockAPICommunicator來替代它,來提供fake data。
class MockAPICommunicator: APICommunicatorProtocol { var allPersonInfo = [PersonInfo]() var postPersonGotCalled = false func getPeople() -> (NSError?, [PersonInfo]?) { return (nil, allPersonInfo) } func postPerson(personInfo: PersonInfo) -> NSError? { postPersonGotCalled = true return nil } }
這樣在服務端API還沒有部署時,我們可以很方便的用一些假數據來幫助完成功能的開發,等API上線後換成真正的APICommunicator類。
同樣可以提供一個實現了PeopleListDataProviderProtocol的MockDataProvider類。
也可以很方便的借用Mock對象來進行單元測試。
例如:
func testFetchingPeopleFromAPICallsAddPeople() { // given let mockDataProvider = MockDataProvider() viewController.dataProvider = mockDataProvider let mockCommunicator = MockAPICommunicator() mockCommunicator.allPersonInfo = [PersonInfo(firstName: "firstname", lastName: "lastname", birthday: NSDate())] viewController.communicator = mockCommunicator // when viewController.fetchPeopleFromAPI() // then XCTAssertTrue(mockDataProvider.addPersonGotCalled, "addPerson should have been called") }
總結
MVVM的優勢在於較為普遍,大家都懂的模式,減少了溝通成本。但是對於響應式編程、事件管道,ReactiveCocoa等概念,還是需要一定學習成本的。
在不使用MVVM的情況下,不妨試試本文介紹的結構來實現ViewController,為ViewController瘦身。
參考資料:
http://www.raywenderlich.com/101306/unit-testing-tutorial-mocking-objects
源碼下載:
http://cdn2.raywenderlich.com/wp-content/uploads/2015/04/Birthdays_Final1.zip