你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 基於面向協議MVP模式下的軟件設計-iOS篇

基於面向協議MVP模式下的軟件設計-iOS篇

編輯:IOS開發基礎

1.jpg

  • 傳統模式下的開發

  • MVC

  • MVVM

  • 基於面向協議MVP的介紹

  • MVP實戰開發

說在前面:
相信就算你是個iOS新手也應該聽說過MVC的,MVC是構建iOS App的標准模板。隨著時間的推移,在iOS平台上MVC也逐漸開始面臨著越來越多的問題,最近又開始流行MVVM,MVVM使由MVC衍生而來,MVVM作為一種新的開發模式和響應式編程相結合用來解決一部分業務場景等,今天,我要介紹給大家的是一個新的方式來架構你的App: Model-View-Protocol,暫時可以理解為是基於協議的一種設計規范,拿出你的流行語bingo card,因為我們即將進行一次范式轉變。

一、軟件設計鼻祖MVC

1.1、MVC

第一次聽到MVC這個名詞是在C#中,相信對於MVC大家都已經很熟悉了,作為一種軟件設計模式,MVC這個概念已經誕生好多年了。

如果你已經開發一段時間的iOS應用,你一定聽說過Model-View-Controller,在iOS開發中Apple從一開始就給我們引入這一理念,相信這個名詞大家都不陌生。

模型-視圖-控制器(Model-View-Controller,MVC)是Xerox PARC在20世紀80年代為編程語言Smalltalk-80發明的一種軟件設計模式,至今已廣泛應用於用戶交互應用程序中。在iOS開發中MVC的機制被使用的淋漓盡致,充分理解iOS的MVC模式,有助於我們程序的組織合理性。

Model-View-Controller

MVC

模型對象

模型對象封裝了應用程序的數據,並定義操控和處理該數據的邏輯和運算。例如,模型對象可能是表示商品數據list。用戶在視圖層中所進行的創建或修改數據的操作,通過控制器對象傳達出去,最終會創建或更新模型對象。模型對象更改時(例如通過網絡連接接收到新數據),它通知控制器對象,控制器對象更新相應的視圖對象。

視圖對象

視圖對象是應用程序中用戶可以看見的對象。視圖對象知道如何將自己繪制出來,並可能對用戶的操作作出響應。視圖對象的主要目的,就是顯示來自應用程序模型對象的數據,並使該數據可被編輯。盡管如此,在 MVC 應用程序中,視圖對象通常與模型對象分離。

在iOS應用程序開發中,所有的控件、窗口等都繼承自 UIView,對應MVC中的V。UIView及其子類主要負責UI的實現,而UIView所產生的事件都可以采用委托的方式,交給UIViewController實現。

控制器對象

在應用程序的一個或多個視圖對象和一個或多個模型對象之間,控制器對象充當媒介。控制器對象因此是同步管道程序,通過它,視圖對象了解模型對象的更改,反之亦然,控制器主要負責數據的傳遞解耦等工作。控制器對象還可以為應用程序執行設置和協調任務,並管理其他對象的生命周期。

控制器對象解釋在視圖對象中進行的用戶操作,並將新的或更改過的數據傳達給模型對象。模型對象更改時,一個控制器對象會將新的模型數據傳達給視圖對象,以便視圖對象可以顯示它。

M和V永遠不能相互通信,只能通過控制器傳遞。控制器可以直接與Model對話(讀寫調用Model),Model通過通知或者KVO機制與控制器間接通信。控制器可以直接與View對話,通過outlet,直接操作View,outlet直接對應到View中的控件,View通過action向控制器報告事件的發生(如用戶的點擊事件)。控制器是View的直接數據源(數據很可能是控制器從Model中取得並經過加工了)。控制器是View的代理(delegate),以同步View與Controller。

MVC是一個用來組織代碼的權威范式,也是構建iOS App的標准模式。Apple甚至是這麼說的。在MVC下,所有的對象被歸類為一個model,一個view,或一個controller。Model持有數據,View顯示與用戶交互的界面,而View Controller調解Model和View之間的交互。然而,隨著模塊的迭代我們越來越發現MVC自身存在著很多不足。因此,MVVM從其他應用而出,在 iOS中從此我們完全將業務邏輯加以區分並使用這套思想。

MVC在現實應用中的不足

