1、MVC
-
從字面意思來理解,MVC 即 Modal View Controller(模型 視圖 控制器),是 Xerox PARC 在 20 世紀 80 年代為編程語言 Smalltalk-80 發明的一種軟件設計模式,至今已廣泛應用於用戶交互應用程序中。其用意在於將數據與視圖分離開來。在 iOS 開發中 MVC 的機制被使用的淋漓盡致,充分理解 iOS 的 MVC 模式,有助於我們程序的組織合理性。
-
MVC 的幾個明顯的特征和體現:
-
View 上面顯示什么東西,取決於 Model。
-
只要 Model 數據改了,View 的顯示狀態會跟着更改。
-
Control 負責初始化 Model,並將 Model 傳遞給 View 去解析展示。
-
1)Modal 模型對象:
- 模型對象封裝了應用程序的數據,並定義操控和處理該數據的邏輯和運算。例如,模型對象可能是表示商品數據 list。用戶在視圖層中所進行的創建或修改數據的操作,通過控制器對象傳達出去,最終會創建或更新模型對象。模型對象更改時(例如通過網絡連接接收到新數據),它通知控制器對象,控制器對象更新相應的視圖對象。
-
2)View 視圖對象:
-
視圖對象是應用程序中用戶可以看見的對象。視圖對象知道如何將自己繪制出來,可能對用戶的操作作出響應。視圖對象的主要目的就是顯示來自應用程序模型對象的數據,並使該數據可被編輯。盡管如此,在 MVC 應用程序中,視圖對象通常與模型對象分離。
-
在iOS應用程序開發中,所有的控件、窗口等都繼承自 UIView,對應 MVC 中的 V。UIView 及其子類主要負責 UI 的實現,而 UIView 所產生的事件都可以采用委托的方式,交給 UIViewController 實現。
-
-
3)Controller 控制器對象:
-
在應用程序的一個或多個視圖對象和一個或多個模型對象之間,控制器對象充當媒介。控制器對象因此是同步管道程序,通過它,視圖對象了解模型對象的更改,反之亦然。控制器對象還可以為應用程序執行設置和協調任務,並管理其他對象的生命周期。
-
控制器對象解釋在視圖對象中進行的用戶操作,並將新的或更改過的數據傳達給模型對象。模型對象更改時,一個控制器對象會將新的模型數據傳達給視圖對象,以便視圖對象可以顯示它。
-
對於不同的 UIView,有相應的 UIViewController,對應 MVC 中的 C。例如在 iOS 上常用的 UITableView,它所對應的 Controller 就是UITableViewController。
-
-
1.1 簡單的 MVC
-
控制器加載模型數據並將數據轉換為數據模型。
-
控制器創建視圖控件,並將模型數據傳遞給視圖控件
1.2 iOS MVC 示意圖
-
1)Model 和 View 永遠不能相互通信,只能通過 Controller 傳遞。
-
2)Controller 可以直接與 Model 對話(讀寫調用 Model),Model 通過 Notification 和 KVO 機制與 Controller 間接通信。
-
3)Controller 可以直接與 View 對話,通過 outlet,直接操作 View,outlet 直接對應到 View 中的控件,View 通過 action 向 Controller 報告事件的發生(如用戶 Touch 我了)。Controller 是 View 的直接數據源(數據很可能是 Controller 從 Model 中取得並經過加工了)。Controller 是 View 的代理(delegate),以同步 View 與 Controller。
1.3 蘋果推薦的 MVC -- 願景
-
Cocoa MVC
-
由於 Controller 是一個介於 View 和 Model 之間的協調器,所以 View 和 Model 之間沒有任何直接的聯系。Controller 是一個最小可重用單元,這對我們來說是一個好消息,因為我們總要找一個地方來寫邏輯復雜度較高的代碼,而這些代碼又不適合放在 Model 中。
-
理論上來講,這種模式看起來非常直觀,但你有沒有感到哪里有一絲詭異?你甚至聽說過,有人將 MVC 的縮寫展開成 (Massive View Controller),更有甚者,為 View controller 減負也成為 iOS 開發者面臨的一個重要話題。如果蘋果繼承並且對 MVC 模式有一些進展,所有這些為什么還會發生?
-
1.4 蘋果推薦的 MVC -- 事實
-
Realistic Cocoa MVC
-
Cocoa 的 MVC 模式驅使人們寫出臃腫的視圖控制器,因為它們經常被混雜到 View 的生命周期中,因此很難說 View 和 ViewController 是分離的。盡管仍可以將業務邏輯和數據轉換到 Model,但是大多數情況下當需要為 View 減負的時候我們卻無能為力了,View 的最大的任務就是向 Controller 傳遞用戶動作事件。ViewController 不再承擔一切代理和數據源的職責,通常只負責一些分發和取消網絡請求以及一些其他的任務。
-
你可能會看見過很多次這樣的代碼:
BookModel *bookModel = [myDataArray objectAtIndex:indexPath.row]; [cell configWithModel:bookModel];
- 這個 cell,正是由 View 直接來調用 Model,所以事實上 MVC 的原則已經違背了,但是這種情況是一直發生的甚至於人們不覺得這里有哪些不對。如果嚴格遵守 MVC 的話,你會把對 cell 的設置放在 Controller 中,不向 View 傳遞一個 Model 對象,這樣就會大大減少 Controller 的體積。Cocoa 的 MVC 被寫成 Massive View Controller 是不無道理的。
-
直到進行單元測試的時候才會發現問題越來越明顯。因為你的 ViewController 和 View 是緊密耦合的,對它們進行測試就顯得很艱難--你得有足夠的創造性來模擬 View 和它們的生命周期,在以這樣的方式來寫 View Controller 的同時,業務邏輯的代碼也逐漸被分散到 View 的布局代碼中去。
-
1.5 MVC 自身的不足
-
MVC 是一個用來組織代碼的權威范式,也是構建 iOS App 的標准模式。Apple 甚至是這么說的。在 MVC 下,所有的對象被歸類為一個 model,一個 view,或一個 controller。Model 持有數據,View 顯示與用戶交互的界面,而 View Controller 調解 Model 和 View 之間的交互。然而,隨着模塊的迭代我們越來越發現 MVC 自身存在着很多不足。
-
1)MVC 在現實應用中的不足:
- 在 MVC 模式中 view 將用戶交互通知給控制器。view 的控制器通過更新 Model 來反應狀態的改變。Model(通常使用 Key-Value-Observation)通知控制器來更新他們負責的 view。大多數 iOS 應用程序的代碼使用這種方式來組織。
-
2)愈發笨重的 Controller:
-
在傳統的 app 中模型數據一般都很簡單,不涉及到復雜的業務數據邏輯處理,客戶端開發受限於它自身運行的的平台終端,這一點注定使移動端不像 PC 前端那樣能夠處理大量的復雜的業務場景。然而隨着移動平台的各種深入,我們不得不考慮這個問題。傳統的 Model 數據大多來源於網絡數據,拿到網絡數據后客戶端要做的事情就是將數據直接按照順序畫在界面上。隨着業務的越來越來的深入,我們依賴的 service 服務可能在大多時間無法第一時間滿足客戶端需要的數據需求,移動端愈發的要自行處理一部分邏輯計算操作。這個時間一慣的做法是在控制器中處理,最終導致了控制器成了垃圾箱,越來越不可維護。
-
控制器 Controller 是 app 的 “膠水代碼”,協調模型和視圖之間的所有交互。控制器負責管理他們所擁有的視圖的視圖層次結構,還要響應視圖的 loading、appearing、disappearing 等等,同時往往也會充滿我們不願暴露的 Model 的模型邏輯以及不願暴露給視圖的業務邏輯。這引出了第一個關於 MVC 的問題...
-
視圖 view 通常是 UIKit 控件(component,這里根據習慣譯為控件)或者編碼定義的 UIKit 控件的集合。進入 .xib 或者 Storyboard 會發現一個 app、Button、Label 都是由這些可視化的和可交互的控件組成。View 不應該直接引用 Model,並且僅僅通過 IBAction 事件引用 controller。業務邏輯很明顯不歸入 view,視圖本身沒有任何業務。
-
厚重的 View Controller 由於大量的代碼被放進 viewcontroller,導致他們變的相當臃腫。在 iOS 中有的 view controller 里綿延成千上萬行代碼的事並不是前所未見的。這些超重 app 的突出情況包括:厚重的 View Controller 很難維護(由於其龐大的規模);包含幾十個屬性,使他們的狀態難以管理;遵循許多協議(protocol),導致協議的響應代碼和 controller 的邏輯代碼混淆在一起。
-
厚重的 view controller 很難測試,不管是手動測試或是使用單元測試,因為有太多可能的狀態。將代碼分解成更小的多個模塊通常是件好事。
-
-
3)太過於輕量級的 Model:
- 早期的 Model 層,其實就是如果數據有幾個屬性,就定義幾個屬性,ARC 普及以后我們在 Model 層的實現文件中基本上看不到代碼(無需再手動管理釋放變量,Model 既沒有復雜的業務處理,也沒有對象的構造,基本上 .m 文件中的代碼普遍是空的);同時與控制器的代碼越來厚重形成強烈的反差,這一度讓人不禁對現有的開發設計構思有所懷疑。
-
4)遺失的網絡邏輯:
-
蘋果使用的 MVC 的定義是這么說的:所有的對象都可以被歸類為一個 Model,一個 view,或是一個控制器。就這些,那么把網絡代碼放哪里?和一個 API 通信的代碼應該放在哪兒?
-
你可能試着把它放在 Model 對象里,但是也會很棘手,因為網絡調用應該使用異步,這樣如果一個網絡請求比持有它的 Model 生命周期更長,事情將變的復雜。顯然也不應該把網絡代碼放在 view 里,因此只剩下控制器了。這同樣是個壞主意,因為這加劇了厚重控制器的問題。那么應該放在那里呢?顯然 MVC 的 3 大組件根本沒有適合放這些代碼的地方。
-
-
5)較差的可測試性
-
MVC 的另一個大問題是,它不鼓勵開發人員編寫單元測試。由於控制器混合了視圖處理邏輯和業務邏輯,分離這些成分的單元測試成了一個艱巨的任務。大多數人選擇忽略這個任務,那就是不做任何測試。
-
上文提到了控制器可以管理視圖的層次結構;控制器有一個 “view” 屬性,並且可以通過 IBOutlet 訪問視圖的任何子視圖。當有很多 outlet 時這樣做不易於擴展,在某種意義上,最好不要使用子視圖控制器(child view controller)來幫助管理子視圖。在這里有多個模糊的標准,似乎沒有人能完全達成一致。貌似無論如何,view 和對應的 controller 都緊緊的耦合在一起,總之,還是會把它們當成一個組件來對待。Apple 提供的這個組件一度以來在某種程度誤導了大多初學者,初學者將所有的視圖全部拖到 xib 中,連接大量的 IBoutLet 輸出口屬性,都是一些列問題。
-
2、MVC 的使用
-
Modal 模型的創建
-
Objective-C
// BookModel.h @interface BookModel : NSObject // 根據需要使用的數據創建數 Modal 數據模型屬性變量 @property(nonatomic, copy)NSString *title; @property(nonatomic, copy)NSString *detail; @property(nonatomic, copy)NSString *icon; @property(nonatomic, copy)NSString *price; + (instancetype)bookModelWithDict:(NSDictionary *)dict; @end // BookModel.m @implementation BookModel + (instancetype)bookModelWithDict:(NSDictionary *)dict { BookModel *book = [[self alloc] init]; [book setValuesForKeysWithDictionary:dict]; return book; } @end
-
Swift
// BookModel.swift class BookModel: NSObject { // 根據需要使用的數據創建數 Modal 數據模型屬性變量 var title:String? var detail:String? var icon:String? var price:String? }
-
-
View 視圖的創建
-
Objective-C
// BookCell.h @class BookModel; @interface BookCell : UITableViewCell // 創建 Cell 視圖包含的內容,Cell 使用 xib 創建 @property (weak, nonatomic) IBOutlet UIImageView *iconView; @property (weak, nonatomic) IBOutlet UILabel *titleLabel; @property (weak, nonatomic) IBOutlet UILabel *detailLabel; @property (weak, nonatomic) IBOutlet UILabel *priceLabel; // 創建 Cell 視圖賦值方法 @property (nonatomic, strong) BookModel *bookModel; @end // BookCell.m // 包含數據模型頭文件 #import "BookModel.h" @implementation BookCell // 從 Model 數據模型中取出數據更新 View 的內容 - (void)setBookModel:(BookModel *)bookModel { _iconView.image = [UIImage imageNamed:bookModel.icon]; _titleLabel.text = bookModel.title; _detailLabel.text = bookModel.detail; _priceLabel.text = bookModel.price; } @end
-
Swift
// BookCell.swift class BookCell: UITableViewCell { // 創建 Cell 視圖包含的內容,Cell 使用 xib 創建 @IBOutlet weak var iconView: UIImageView! @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var detailLabel: UILabel! @IBOutlet weak var priceLabel: UILabel! // 創建 Cell 視圖賦值方法,從 Modal 數據模型中取出數據更新 View 的內容 func configWithModel(bookModel:BookModel) { iconView!.image = UIImage(named: bookModel.icon!) titleLabel!.text = bookModel.title detailLabel!.text = bookModel.detail priceLabel!.text = bookModel.price } }
-
-
Controller 控制器的創建
-
Objective-C
// ViewController.m // Modal 模型處理 // 聲明數據源 @property (nonatomic, strong) NSArray *myDataArray; // 加載模型數據 - (NSArray *)myDataArray { if (_myDataArray == nil) { NSArray *array = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"bookData" ofType:@"plist"]]; NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:array.count]; [array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { // KVC BookModel *bookModel = [BookModel bookModelWithDict:obj]; // 使用 Modal 數據模型初始化數據源數組 [arrayM addObject:bookModel]; }]; _myDataArray = [arrayM copy]; } return _myDataArray; } // View 視圖處理 UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 20, self.view.bounds.size.width, self.view.bounds.size.height - 20)]; myTableView.delegate = self; myTableView.dataSource = self; [myTableView registerNib:[UINib nibWithNibName:@"BookCell" bundle:nil] forCellReuseIdentifier:@"BookCell"]; [self.view addSubview:myTableView]; - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return [self.myDataArray count]; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ return 80; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ BookCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BookCell" forIndexPath:indexPath]; // 從 Modal 數據模型中取出數據更新 View 的內容 cell.bookModel = self.myDataArray[indexPath.row]; return cell; }
-
Swift
// ViewController.swift // Modal 模型處理 var myDataArray:NSMutableArray! myDataArray = NSMutableArray() for bookInfoDic in NSArray(contentsOfFile: NSBundle.mainBundle().pathForResource("bookData", ofType: "plist")!)! { let bookModel = BookModel() // KVC bookModel.setValuesForKeysWithDictionary(bookInfoDic as! Dictionary) // 使用 Modal 數據模型初始化數據源數組 myDataArray.addObject(bookModel) } // View 視圖處理 let myTableView:UITableView = UITableView(frame: CGRectMake(0, 20, self.view.bounds.size.width, self.view.bounds.size.height - 20)) myTableView.delegate = self myTableView.dataSource = self myTableView.registerNib(UINib(nibName: "BookCell", bundle: nil), forCellReuseIdentifier: "BookCell") self.view.addSubview(myTableView) func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return myDataArray.count } func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { return 80 } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("BookCell", forIndexPath: indexPath) as! BookCell let bookModel:BookModel = myDataArray.objectAtIndex(indexPath.row) as! BookModel cell.configWithModel(bookModel) // 從 Modal 數據模型中取出數據更新 View 的內容 return cell }
-