在上圖中,view將用戶交互通知給控制器。view的控制器通過更新Model來反應狀態的改變。Model(通常使用Key-Value-Observation)通知控制器來更新他們負責的view。大多數iOS應用程序的代碼使用這種方式來組織。

愈發笨重的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很難測試,不管是手動測試或是使用單元測試,因為有太多可能的狀態。將代碼分解成更小的多個模塊通常是件好事。

太過於輕量級的Model

太過於輕量級的Model,早期的Model層,其實就是如果數據有幾個屬性,就定義幾個屬性,ARC普及以後我們在Model層的實現文件中基本上看不到代碼( 無需再手動管理釋放變量,Model既沒有復雜的業務處理,也沒有對象的構造,基本上.m文件中的代碼普遍是空的);同時與控制器的代碼越來厚重形成強烈的反差,這一度讓人不禁對現有的開發設計構思有所懷疑。

遺失的網絡邏輯

蘋果使用的MVC的定義是這麼說的:所有的對象都可以被歸類為一個Model,一個view,或是一個控制器。就這些。那麼把網絡代碼放哪裡?和一個API通信的代碼應該放在哪兒?
你可能試著把它放在Model對象裡,但是也會很棘手,因為網絡調用應該使用異步,這樣如果一個網絡請求比持有它的Model生命周期更長,事情將變的復雜。顯然也不應該把網絡代碼放在view裡,因此只剩下控制器了。這同樣是個壞主意,因為這加劇了厚重控制器的問題。

那麼應該放在那裡呢?顯然MVC的3大組件根本沒有適合放這些代碼的地方。

較差的可測試性

MVC的另一個大問題是,它不鼓勵開發人員編寫單元測試。由於控制器混合了視圖處理邏輯和業務邏輯,分離這些成分的單元測試成了一個艱巨的任務。大多數人選擇忽略這個任務,那就是不做任何測試。

上文提到了控制器可以管理視圖的層次結構;控制器有一個“view”屬性,並且可以通過IBOutlet訪問視圖的任何子視圖。當有很多outlet時這樣做不易於擴展,在某種意義上,最好不要使用子視圖控制器(child view controller)來幫助管理子視圖。

在這裡有多個模糊的標准,似乎沒有人能完全達成一致。貌似無論如何,view和對應的controller都緊緊的耦合在一起,總之,還是會把它們當成一個組件來對待。Apple提供的這個組件一度以來在某種程度誤導了大多初學者,初學者將所有的視圖全部拖到xib中,連接大量的IBoutLet輸出口屬性,都是一些列問題。

二、大劍之初MVVM

在經歷了一大堆吐槽之後,誕生了MVVM(一個高大尚牛逼哄哄的名詞,從此又多了一種人,你懂MVVM ?如果你的回答是否,瞬間被鄙視一把)。

新思維

其實MVVM據說最早在微軟的.NET平台中出現過(具體什麼背景,什麼原因就不一一介紹了,還是要感謝偉大的.NET平台工程師,造劍不如造經,世間萬道皆不離其宗),無論是是MVVM還是MVC我們無需堅持反對或者迷戀於它。在MVVM中他的設計思路和MVC很像。它正式規范了視圖和控制器緊耦合的性質,並引入新的組件。

Model-View-ViewModel
在理想的世界裡,MVC也許工作的很好。然而,我們生活在真實的世界。既然我們已經詳細說明了MVC在典型場景中的問題,那讓我們看一看一個可供替換的選擇:Model-View-ViewModel。

在MVVM裡,view和view controller正式聯系在一起,我們把它們視為一個組件。視圖view仍然不能直接引用模型Model,當然controller也不能。相反,他們引用視圖模型view Model。

view Model是一個放置用戶輸入驗證邏輯,視圖顯示邏輯,發起網絡請求和其他各種各樣的代碼的極好的地方。有一件事情不應歸入view Model,那就是任何視圖本身的引用。view Model的概念同時適用於於iOS和OS X。(換句話說,不要在view Model中使用 #import UIKit.h)

由於展示邏輯(presentation logic)放在了view Model中(比如Model的值映射到一個格式化的字符串),視圖控制器本身就會不再臃腫。當你開始使用MVVM的最好方式是,可以先將一小部分邏輯放入視圖模型,然後當你逐漸習慣於使用這個范式的時候再遷移更多的邏輯到視圖模型中。

以我的經驗,使用MVVM會輕微的增加代碼量,但總體上減少了代碼的復雜性。這是一個劃算的交易。

回過頭再來看MVVM的圖示,你會注意到我使用了模糊的動詞“notify”和“update”,而沒有詳細說明該怎麼做。你可以使用KVO,就像MVC那樣,但這很快就會變得難以管理。事實上,使用ReactiveCocoa會是更好的方式來組織各個部分。

關於怎麼結合ReactiveCocoa來使用MVVM的信息,可以閱讀開源app。你也可以閱讀我的關於ReactiveCocoa和MVVM的書。

關於MVVM的具體細節此處就不多詳細介紹,此處重在做對比分析,如果了解的可以去了解相關資料。

三、基於面向協議MVP的介紹

曾經有無數個人總喜歡問我你們的iOS采用什麼樣的架構,其實每次被問到這樣的問題,不是瞬間被萌了,就是想自己問自己iOS也有架構??

上文提到了MVC、MVVM,真實的業務場景中,如果場景的邏輯異常復雜,在反復的迭代中仍會出現各式各樣的問題。真對MVVM我個人理解主要是將原來Controller中處理數據邏輯的代碼統一歸到一個新的class(viewModel)中去,更甚之網絡請求等工作全部從Controller移到viewModel。剛一開始總覺的怪怪的。現階段客戶端開發越來越進入一個2.0的階段,早期的app功能都相對比較簡單,無論是從界面還是從業務邏輯上給人的感覺都是簡潔實用,這中間包括UI的設計、功能的設計、產品的設計定位等。隨著行業的深入,用戶的過渡依賴移動端最終導致業各式各樣的業務更加依賴客戶端,這就導致客戶端的開發不得不向PC端靠齊,在版本的反復迭代中業務場景變的愈發不盡人意,仿佛又回到了軟件設計的早期。

在傳統軟件領域,從MVC的誕生主要是為了解決軟件界面的行為的分離,在復雜的業務場景內會進一步區分業務邏輯場景的分離,這些手段的最終的目的都是盡最大限度的降低整個場景的藕合度,使其達到分離的目的,模塊與模塊最終得到獨立,將整個場景化整為零,最終使每個模塊在一個零上工作,這對於無論是軟件的開發還是後續的維護、以及使用普遍遵循這個原則,現有的模式大概產生了相關的類似架構。

傳統web架構裡面是這樣解決的 :

service
  • web段以及其他業務層負責從接口層獲取數據並執行自己的邏輯

  • service層為外部提供接口

  • DTO從負責從DB鏈接並進行數據讀寫操作

  • DB層(物理機負責數據存儲)

現有客戶度一度采用下面的模式:

MVC

客戶端通過service拿到json 數據,然後通過MVC的結構展示到UI界面上,在iOS中一直流行MVC的開發模式,通過與傳統開發模式對比可以發現,其實
service層-客戶端交互與服務端service服務滿足外部業務場景無非是兩個互逆的過程(一個輸出層,一個輸入層,都是為了更好的滿足的下一步的業務需求,一個是將原始數據邏輯話,一個是將獲得邏輯數據存檔並且展示到用戶面前)。service層根據具體的業務場景提供對應的數據服務,service根據不同的業務場景通過DTO層拿到對應
的數據然後組織好數據提供給外界(service 層負責將原始物理數據轉換成對應的邏輯數據提供給外界)。

相反,客戶端通過網絡層拿到對應的網絡數據繪制到對應的View上,但是實際的開發過程中,網絡數據與真實客戶端使用場景也是有一定的差距,MVVM層將對應的
一部分邏輯處理移植到了ViewModel中,這並沒有從根本上解決問題,無非是將代碼做了一份對應的copy轉移,並沒有從根本上達到邏輯分層的概念。相反MVP模
式恰好解決了這一難題,MVP模式衍生於傳統service架構,針對不同的業務場景圖供對應的匹配的抽象service服務,客戶端拿到網絡數據後未達到指定的目的,
為滿足相同抽象邏輯的業務場景,在客戶端網絡層與Model層之間加一協議層,Model層實現整個協議層,之後在基於MVC的結構下將一概相同層次的
業務場景繪制解釋到對應的View上。

MVP
  • M : 邏輯Model層

  • V : 視圖層

  • P : protocol協議層

Model層類似於MVVM的ViewModel,主要負責存儲抽象邏輯數據,另外Model層主還有部分工作實現對應的協議層協議,提供協議對應的各種屬性以及服務。Model經過協議層抽象約束,最後Model被抽象成具有統一抽象邏輯的業務場景,最終Model層在講數據交付整個MVC結構繪制展示的時間,我們可以按照同一套抽象的邏輯標准去執行。

在傳統的web層面,為了滿足各式各樣的業務邏輯場景服務,最紅我們實現軟件羅傑的層次的分離,誕生了service服務這個概念(service就類似一個標准尺寸的水龍頭出口,只要對應的水龍頭都按照這樣的規則來生產,service就能夠滿足格式各樣的業務場景,極大的解決的傳統軟件服務業務場景層次的一系列難題);相同的原理在客戶端同樣可以使用,為了滿足客戶端MVC結構層裡面的穩定,避免各式各樣的業務場景迭代插入不同的邏輯,避免最終軟件危機的產生,我們采用追加協議層的模式來滿足這一目的。

遍觀整個軟件開發,從早期的軟件開發,到後來軟件生產管理的危機,軟件開發模式一步步的確立,軟件行業的每個階段都是一個裡程碑。這世間沒有相對完美獨到的設計法則,但是亘古不變永遠只有一個那就是軟件的開發更佳面相生產化、規范化、更加的利於可維護化。一直以來我本人並不特別的注重軟件的設計一定、必須按照某種規則來做,畢竟不同的人、不同的業務場景、不同的工程師總有不同的實際境況,站在一個開發工程師的角度來說我並不固執於都按照固定的規則來(比如說你必須按照某個模式來做,必須用MVVM來做;必須用ReactCocoa信號型機制來做...)。相反我個人認為太過於固執只不過某些人的一廂情願的罷了。相反我覺得因地制宜、應運而生豈不更加快哉,設計不拘於模式,更多時間更是不局限於思考。無論是MVVM、MVP哪一個不是脫胎於MVC,這個世間萬變不離其宗,萬千功法始終都離不開一部最終的母經。

四、MVP實戰開發

說了這麼多,下面上實戰例子。

大概描述一下業務場景,作為電商app,我們希望在原生的基礎上開發一套定制的可控、可配、可維護的通用型原生模版(至於說的這麼靈活 有多麼的好,為啥不用H5、ReactNative,這個問題不要來問我,產品狗們讓做原生,程序員只能執行)。大概是這樣一個場景,可以配置的樓層樣式多達十幾種(至少目前已經有十幾種,以後可能會更多);每種可配置樓層樣式是多元的,外觀長相不一,數據格式也不盡相同但有部分類同;要求後台CMS配置界面配置法則有共同相似之處;要求每種樣式樓層處理事件記憶跳轉不盡相同;最可恨的頁面已經很長了以後會源源不斷加入新的模版。
考慮到長遠,這樣的復雜樓層,如果仍舊按照傳統的模式來做,問題會很多,數據無法統一、無法統一繪制、無法統一處理。具體場景相信大家應該理解了。

上設計思路

潛在問題

  • server段需要針對不同的樓層場景下發不同的數據 數據結構不盡相同

  • 模版樓層樣式不盡相同 可能對應多種View

  • 多種View與多種數據結構的解釋解耦問題

  • 多種業務場景用戶操作邏輯處理問題

  • 樓層過於復雜 Controller代碼大爆炸

邏輯建模分析

  • 暫時可以將每種模版樓層的整體數據作為一個容器Modle,主要負責該樓層的整體數據調度

  • 將每種樓層公有的屬性以及內容抽象出來放入一個容器父類Container,然後將不同模版特有的屬性放在子模版派生Model中,作為派生屬性

  • 對准一個容器類,我可以將每種容器Model的使用法則抽象總結歸納(1、樓層是否有Header,是否要吸頂;2、該樓層具體要由什麼樣的View模版去繪制; 3、樓層內容是繪制在單個section單個cell中還是繪制在多行上; 4、每個樓層的元素點擊跳轉處理等). 我們將容器這塊作為一個數據源概念最終抽象出一套可供外界獲取數據的Interface(Protocol)

  • 當我們拿到樓層數據後在父容器基礎上做子樓層的派生,我們要求派生容器去實現上述父容器的Protocol,通過協議我們可以知道具體的繪制的目標,以及要繪制的元素個數等,最終達到一個目的,
    將每個樓層的數據裝配在我們定義好的一個適配器容器內,然後通過協議給外界提供一套統一的操作入口,之後我們才用統一的操作方式操作容器,最終實現一個容器對應一個樓層。

  • Render 協議,在這個我們對准每個要具體繪制到UI上的Model,我們統一讓其實現Render協議,通過適配器容器我們我們拿到具體要繪制的目標,
    目標繪制題都實現了Render協議,在Render協議我們可以拿到具體當前Model將由哪個具體的Cell去呈遞。在每個繪制目標題內由Model決定
    當前內容由什麼樣式的cell模版去繪制。我們把所有的樓層數據處理邏輯壓在適配器容器內,再將Model的繪制目標都交由Model自己決定。

  • 實現上述目標後,在ViewController層面,我們看到的只有一個實現了適配器協議的Model數組,在 table的繪制過程我們通過操作一個
    id<適配器protocol> 類型的Model對象, 拿到這個具體的索引對應的對象後,通過內部已經實現的協議我們很快的拿到下一個要繪制的目標Model
    然後再拿到具體的Cell模版的Identifier,然後從tableview中取到當前Identifier對應的cell模版,傳入數據最後返回一個cell。

    這個地方我們先定義了一個適配容器協議,以及一個父容器類,我們將樓層公有屬性放在父類中,將派生容器子類(具體的樓層Model)實現一個抽象協議。

MVP模式的原則是,在service層提供一大堆不盡相同的業務場景之後,我們將這一系列數據全部抽象歸納,通過定制一套標准的protocol協議,讓不同業務場景的都去實現這一協議,最終將數據全部裝配、拼裝成一套具有相同調度規則的統一編制話數據Model,之後我們采用id的標准去操作數據。

MVP除了將數據邏輯完全鎮封的各自的Model,同時將那些Model需要繪制,哪些Model需要校驗, 哪些Model需要接受處理點擊Action這些邏輯全部由Model自己來決定,Controller只作為一個粘合性的模版,Controller只處理一批具有共性的范型類數據,至於具體的操作操作毫不關心。

MVP面相的更多的是在MVC上層與service之間追加了一層協議層,我們認為通過協議層處理過的數據是暫時可以客戶端場景使用的數據,在數據到達MVC我們針對數據進行再加工、再構造處理,這一切全部在容器內操作。這一點完全與別的設計模式相反。

MVP並不是讓用戶在Model打上網絡請求操作、在Model層執行[self.navigationController pushViewController:*]等這些操作,其實相對大多人來說對於部分對象生命周期長短問題還是很在乎,所以在處理TemplateActionProtocol協議的時間,MVP只是對准Action做了一層抽象封裝。通過實現TemplateActionProtocol的Model會產生一個Action對象,最終通過block的調用鏈傳回控制器中,我們在控制器中統一做handler處理。

MVP我們最初設計目的就是為了強調一個裝配概念,如果發生了業務場景的追加,控制器我不會改動其中的代碼,只需要將新數據追加成相同批次的ViewModel,然後配置進容器,之後控制器不做任何修改就可以滿足需求了。通過具體的剝離、抽取我們成功了的最大限度的剝離了控制器,滿足了輕量級Controller這一概念。

MVP與傳統軟件相比,在設計這一點的時間我們完全借鑒了傳統軟件的思維模式,Java平台的service設計模式、三層架構這些設計規范都相對做了一些對比分析,最終得出了MVP這一理念。

分析場景完畢,下面來分析一個模版的例子來說命一切吧!!

Protocol 設計

Template 協議

  • -TemplateRenderProtocol           //任何一個具體繪制到cell上的Model都需要實現該協議

  • -TemplateSorbRenderProtocol   //樓層的header如果要吸頂需要使用該協議替代基本的Render協議

  • -TemplateActionProtocol        //具有Action 操作的Model需要實現,返回一個通用的Action對象

  • -TemplateCellProtocol            //整個體系中所有的Cell統一實現該協議

    • TemplateValidationProtocol   //- 處理一部分認證校驗數據

ViewController

    • 樓層顯示統一交與Model層定制

    • VC中生成ActionModel,跳轉邏輯全部應用於Action協議層,ViewController實現ActionManager 代理,作為回調處理

    • 特定屬性處理邏輯放在分類內

    • 網絡層調用扔保持在ViewController,這一點與傳統保持相似,有利於結構分明(優於市面上的所謂MVVM)

//TemplateRenderProtocol.h
@protocol TemplateRenderProtocol @required
- (NSString *)floorIdentifier;

@end

//TemplateSorbRenderProtocol.h
@protocol TemplateSorbRenderProtocol - (NSString *)headerFloorIdentifier;
- (id )headerFloorModelAtIndex:(NSInteger)index;

@end


//TemplateActionProtocol.h
 */
@protocol TemplateActionProtocol @optional

- (TemplateAction *)jumpFloorModelAtIndexPath:(NSIndexPath *)indexPath;

@end


//TemplateCellProtocol.h
@protocol TemplateBaseProtocol;

typedef void (^TapBlock) (NSIndexPath* index);

@protocol TemplateCellProtocol @optional
+ (CGSize)calculateSizeWithData:(id)data constrainedToSize:(CGSize)size;

- (void)processData:(id )data;

- (void)tapOnePlace:(TapBlock) block;

@end

Model設計

//  TemplateContainerModel.h

/**
 *  容器概念
 */
@protocol TemplateContainerProtocol @required

- (NSInteger)numberOfChildModelsInContainer;

- (id )childFloorModelAtIndex:(NSInteger)index;

@end

@class TemplateChannelModel;
@interface TemplateContainerModel : NSObject//netList
@property (nonatomic,strong) NSNumber                 *identityId;
@property (nonatomic,strong) NSString                 *pattern;
@property (nonatomic,strong) TemplateFHeaderModel     *fheader;
@property (nonatomic,strong) NSArray                  *itemList;
@property (nonatomic,strong) TemplateJumpModel        *jump;
@property (nonatomic,strong) TemplateMarginModel      *margin;
//other add
@property (nonatomic,assign) TemplateChannelModel     *channelModel;
@end

下面的就先引用一個具體的業務場景吧,頂部banner樓層,每個大的樓層都是一個容器Model,是繼承於父容器,並且會適當重寫父類協議以及方法

//TemplateFloorFocusModel.h

//此處,banner是多個對象繪制成輪播的樣式,整體是繪制在同一個cell上的,所以TemplateFloorFocusModel首先是一個容器類,是具有數據源的
功能,但是他又是一個繪制目標Model,TemplateFloorFocusModel實現了Render協議,就決定這個接下來會將TemplateFloorFocusModel繪制到UI界面上(如果此處的容器存儲的是一個section下的list形式,容器類就無需實現render協議,只需要將list 中的Model實現render協議即可)

@interface TemplateFloorFocusModel : TemplateContainerModel@property (nonatomic,assign) NSNumber *width;
@property (nonatomic,assign) NSNumber *height;

@end


//TemplateFloorFocusModel.m

@implementation TemplateFloorFocusModel

+ (NSDictionary *)mj_replacedKeyFromPropertyName
{
    return @{
             @"itemList" : @"picList"
             };
}

+ (NSDictionary *)mj_objectClassInArray
{
    return @{
             @"itemList" : @"TemplatePicModel"
             };
}

//pragma mark - TemplateContainerProtocol

- (NSInteger)numberOfChildModelsInContainer
{
    NSUInteger rows = 0;
    if (self.margin) rows++;
    if (self.itemList) rows++;
    return rows;
}

//(如果此處的容器存儲的是一個section下的list形式,此處返回一個實現render協議的Model即可)
- (id )childFloorModelAtIndex:(NSInteger)index
{
    if ((self.margin)&&(index+1) == [self numberOfChildModelsInContainer])
        return self.margin;  //最後一行
    return self;
}

//pragma mark - TemplateActionProtocol

- (TemplateAction *)jumpFloorModelAtIndexPath:(NSIndexPath *)indexPath
{
    NSUInteger position = [indexPath indexAtPosition:0];
    if (position < self.itemList.count) {

        TemplatePicModel *picModel = self.itemList[position];
        TemplateJumpAction *action = [[TemplateJumpAction alloc] init];
        action.jumpToType = TemplateJumpToActivityM;
        action.jumpToUrl = picModel.jump.url;
        action.eventId = @"GeneralChannel_BannerPic";
        return action;
    }
    return nil;
}

//pragma mark -  TemplateRenderProtocol
- (NSString *)floorIdentifier
{
    return @"TemplateFocusCell";
}

View 設計

View設計此處我們才用方式依舊是將Cell作為模版,將對應的視圖邏輯統一放在一個UIViewSubView中, 之後在Cell中將View直接add到cell.ContentView上。

針對焦點圖cell TemplateFocusCell我們有一個TemplateFocusView來對應,下面看下代碼設計

TemplateFocusCell

// TemplateFocusCell
@interface TemplateFocusCell : UITableViewCell@end

@interface TemplateFocusCell (){
    TemplateFocusView *_focusView;
}

@property (nonatomic,strong) id  data;

@end
@implementation TemplateFocusCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];

    if (self) {
        _focusView = [[TemplateFocusView alloc] init];
        _focusView.translatesAutoresizingMaskIntoConstraints = NO;
        [self.contentView addSubview:_focusView];
       }
    return self;
}

- (void)processData:(id )data
{
    if([data isKindOfClass:[TemplateFloorFocusModel class]])
    {
        self.data = data;
        [_focusView processData:(id )data];
    }
}

+ (CGSize)calculateSizeWithData:(id)data constrainedToSize:(CGSize)size
{
//    id model = data;
    CGSize curSize = CGSizeMake(ScreenWidth, 110);
    return curSize;
}
- (void)tapOnePlace:(TapBlock) block
{
    [_focusView setTapBlock:block];
}

TemplateFocusView

@interface TemplateFocusView : UIView@end

@interface TemplateFocusView (){
    UIPageControl *_pageControl;
    iCarousel     *_scrollView;
}

@property (nonatomic,strong) TemplateFloorFocusModel *focusModel;
@end

@implementation TemplateFocusView

- (instancetype)init
{
    self = [super init];

    if (self)
    {
        _scrollView = [[iCarousel alloc] init];
        _scrollView.delegate = self;
        _scrollView.dataSource = self;
        _scrollView.type = iCarouselTypeLinear;
        _scrollView.pagingEnabled = YES;
        _scrollView.bounceDistance = 0.5;
        _scrollView.decelerationRate = 0.5;
        _scrollView.clipsToBounds = YES;
        _scrollView.translatesAutoresizingMaskIntoConstraints = NO;
        [self addSubview:_scrollView];

        _pageControl = [[UIPageControl alloc] init];
        _pageControl.translatesAutoresizingMaskIntoConstraints = NO;
        [self addSubview:_pageControl];

        [_scrollView mas_makeConstraints:^(MASConstraintMaker *make){
            make.edges.equalTo(self).insets(UIEdgeInsetsZero);
        }];

        [_pageControl mas_makeConstraints:^(MASConstraintMaker *make){
            make.bottom.mas_equalTo(@(5));
            make.centerX.equalTo(self);
        }];
    }
    return self;
}

+ (CGSize)calculateSizeWithData:(id)data constrainedToSize:(CGSize)size
{
    return size;
}

- (void)processData:(id )data
{
    self.focusModel = (TemplateFloorFocusModel *)data;
    _pageControl.numberOfPages = self.focusModel.itemList.count;
    [_scrollView reloadData];

    [self layoutIfNeeded];
}

//pragma mark -
- (NSInteger)numberOfItemsInCarousel:(iCarousel *)carousel
{
    return _focusModel.itemList.count;
}

- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view
{
    UIImageView *imageView = nil;
    if (!view) {
        imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, ScreenWidth, ScreenWidth/2)];
        imageView.contentMode = UIViewContentModeScaleAspectFit;
        }else{
        imageView = (UIImageView *)view;
    }

    TemplatePicModel *model = self.focusModel.itemList[index];
    [imageView setImageWithURL:[NSURL URLWithString:model.img]];
    return imageView;
}

- (CGFloat)carousel:(iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value
{
    if (option == iCarouselOptionWrap)
    {
        return YES;
    }
    return value;
}

- (void)carouselDidEndScrollingAnimation:(iCarousel *)carousel
{
    NSInteger index = _scrollView.scrollOffset;

    [_pageControl setCurrentPage:index];
}

- (void)carousel:(iCarousel *)carousel didSelectItemAtIndex:(NSInteger)index
{
    NSIndexPath *indexPath = [NSIndexPath indexPathWithIndex:index];
    if (_tapBlock) {
        _tapBlock(indexPath);
    }
}

從View層可以看到,我們仍舊遵循以往的模式,將cell高度的計算,最終放在View中來完成(此處我們並沒有Model化,而是仍舊遵循大家的習慣,具體的高度根據具體的視圖場景來控制),看到此處的計算高度的方法,接下來的問題就不多說了....

Controller 設計

在做完以上的一些列的邏輯化抽象工作以後,從新回到控制器層面,此時應該是大松了一口氣了,到目前為止,我們一大堆系列的工作都已經做完了,只是還有一點失望的感覺是暫時還沒看到是否真的有卵用,這就好比十年鑄一劍,繼而十年在磨一劍,看不到成效始終覺得心中似有虧欠。

到目前為止,我們在控制器層面能做的僅有的是范型數據的操作,已經安全沒有邏輯了,邏輯全部壓入了Model,下面就看下控制器層面的邏輯:

//處理action
- (TapBlock)tapBlockForModel:(id)model
{
    __weak typeof (self) weakself = self;
    return ^(NSIndexPath * indexPath){
        if ([model conformsToProtocol:@protocol(TemplateActionProtocol)]) {
            TemplateAction *action = [(id)model jumpFloorModelAtIndexPath:indexPath];
            [weakself.handler handlerAction:action];
        }
    };
}

//注冊cell

[self.tableView registerClass:[TemplateFocusCell class] forCellReuseIdentifier:@"TemplateFocusCell"];


//tableView 代理實現
//pragma mark - UITableViewDataSource,UITableViewDelegate

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.floorModel.floors count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    TemplateContainerModel *list = self.floorModel.floors[section];
    return [list numberOfChildModelsInContainer];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    id  model = [self.floorModel rowModelAtIndexPath:indexPath];
    UITableViewCell  * cell = [tableView dequeueReusableCellWithIdentifier:[model floorIdentifier]];
    [cell processData:model];
    [cell tapOnePlace:[self tapBlockForModel:model]];
    if(!cell){
        return [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
    }else{
        return (UITableViewCell *)cell;
    }
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{

    id   floor = [self.floorModel rowModelAtIndexPath:indexPath];
    if ([floor respondsToSelector:@selector(floorIdentifier)]) {
        NSString *cellIdentifier = [floor floorIdentifier];
        Class viewClass = NSClassFromString(cellIdentifier);
        CGSize size = [viewClass calculateSizeWithData:floor constrainedToSize:CGSizeMake(tableView.frame.size.width, 0.0)];
        return size.height;
    }
    return 0;
}

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
    id  floor = self.floorModel.floors[section];
    if ([floor conformsToProtocol:@protocol(TemplateSorbRenderProtocol)]) {
        NSString *headerIdentifier = [floor headerFloorIdentifier];
        if (headerIdentifier) {
            Class viewClass = NSClassFromString(headerIdentifier);
            CGSize size = [viewClass calculateSizeWithData:floor constrainedToSize:CGSizeMake(tableView.frame.size.width, 0.0)];
            return size.height;
        }
    }

    return 0;
}

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
    id  floor = self.floorModel.floors[section];
    if ([floor conformsToProtocol:@protocol(TemplateSorbRenderProtocol)]) {
        id headerModel = [floor headerFloorModelAtIndex:section];
        if (headerModel) {
            NSString *identifier = [headerModel headerFloorIdentifier];
            UIView  *headerView = (UIView  *)[tableView dequeueReusableHeaderFooterViewWithIdentifier:identifier];
            [headerView processData:floor];
            return headerView;
        }
    }
    return nil;
}

至此,控制器只剩下以上操作,相對來說已經最大限度的梳理了邏輯,將所有的邏輯壓入Model,如果服務端新增了新型的業務場景的數據,依舊可以通過協議層的適配,將數據最終的組裝上述模式,最後直接拿來使用,如果需要修改對應的View,直接可以在Model內修改具體的將要渲染的View的名字即可,這些工作都跟控制器層沒有任何關系。

在Action協議中,具有Action操作的Model會在使用過程中實現TemplateActionProtocol這一協議,在事件處理的時間會拋出這樣一個ActionModel,之後此處我們會直接對Action對象handler操作,此處並沒有控制器層UI界面的操作,這一點遵循了設計模式中的命令行模式(這一點原理脫胎於於strus框架中XWork框架,將控制器與UI工作無關的內務以命令行的模式跑出來,放在別的一個代理中去完成,這樣能夠最大的限度的做到對控制器層面的瘦身工作)。

說到控制器瘦身工作,iOS常用的大概是就是Category了,將部分全局型屬性、邏輯放在對應的分類裡面,有助於邏輯的抽離、代碼的分割。

MVPDemo

容器模式 適配器模式 命令行模式

MVVM

說說Android的MVP模式

用Model-View-ViewModel構建iOS App

淺談iOS中MVVM的架構設計與團隊協作

Model-View-Controller

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved