iOS的MVC框架之控制層的構(gòu)建(下)

在我的iOS的MVC框架之控制層的構(gòu)建(上)一文中介紹了一些控制層的構(gòu)建方法褂始,而這篇文章則繼續(xù)對一些方法進(jìn)行展開討論。MVC被眾多開發(fā)者所詬病的C層的膨脹缔恳,究其原因不外乎有如下幾點(diǎn):

  1. 所有視圖的構(gòu)建和布局代碼都在控制器中完成宝剖。有很多同學(xué)不喜歡系統(tǒng)提供的Storyboard和XIB來構(gòu)建視圖,而是喜歡通過代碼的形式來完成視圖界面布局歉甚,并且通常這部分代碼都集中在loadView或者viewDidLoad或者通過懶加載的形式分散在各處万细。通過代碼來構(gòu)建和布局視圖的代碼量有可能會超過您視圖控制器總代碼量的50%。
  2. 對服務(wù)端的請求纸泄,往往就是包裝了一層非常薄的請求層赖钞,通常稱之為APIService。 這部分代碼只是簡單封裝了對服務(wù)端URL的請求聘裁,同時(shí)通過一些報(bào)文轉(zhuǎn)數(shù)據(jù)模型的第三方框架直接將報(bào)文轉(zhuǎn)化為數(shù)據(jù)模型并通過異步回調(diào)的形式回吐給控制器或者視圖雪营。APIService的簡單實(shí)現(xiàn)卻增加了控制器的負(fù)荷,導(dǎo)致控制器除了要構(gòu)建視圖并且請求網(wǎng)絡(luò)服務(wù)外還要擔(dān)負(fù)非常多的一部分業(yè)務(wù)邏輯的實(shí)現(xiàn)衡便。
  3. 對于一些復(fù)雜展示邏輯的功能界面沒有進(jìn)行合理拆解和有效設(shè)計(jì)導(dǎo)致所有代碼都在一個(gè)視圖控制器內(nèi)完成献起,從而導(dǎo)致控制器膨脹臃腫洋访。
  4. 在應(yīng)用中最多使用的UITableView以及UITableViewCell中的數(shù)據(jù)更新的處理機(jī)制使用不恰當(dāng)導(dǎo)致delegate中的方法實(shí)現(xiàn)異常的復(fù)雜,尤其是那些復(fù)雜的UITableViewCell的更新處理不得當(dāng)導(dǎo)致代碼混亂不堪。

可以看出框架本身沒有問題,問題在于使用的人不了解或者不恰當(dāng)?shù)脑O(shè)計(jì)思想導(dǎo)致問題出現(xiàn)了免猾。當(dāng)出現(xiàn)問題時(shí)我們首先應(yīng)該反思的是自己哪里不對而不是去怪別人哪里不對。(這個(gè)雞湯撒得真LOW!!) 怎么解決上面所說的導(dǎo)致C層膨脹的幾個(gè)問題呢汁展?這也是這篇文章所要重點(diǎn)介紹的。

不同代碼的構(gòu)建時(shí)機(jī)

控制器類是一個(gè)功能的調(diào)度總控室厌殉,而且他還通過模板方法的設(shè)計(jì)模式提供給了我們在控制器的生命周期內(nèi)各階段事件發(fā)生時(shí)的處理回調(diào)食绿。比如控制器構(gòu)建時(shí)(init)、 視圖構(gòu)建時(shí)(loadView)公罕、視圖構(gòu)建完成時(shí)(viewDidLoad)器紧、視圖將要呈現(xiàn)到窗口前(viewWillAppear)、視圖已經(jīng)呈現(xiàn)到窗口(viewDidAppear)熏兄、視圖將要從窗口刪除(viewWillDisappear)品洛、視圖已經(jīng)從窗口刪除(viewDidDisappear)、視圖被銷毀(viewDidUnload,這個(gè)方法在iOS6.0以后將不起作用了)摩桶、控制器被銷毀(dealloc)桥状。為了實(shí)現(xiàn)功能,我們可能需要在上述的某個(gè)地方添加對應(yīng)的處理代碼硝清。如何添加代碼辅斟?以及在上述的模板方法中添加什么樣的代碼?就非常的關(guān)鍵了芦拿。在這里面我想強(qiáng)調(diào)一點(diǎn)的是雖然控制器中擁有了一個(gè)view的根視圖屬性士飒,但是控制器的生命周期一般要比根視圖的生命周期要長,而且有可能會出現(xiàn)一個(gè)功能在不同場景下的視圖呈現(xiàn)完全不一樣蔗崎,或者有可能會通過重新構(gòu)建視圖來實(shí)現(xiàn)一些換膚功能的場景酵幕。在iOS6以后的控制器中只提供了視圖構(gòu)建以及構(gòu)建完成的模板方法,但卻不再提供視圖被銷毀之前或者之后的模板方法缓苛,因此我們在loadView以及viewDidLoad中添加代碼時(shí)就一定要考慮到這么一點(diǎn)芳撒,因?yàn)樗幌衿渌姆椒ㄒ粯犹峁┝嘶ツ嫣幚淼臋C(jī)制。

  • 控制器初始化(init)
    如果你的業(yè)務(wù)模型對象的生命周期和控制器的生命周期一樣未桥,那么建議將業(yè)務(wù)模型對象的構(gòu)建放在控制器的初始化代碼中笔刹,當(dāng)然前提是你的業(yè)務(wù)模型對象是一個(gè)輕量級的對象,如果你的業(yè)務(wù)模型對象的構(gòu)建特別消耗時(shí)間那么不建議放在控制器的初始化中構(gòu)建而是通過懶加載或者在某個(gè)觸摸事件發(fā)生時(shí)再構(gòu)建冬耿。如果你的控制器由多個(gè)子控制器組成舌菜,那么子控制器的初始化工作也在這里完成最佳。在控制器初始化時(shí)我們還可以初始化以及創(chuàng)建一些其他的輕量級的屬性亦镶,這些屬性或者變量的生命周期和控制器的生命周期一致日月。

  • 視圖構(gòu)建(loadView)
    如果你的視圖是通過SB或者XIB來建立的袱瓮,那么恭喜你,你可以省略這部分代碼爱咬。如果你是通過代碼來構(gòu)建你的視圖懂讯,那么你就有必要在這個(gè)地方添加你的視圖構(gòu)建和布局代碼。你需要重載loadView的方法台颠,并在最好在這里完成所有視圖的構(gòu)建和布局。如果你想復(fù)用默認(rèn)的根視圖作為自己的根視圖那么你需要在構(gòu)建你的其他子視圖之前調(diào)用基類的loadView方法勒庄,而如果你想要完全構(gòu)建自己的根視圖以及子視圖體系那么你就不必要調(diào)用基類的loadView方法串前。很多人都喜歡在viewDidLoad里面進(jìn)行視圖的構(gòu)建,其實(shí)不是最佳的解決方案实蔽,因?yàn)楦鶕?jù)字面意思viewDidLoad里面添加的應(yīng)該是視圖構(gòu)建并加載完成后的一些處理邏輯荡碾。如何在loadView中更加優(yōu)雅以及合理的構(gòu)造界面布局代碼,后面我將會給出一個(gè)具體解決方案局装。

-(void)loadView
{
   /*
   自定義根視圖的構(gòu)建坛吁,不需要調(diào)用基類的方法。你也可以直接在這里將UIScrollView或者UITableView作為根視圖铐尚。
   這樣就不必在默認(rèn)的根視圖上再建立滾動視圖或者列表子視圖了拨脉。
   */
    self.view = [[UIView alloc] initWithFrame: [UIScreen mainScreen].bounds];

   //...建立其他子視圖。

}

  • 事件綁定的代碼(viewDidLoad)
    當(dāng)視圖構(gòu)建完畢后系統(tǒng)會調(diào)用viewDidLoad宣增。因此您應(yīng)該在這里完成一些業(yè)務(wù)邏輯初始化的動作玫膀、業(yè)務(wù)模型服務(wù)接口的初始請求、一些控件的事件處理綁定的動作爹脾、視圖的delegate以及dataSource的設(shè)置帖旨。也就是這里一般用來完成視圖和控制器之間的關(guān)聯(lián)處理以及控制器和業(yè)務(wù)模型的關(guān)聯(lián)處理。在viewDidLoad中最適合做的就是實(shí)現(xiàn)視圖和控制器之間的綁定以及控制器和業(yè)務(wù)模型之間的綁定操作灵妨。這里不建議進(jìn)行視圖的構(gòu)建解阅,以及一些涉及到整個(gè)控制器生命周期相關(guān)的處理。

  • 視圖的呈現(xiàn)和消失(viewWill/DidAppear,viewWill/DidDisappear)
    視圖的呈現(xiàn)和消失有可能會被反復(fù)調(diào)用泌霍。建議在這里完成定時(shí)器货抄、通知觀察者的添加和銷毀處理。一般來說定時(shí)器和觀察者都只是在界面被呈現(xiàn)時(shí)產(chǎn)生作用烹吵,而界面消失時(shí)則不處理碉熄,因此在這里添加定時(shí)器和通知觀察者是最合適的。而且還有一個(gè)好處就是在這里實(shí)現(xiàn)定時(shí)器和觀察者時(shí)不會產(chǎn)生循環(huán)引用而導(dǎo)致控制器不能被釋放的問題發(fā)生肋拔。

  • 控制器被銷毀(dealloc)
    控制器被銷毀時(shí)表明控制器的生命周期已經(jīng)完結(jié)了锈津。一般情況下不需要添加特殊的代碼,這里一再強(qiáng)調(diào)的就是:
    一定要在這里把各種控件視圖中的delegate以及dataSource設(shè)置為nil!
    一定要在這里把各種控件視圖中的delegate以及dataSource設(shè)置為nil!
    一定要在這里把各種控件視圖中的delegate以及dataSource設(shè)置為nil!

重要的事情說三遍凉蜂!不管這些delegate是assign還是weak的琼梆。

懶加載

懶加載的目的是為了解決按需創(chuàng)建使用以及可選使用以及耗時(shí)創(chuàng)建的場景性誉。在某種情況下使用懶加載可以加快展示的速度,懶加載可以將某些對象的創(chuàng)建時(shí)機(jī)延后茎杂。那么是不是要將所有的對象的創(chuàng)建都采用懶加載的形式進(jìn)行創(chuàng)建错览? 答案是否定的。 有不少同學(xué)都喜歡將控制器中的所有視圖的創(chuàng)建和布局都通過懶加載的形式來完成煌往,如下面的代碼片段:

@interface XXXViewController()
   @property(strong) UILabel *label;
   @property(strong) UITableView *tableView;
@end

@implementation XXXViewController

-(UILabel*)label
{
     if (_label == nil)
    {
          _label = [UILabel new];
          [self.view addSubview:_label];
         //有些同學(xué)會在這里添加附加代碼比如布局相關(guān)的代碼
    }
    return _label;
}

-(UITableView*)tableView
{
     if (_tableView == nil)
    {
           _tableView = [UITableView new];
          [self.view addSubview:_tableView];
          _tableView.delegate = self;
         //有些同學(xué)會在這里添加附加代碼比如布局相關(guān)的代碼
    }
    return _tableView;
}


-(void)viewDidLoad
{
    [super viewDidLoad];

    self.label.text = @"hello";
    [self.tableView reloadData];
}


@end

看起來代碼很簡潔也很清晰倾哺,起碼在viewDidLoad中是這樣的。但是這里面卻有可能存在著一些隱患:

  • 視圖層次順序被打亂和代碼分散
    因?yàn)橐晥D都是懶加載并且分散的刽脖,因此你不能從整體看出視圖層次結(jié)構(gòu)是如何的羞海,以及排列的順序是如何的。這就為我們的代碼閱讀以及調(diào)試和維護(hù)增加了困難曲管。

  • 職責(zé)不明確
    懶加載的主要作用是延遲創(chuàng)建却邓,但是上述的視圖屬性的重寫卻已經(jīng)超出了單純的創(chuàng)建的范疇了,除了創(chuàng)建視圖之外還實(shí)現(xiàn)了視圖添加到父視圖的功能以及進(jìn)行布局的功能院水,更有甚者還有可能實(shí)現(xiàn)其他更加復(fù)雜的邏輯腊徙。這樣就會導(dǎo)致一個(gè)get屬性的實(shí)現(xiàn)承載的功能過多,嚴(yán)重的超過了一個(gè)方法所應(yīng)承擔(dān)的責(zé)任檬某。在使用時(shí)我們只是簡單的將其當(dāng)做一個(gè)讀取屬性來使用并且還有可能發(fā)生有些代碼重復(fù)的問題撬腾。

  • 莫名的問題和崩潰
    懶加載視圖使得我們的視圖屬性必須要設(shè)置為strong類型的,而且代碼的實(shí)現(xiàn)是只創(chuàng)建一次橙喘。如果因?yàn)槟承┰蚴沟梦覀兊目刂破骼锩娴乃幸晥D都需要重新創(chuàng)建(比如換膚)時(shí)那么就有可能導(dǎo)致這個(gè)懶加載的視圖不會再次被創(chuàng)建而產(chǎn)生界面上莫名其妙的問題时鸵。更有甚者因?yàn)樵趹屑虞d中實(shí)現(xiàn)過多的代碼導(dǎo)致在某些地方訪問屬性時(shí)產(chǎn)生了崩潰。

因此不建議對一個(gè)控制器里面的所有視圖構(gòu)建都采用懶加載模式厅瞎,視圖的構(gòu)建和布局應(yīng)該在loadView中進(jìn)行統(tǒng)一處理饰潜。懶加載的方式不能濫用,尤其是視圖的構(gòu)建代碼和簸。我們應(yīng)該只對那些可選存在的對象以及那些有可能會影響性能的對象采用懶加載的方式來進(jìn)行構(gòu)建彭雾,而不是所有的對象都采用懶加載的形式來創(chuàng)建。同時(shí)還需要注意的就是如果一定要采用懶加載來實(shí)現(xiàn)對象的構(gòu)建時(shí)锁保,在懶加載中的代碼也應(yīng)該盡量的簡化薯酝,只需要實(shí)現(xiàn)創(chuàng)建部分的功能即可,而不要將一些非必要的邏輯代碼放入到懶加載的實(shí)現(xiàn)處爽柒,越多的邏輯實(shí)現(xiàn)吴菠,就會對使用著產(chǎn)生越多的限制和不確定因素的發(fā)生。就以上面的例子來說使用者在調(diào)用self.label或者self.tableView時(shí)一般都只是將它們當(dāng)做普通的屬性來使用浩村,而不會去考慮它們的內(nèi)部還進(jìn)行了如此多的設(shè)置和處理(比如完成布局和添加到父視圖中去)做葵。這樣就可能會造成對這些屬性的使用不當(dāng)而造成災(zāi)難的后果。另外雖然你的視圖的構(gòu)建是通過懶加載的形式來完成的心墅,但是如果你在比如viewDidLoad中大量的訪問這些屬性時(shí)一樣的會產(chǎn)生視圖的構(gòu)建操作酿矢,這樣其實(shí)和直接創(chuàng)建視圖對象是一樣的榨乎,并沒有起到任何優(yōu)化性能的作用,而且這樣也是和懶加載的初衷是違背的瘫筐。

我們項(xiàng)目中的一個(gè)案例就是UITableView的創(chuàng)建使用的懶加載蜜暑,里面除了創(chuàng)建UITableView的實(shí)例外還在里面設(shè)置了delegate的值以及其他代碼邏輯。而這個(gè)UITableView又剛好是一個(gè)可選的顯示視圖策肝。同時(shí)我們又在視圖控制器的dealloc中對這個(gè)UITableView的delegate做了置為nil的處理肛捍。結(jié)果這段代碼最終在線上出現(xiàn)了crash的情況了。

簡化控制器中的視圖構(gòu)建

視圖的構(gòu)建有兩種方式:一種是通過Storyboard或者XIB以可視化的方式來構(gòu)建之众;一種是通過程序代碼的方式來完成構(gòu)建篇梭。兩種方法各有優(yōu)劣。iOS以及Android系統(tǒng)都提供了強(qiáng)大的可視化界面布局系統(tǒng)酝枢,并且二者都是采用XML文件的方式來描述布局。這種方式非常符合MVC中關(guān)于V的定義悍手,視圖部分獨(dú)立存在并且層次分明帘睦。采用這種方式來構(gòu)建你的視圖在一定程度上不會對你的控制器中的代碼產(chǎn)生污染以及導(dǎo)致你控制器中的代碼的膨脹。通過SB和XIB的使用就可以簡化我們對視圖部分的構(gòu)建坦康。在實(shí)踐中你會發(fā)現(xiàn)如果你是通過代碼來完成視圖的構(gòu)建和布局那么這部分代碼就有可能超過你控制器50%的代碼行數(shù)竣付。因此解決C層臃腫的一個(gè)方法就是將你的界面布局的代碼都統(tǒng)一通過SB或者XIB來實(shí)現(xiàn)。有的同學(xué)可能會說通過SB或者XIB的方式不利于協(xié)同開發(fā)滞欠,很容易造成合并時(shí)的代碼沖突古胆。其實(shí)這是一個(gè)偽命題。一般情況下我們的功能都會拆分為一個(gè)個(gè)視圖控制器來實(shí)現(xiàn)筛璧,并且一個(gè)人負(fù)責(zé)一個(gè)控制器逸绎。如果你用XIB來實(shí)現(xiàn)自己負(fù)責(zé)的那個(gè)控制器的界面布局那么又怎么可能會產(chǎn)生代碼合并的沖突呢?即使是你用SB的方式來構(gòu)建你的界面夭谤,雖然SB是將大部分界面都放在一個(gè)文件中來完成棺牧,但是在實(shí)踐中我們的應(yīng)用是可以建立多個(gè)SB的。我們可以從功能相似性的角度出發(fā)將相同的功能放在一個(gè)SB中朗儒,不同大模塊建立不同的SB文件颊乘,這樣就可以將一個(gè)SB根據(jù)應(yīng)用模塊分解為多個(gè)小SB。只要拆分的合理那么在進(jìn)行協(xié)同開發(fā)時(shí)就會最大限度的減少沖突的發(fā)生醉锄。隨著XCODE版本的更新乏悄,SB所具有的功能越來越強(qiáng)大,通過SB除了能實(shí)現(xiàn)界面布局外包括邏輯的跳轉(zhuǎn)以及頁面的切換我們都不需要編寫一行代碼恳不。我們其實(shí)可以花一點(diǎn)時(shí)間靜下心來好好的去研究一下它檩小,而不是一味的去拒絕和抵觸。君不見Android的開發(fā)者還是喜歡通過XML并且基本是通過XML的編寫來完成界面布局的呢妆够。

也許上面的方式說不服你识啦,你還是通過代碼來構(gòu)建布局那一派的负蚊。沒有關(guān)系,本文探討的是如何解決控制器代碼膨脹的問題颓哮,而不是掀起派系之爭家妆。那么如果我就是要通過代碼的方式來完成界面布局呢?畢竟通過代碼布局的方式更加靈活和可配置性(犧牲了所見即所得性)冕茅。我們知道在iOS的loadView的默認(rèn)實(shí)現(xiàn)邏輯是首先會到SB或者XIB中去根據(jù)視圖控制器的類型去搜索是否有匹配的視圖布局文件伤极,如果有則將這個(gè)視圖布局文件進(jìn)行解析并構(gòu)建對應(yīng)的視圖層次樹并設(shè)置視圖控制器中的那些插座變量(IBOutlet)以及綁定視圖控件所關(guān)聯(lián)的事件處理器(IBAction)。如果沒有找到對應(yīng)的布局文件的話就會創(chuàng)建一個(gè)空白的根視圖(self.view)姨伤∩谄海可見loadView的主要目的就是為了完成視圖的構(gòu)建和布局。因此當(dāng)我們通過代碼的方式來完成視圖的創(chuàng)建以及布局時(shí)也應(yīng)該將代碼邏輯放到這里而不應(yīng)該放到viewDidLoad中去乍楚。視圖的構(gòu)建和布局應(yīng)該在一個(gè)地方統(tǒng)一進(jìn)行而不應(yīng)該通過懶加載的方式來將代碼分散到對各個(gè)視圖屬性進(jìn)行重寫來完成当编。 在這里我提供2種方法來實(shí)現(xiàn)視圖構(gòu)建和布局從控制器中分離或者歸類處理。

一. 采用分類擴(kuò)展的方法

顧名思義徒溪,采用分類擴(kuò)展的方法就是為視圖控制器專門建立一個(gè)視圖構(gòu)建和布局的分類擴(kuò)展忿偷。為了將這部分代碼和控制器中其他代碼分離,我們可以將視圖構(gòu)建的分類擴(kuò)展代碼單獨(dú)放到新文件中來實(shí)現(xiàn)臊泌。

//為每個(gè)控制器都建立一個(gè) 控制器名字+CreateView的頭文件
//XXXXViewController+CreateView.h
#import "XXXXViewController.h"

//定義一個(gè)擴(kuò)展鲤桥,擴(kuò)展里面定義所有控制器可能要用到的視圖屬性,定義屬性的方式就和通過SB或者XIB的方式一致渠概。
@interface XXXXViewController ()
  @property(nonatomic, weak) IBOutlet UILabel *label;
  @property(nonatomic, weak) IBOutlet UIButton *button;
  @property(nonatomic, weak) IBOutlet UITableView *tableView;
   //...
@end

..................................
//代碼布局的實(shí)現(xiàn)部分
//XXXXViewController+CreateView.m

#import "XXXXViewController+CreateView.h"

//這里定義一個(gè)分類茶凳,分類只實(shí)現(xiàn)loadView的重載來完成視圖的構(gòu)建和布局
@implementation XXXXViewController(CreateView)

-(void)loadView
{
    [super loadView];   //如果你想完全自定義根視圖就可以和上面我曾經(jīng)列出的代碼一樣不調(diào)用父類的方法。

  //這里完成所有子視圖的構(gòu)建和布局播揪。因?yàn)橐晥D構(gòu)建的代碼都是統(tǒng)一寫在一起的贮喧,所以這里面就可以很方便的通過閱讀代碼的方式來看清怎么視圖的布局層次。

    UILabel *label = [UILabel new];
    label.textColor = [UIColor redColor];
    label.font = ....
    [self.view addSubview:label];
    self.label = label;

    UIButton *button = [UIButton new];
    [self.view addSubview:button];
    self.button = button;

    UITableView *tableView = [UITableView new];
    [self.view addSubview:tableView];
    self.tableView = tableView;

   //....

   //你可以在這里對上面所有的子視圖通過autolayout的方式來完成代碼布局的編寫猪狈、也可以在上面每個(gè)視圖創(chuàng)建完成后就進(jìn)行代碼布局的編寫塞淹,這個(gè)沒有限制。

}

@end

上面的代碼可以看出我們單獨(dú)建立了一個(gè)擴(kuò)展來定義所有視圖屬性罪裹,并建立了一個(gè)分類并且重載loadView來實(shí)現(xiàn)視圖的建立和布局饱普。代碼中我們只做構(gòu)建和布局,而不做其他的事情状共。比如UIButton的事件綁定以及UITableView的delegate和dataSource的設(shè)置都不在這里面進(jìn)行套耕。這個(gè)分類就是一個(gè)非常存粹的代碼構(gòu)建和界面布局的代碼。這樣我們看下面的控制器的主要代碼實(shí)現(xiàn)部分就非常的干凈了峡继。

//XXXXViewController.h

@interface  XXXXViewController

@end

..............................
//XXXXViewController.m

//這里導(dǎo)入分類為了能夠訪問其中的視圖屬性
#import XXXXViewController+CreateView.h

@implementation  XXXXViewController

-(void)viewDidLoad
{
    [super viewDidLoad];
    
     //這里對按鈕綁定事件冯袍,對tableView指定委托和數(shù)據(jù)源,可以看出在viewDidLoad里面最適合做的事情就是建立視圖和控制器之間的關(guān)聯(lián)和綁定。
     [self.button  addTarget:self action:@selector(handleClick:) forControlEvents:UIControlEventTouchUpInside];
     self.tableView.delegate = self;
     self.tableView.dataSource = self;
}

@end

通過分類擴(kuò)展的方法并不能減少控制器的代碼康愤,但是卻可以將特定的邏輯進(jìn)行歸類分解儡循,從而增強(qiáng)代碼的可閱讀性以及可維護(hù)性。因?yàn)殛P(guān)于視圖構(gòu)建和布局部分的代碼都拆分到其他單獨(dú)的地方征冷,而我們的控制器的主要實(shí)現(xiàn)部分就可以專心編寫控制邏輯了择膝。甚至這種拆分的方法還可以將工作一分為二:一人專門負(fù)責(zé)界面布局、一人專門負(fù)責(zé)控制邏輯的編寫检激。

二. 采用接口和消息轉(zhuǎn)發(fā)

視圖控制器通過對分類擴(kuò)展來實(shí)現(xiàn)視圖構(gòu)建的拆分肴捉,代碼還是屬于視圖控制器的一部分。如果我們想完全實(shí)踐MVC中的V獨(dú)立存在并且可以被復(fù)用的話叔收,我們可以將視圖構(gòu)建和布局單獨(dú)抽象到一個(gè)視圖類中齿穗,并且通過接口定義和消息轉(zhuǎn)發(fā)的方法來建立控制器和視圖之間的聯(lián)系。還記得我在上一篇文章里面所提到的forwarding技術(shù)嗎饺律?為了實(shí)現(xiàn)視圖和控制器的分離我們依然可以采用這種方法來實(shí)現(xiàn)層次的分離窃页。

  • 1.定義視圖屬性接口和視圖布局類
//定義一個(gè)以控制器名開頭加View的協(xié)議和實(shí)現(xiàn)類。
//XXXXViewControllerView.h

@protocol  XXXXViewControllerView

@optional
  @property(nonatomic, weak)  UILabel *label;
  @property(nonatomic, weak)  UIButton *button;
  @property(nonatomic, weak)  UITableView *tableView;
  //...
@end


//你的布局根視圖可以繼承自UIView或者UIScrollView或者其他視圖复濒。
@interface XXXXViewControllerView:UIView<XXXXViewControllerView>
  @property(nonatomic, weak) IBOutlet UILabel *label;
  @property(nonatomic, weak) IBOutlet UIButton *button;
  @property(nonatomic, weak) IBOutlet UITableView *tableView;
@end

................................
//XXXXViewControllerView.m

@implementation  XXXXViewControllerView

-(id)initWithFrame:(CGRect)frame
{
   self = [super initWithFrame:frame];
   if (self != nil)
   {
           self.backgroundColor = [UIColor whiteColor];

           UILabel *label = [UILabel new]; 
           [self addSubview:label];
           _label = label;

            UIButton *button = [UIButton new];
            [self addSubview:button];
            _button = button;

           UITableView *tableView = [UITableView new];
           [self addSubview:tableView];
           _tableView = tableView;

           //如果您用的是AutoLayout那么您可以在這里添加布局約束的代碼腮出。如果您是通過frame來進(jìn)行布局那么請?jiān)趌ayoutSubviews中進(jìn)行子視圖的布局處理。
   }

    return self;
}

-(void)layoutSubviews
{
    [super layoutSubviews];
    
    //如果你是通過frame來設(shè)置布局那么就可以在這里進(jìn)行布局的刷新芝薇。。
}


@end

可以看出上述的代碼和控制器之間沒有任何關(guān)系作儿,并且是獨(dú)立于控制器而存在的洛二。視圖布局類的作用就是只用于視圖的布局和構(gòu)建以及展示,這種方式非常符合MVC中V的定義和實(shí)現(xiàn)攻锰。視圖構(gòu)建完成后晾嘶,需要對視圖進(jìn)行布局處理,您可以使用AutoLayout方式來進(jìn)行布局也可以使用frame方式來進(jìn)行布局娶吞。AutoLayout布局是一種通過視圖之間的約束設(shè)置來實(shí)現(xiàn)布局的方式垒迂,而frame方式則是蘋果早期的一種布局方式。AutoLayout進(jìn)行代碼布局時(shí)妒蛇,代碼量非常的多和復(fù)雜机断,這個(gè)問題在iOS9以后簡化了很多。還好有很多第三方的布局類庫比如Mansory可以有效的簡化布局的難度绣夺。如果您的布局要考慮性能問題以及想更加簡單的完成布局那么您可以考慮使用筆者開源的界面布局:MyLayout來實(shí)現(xiàn)界面布局吏奸。

  • 2.視圖控制器和布局視圖類的綁定。
//XXXXViewController.h


@interface XXXXViewController:UIViewController
@end

................................
//XXXXViewController.m

#import "XXXXViewControllerView.h"  //這里導(dǎo)入對應(yīng)的布局視圖類

//視圖控制器也需要實(shí)現(xiàn)XXXXViewControllerView接口陶耍。這樣視圖控制器中就可以直接訪問視圖的一些屬性了奋蔚。
@interface XXXXViewController ()<XXXXViewControllerView>

@end

@implementation XXXXViewController

//重寫loadView來完成視視圖的構(gòu)建。
-(void)loadView
{
    self.view = [[ViewControllerView alloc] initWithFrame:[UIScreen mainScreen].bounds];
}

//這個(gè)部分是實(shí)現(xiàn)的關(guān)鍵,來將控制器對視圖屬性協(xié)議的訪問分發(fā)到布局視圖中去泊碑。
-(id)forwardingTargetForSelector:(SEL)aSelector
{
    struct objc_method_description  omd = protocol_getMethodDescription(@protocol(ViewControllerView), aSelector, NO, YES);
    if (omd.name != NULL)
    {
        return self.view;
    }

    return [super forwardingTargetForSelector:aSelector];

}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    //這里就可以像平常一樣訪問視圖屬性并添加事件的綁定處理坤按。
    [self.button addTarget:self  action:@selector(handleClick:) forControlEvents:UIControlEventTouchUpInside];
    
}

-(void)handleClick:(id)sender
{
    
}

@end

大家可以看到上面通過對loadView和forwardingTargetForSelector方法進(jìn)行重載來實(shí)現(xiàn)視圖控制器與視圖之間的綁定。然后我們就可以在任意位置來訪問視圖接口中的屬性了馒过。綁定操作對于所有視圖控制器類來說都是一致的臭脓,所以你可以通過一個(gè)宏定義的形式來實(shí)現(xiàn)上面的綁定操作:

//在某個(gè)公共的地方定義如下宏

#define BINDVIEW(viewclass)   \
-(void)loadView \
{\
    self.view = [[viewclass alloc] initWithFrame:[UIScreen mainScreen].bounds];\
}\
-(id)forwardingTargetForSelector:(SEL)aSelector\
{\
    struct objc_method_description  omd = protocol_getMethodDescription(@protocol(viewclass), aSelector, NO, YES);\
    if (omd.name != NULL)\
    {\
        return self.view;\
    }\
    return [super forwardingTargetForSelector:aSelector];\
}\

...........................

//XXXXViewController.m

#import "XXXXViewControllerView.h"

//視圖控制器也需要實(shí)現(xiàn)XXXXViewControllerView接口。這樣視圖控制器中就可以直接訪問視圖的一些屬性了沉桌。
@interface XXXXViewController ()<XXXXViewControllerView>

@end

@implementation XXXXViewController

//這里直接用宏即可
BINDVIEW(XXXXViewControllerView)

//...這里添加其他代碼谢鹊。

@end

上面的兩種對視圖構(gòu)建和布局進(jìn)行分解的方式都可以解決在控制器中視圖代碼構(gòu)建導(dǎo)致的膨脹問題。第一種方法本質(zhì)上只是做了一些代碼拆分留凭,并未實(shí)現(xiàn)控制器和視圖的完全分離佃扼;第二種方法則完全實(shí)現(xiàn)了視圖和控制器之間的分離,視圖的構(gòu)建和布局不再依賴于控制器的存在蔼夜,而且我們甚至可以對視圖進(jìn)行復(fù)用兼耀,也就是說可以讓多個(gè)控制器類復(fù)用一個(gè)視圖類中的代碼。這些控制器所實(shí)現(xiàn)的功能的展示效果一樣或者有微小的差別求冷,但是事件處理邏輯則可以完全不一樣瘤运。第二種方法的實(shí)現(xiàn)機(jī)制更加體現(xiàn)了MVC中的層次關(guān)系以及V層構(gòu)建的獨(dú)立性。因此不管你是通過SB或者XIB來構(gòu)建您的視圖還是通過代碼來構(gòu)建您的視圖布局匠题,只要設(shè)計(jì)得當(dāng)都可以非常有效的減少視圖控制器中對視圖依賴部分的代碼拯坟。

業(yè)務(wù)邏輯的下沉

視圖的構(gòu)建部分的問題我們已經(jīng)成功解決。我們再來探討一下薄服務(wù)層APIService的問題韭山。在開始我曾經(jīng)說過很多的架構(gòu)設(shè)計(jì)人員都會以和服務(wù)器之間交互的所有API接口為標(biāo)準(zhǔn)而設(shè)計(jì)出一套服務(wù)層API郁季,我們姑且叫他為APIService。APIService會為每一個(gè)和服務(wù)端交互的接口都產(chǎn)生一個(gè)簡單的封裝钱磅,這個(gè)封裝只是完成了對向服務(wù)器請求的數(shù)據(jù)的打包以及URL鏈接的封裝以及將服務(wù)端返回的報(bào)文進(jìn)行反序列化解包后直接通過block回調(diào)的方式返回給視圖控制器梦裂。


@interface APIService:NSObject

  +(void)requestXXXWithDict:(NSDictionary*)input  callback:(void (^)(XXXXModel *model, NSError *error))callback;

  +(void)requestYYYYWithDict:(NSDictionary*)input  callback:(void (^)(YYYYModel *model, NSError *error))callback;

   //.....
@end

我們的視圖控制器中的任何一個(gè)網(wǎng)絡(luò)請求都是直接調(diào)用對應(yīng)的請求方法,并對返回的Model數(shù)據(jù)模型進(jìn)行加工處理盖淡,比如界面視圖數(shù)據(jù)刷新年柠、文件處理、某些邏輯的調(diào)整等等褪迟。在這個(gè)過程中控制器就無形之中承擔(dān)了業(yè)務(wù)邏輯的實(shí)現(xiàn)的工作冗恨,從而加重了控制器中代碼的負(fù)擔(dān)。比如下面的代碼例子:

@ implementation XXXXViewController

//某個(gè)控制器的某個(gè)事件處理代碼味赃。
-(void)handleClick:(id)sender
{
     //這部分代碼需要根據(jù)不同的狀態(tài)來請求不同的服務(wù)派近。假設(shè)這個(gè)狀態(tài)值保存到控制器中

    if (self.status == 1)
    {
          //彈出loading... 等待框 ,并請求服務(wù)
          [APIService  requestXXX:^(XXXModel* model, NSError *error){
                   //銷毀loading... 框
                  if (error == nil)
                  {

                       //將model寫入某個(gè)文件中去洁桌。
                       // 將model的數(shù)據(jù)更新到某個(gè)視圖中去渴丸。
                       self.status = 2;   //更新狀態(tài)。
                       //其他邏輯。谱轨。
                 }
                else
                {
                   //..錯(cuò)誤處理戒幔。
                }   
         }];
   }
   else if (status == 2)
   {
        //彈出loading... 等待框,并請求另外一個(gè)服務(wù),返回的數(shù)據(jù)模型相同。
       [APIService requestYYY:^(XXXModel *model, NSError *error){
                   //銷毀loading... 框
                  if (error == nil)
                  {

                       //將model寫到文件中或者更新到數(shù)據(jù)庫中去土童。
                      // 將model的數(shù)據(jù)更新到某個(gè)視圖中去诗茎。
                      self.status = 1;   //更新狀態(tài)。
                      //其他邏輯献汗。敢订。
                 }
                else
                {
                   //..錯(cuò)誤處理。
                }   
         }];
   }   
}

@end

上面的代碼可以看出控制器除了保存一些狀態(tài)外罢吃,并且根據(jù)不同的狀態(tài)還做了不同的網(wǎng)絡(luò)服務(wù)請求楚午、文件的讀寫、狀態(tài)的更新尿招、視圖的刷新操作等等其他邏輯矾柜,這樣就導(dǎo)致了控制器的代碼非常的臃腫和難以維護(hù)。問題出在哪里了呢就谜?就是對模型層的理解產(chǎn)生了誤區(qū)怪蔑,以及對服務(wù)層的定義產(chǎn)生了錯(cuò)誤的使用。

真實(shí)的MVC中的M模型層所代表的是業(yè)務(wù)模型而非數(shù)據(jù)模型丧荐、業(yè)務(wù)模型的作用就是用來完成業(yè)務(wù)邏輯的具體實(shí)現(xiàn)缆瓣。M層所要做的就是將一些和視圖展現(xiàn)無關(guān)以及和控制器無關(guān)的東西進(jìn)行封裝處理,而只是給控制器提供出非常簡單易用的接口來供其調(diào)用虹统。APIService的封裝是不符合邏輯和錯(cuò)誤的封裝的弓坞!我們知道任何系統(tǒng)都有一套完整的業(yè)務(wù)實(shí)現(xiàn)體系,這個(gè)實(shí)現(xiàn)體系不止在服務(wù)器端存在而且在客戶端上也存在窟却,這兩者之間是一致的。您可以將業(yè)務(wù)實(shí)現(xiàn)的體系理解為服務(wù)端實(shí)現(xiàn)體系的一個(gè)代理呻逆,代理和服務(wù)器服務(wù)之間通信的紐帶就是接口報(bào)文夸赫。
我們不能將客戶端的代理實(shí)現(xiàn)簡單理解為只是對接口報(bào)文的簡單封裝,而是應(yīng)該設(shè)計(jì)為和服務(wù)端一樣具有完整架構(gòu)體系的業(yè)務(wù)邏輯實(shí)現(xiàn)層咖城,這我想也就是M層的本質(zhì)所在吧茬腿。所以我們在設(shè)計(jì)客戶端的M層時(shí)也一定要本著這個(gè)思想去設(shè)計(jì),不能只是簡單的為接口報(bào)文進(jìn)行封裝宜雀,并且在控制器里面去實(shí)現(xiàn)一些業(yè)務(wù)邏輯切平,而是應(yīng)該將業(yè)務(wù)邏輯的實(shí)現(xiàn)、網(wǎng)絡(luò)的請求辐董、報(bào)文的處理以一種抽象的以及和業(yè)務(wù)場景相關(guān)的東西統(tǒng)一的放在M模型層悴品。這種理念和設(shè)計(jì)方法其實(shí)在我的另外兩篇介紹模型層構(gòu)建的文章中都非常詳細(xì)的有說明。我們應(yīng)該在某種程度上將原先屬于在控制器中的邏輯進(jìn)行下沉和分解來將邏輯的實(shí)現(xiàn)部分下移到模型層,這樣我們在設(shè)計(jì)時(shí)就不會只是簡單的實(shí)現(xiàn)一個(gè)一個(gè)APIService中的方法苔严。而是構(gòu)建出一套完整的業(yè)務(wù)模型框架出來供控制器來使用了定枷。還是以上面的例子,解決的方法是我們設(shè)計(jì)出一個(gè)業(yè)務(wù)模型類比如XXXXService届氢,它內(nèi)部封裝了狀態(tài)以及不用的網(wǎng)絡(luò)請求欠窒,以及一些文件讀寫的實(shí)現(xiàn):

//XXXXService.h

 @interface XXXXService
    
     -(void)request:(void (^)(XXXModel *model, NSError *error))callback;

  @end  

..........................
//XXXXService.m

@ implementation XXXXService
{
    int status = 1;
}

-(void)request:(void (^)(XXXModel *model, NSError *error))callback
{
       if (self.status == 1)
       {
             [network   get:@"URL1"   complete:^(id obj, NSError *error){
                 XXXModel *retModel = nil;
                 if (error == nil)
                  {
                       XXXModel *retModel = obj --> XXXModel //報(bào)文到模型的第三方轉(zhuǎn)換工具
                       //這里寫入文件和數(shù)據(jù)庫
                       self.status = 2;  //這里更新狀態(tài)。
                   }
                   callback(retModel, error);
             }];
      } 
     else if (self.status == 2)
     {
         [network   get:@"URL2"   complete:^(id obj, NSError *error){
              XXXModel *retModel = nil;
              if (error == nil)
              {
                    XXXModel *retModel = obj --> XXXModel //報(bào)文到模型的第三方轉(zhuǎn)換工具,假設(shè)URL2和URL1的數(shù)據(jù)模型都非常相似
                   //這里做其他的非視圖相關(guān)的邏輯退子。
                   self.status = 1;  //這里更新狀態(tài)岖妄。
             }
             callback(retModel, error);
         }];
     }
}

@end

上面的業(yè)務(wù)模型代碼只是純粹的邏輯實(shí)現(xiàn)和具體的控制器無關(guān)和具體的視圖無關(guān)。那么我們?nèi)绾卧诳刂破髦惺褂眠@個(gè)業(yè)務(wù)模型呢寂祥?

//XXXXViewController.m

 #import "XXXXService.h"
      
@interface XXXXViewController()
   @property(strong)   XXXXService *service;  //將業(yè)務(wù)模型以對象的形式保存起來荐虐,這里我們將看不到單例對象、也看不到平面的服務(wù)請求了壤靶,而是一個(gè)普通的對象缚俏。而且是一個(gè)真實(shí)的對象!V椤忧换!
@end
   
 @implementation  XXXXViewController
          
//至于service的創(chuàng)建方式可以在控制器初始化時(shí)創(chuàng)建,也可以通過懶加載的方式進(jìn)行創(chuàng)建向拆。這里我們通過懶加載的形式進(jìn)行創(chuàng)建亚茬。這里才是懶加載的最佳實(shí)踐      
 -(XXXService*)service
{
    if (_service == nil){
       _service = [XXXService new];
      } 
   return _service;
}            

//還是原來的事件處理函數(shù)
-(void)handleClick:(id)sender
{
       //彈出loading... 等待框 ,并請求服務(wù)
       [self.service  request^(XXXModel* model, NSError *error){
             //銷毀loading... 框
             if (error == nil){   
                  // 將model的數(shù)據(jù)更新到某個(gè)視圖中去浓恳。
             }
            else
           {
              //..錯(cuò)誤處理刹缝。
            }       
         }];
}
  
@end

可以看出上面我們的視圖控制器中的代碼已經(jīng)非常的簡潔了,控制器不再持有狀態(tài)颈将,不再做一些業(yè)務(wù)實(shí)現(xiàn)相關(guān)的處理了梢夯,只是簡單的調(diào)用業(yè)務(wù)模型提供的服務(wù),并在回調(diào)中將數(shù)據(jù)模型中的數(shù)據(jù)更新視圖就可以了晴圾∷淘遥控制器不再根據(jù)狀態(tài)去發(fā)起不同的請求,不再處理任務(wù)業(yè)務(wù)實(shí)現(xiàn)相關(guān)的東西死姚,而且業(yè)務(wù)模型也不再是向以前那樣干巴巴的使用單例或者使用類方法的形式提供給控制器調(diào)用人乓,而是一個(gè)對象!一個(gè)真實(shí)的對象都毒!一個(gè)面向?qū)ο笾卸x的對象來給控制器調(diào)用色罚。通過對業(yè)務(wù)模型層的封裝使得我們可以在其他的視圖控制器中也非常簡單的使用業(yè)務(wù)模型提供的服務(wù)來完成服務(wù)。從而精簡了控制器中的代碼和邏輯账劲。在上面的例子中就可以很明確的看出MVC中M的責(zé)任負(fù)責(zé)業(yè)務(wù)邏輯的實(shí)現(xiàn)戳护,V的責(zé)任就是負(fù)責(zé)視圖的布局和展示金抡,而C層的責(zé)任就是負(fù)責(zé)將二者關(guān)聯(lián)起來。

控制邏輯的拆分

通過對視圖類的封裝和解耦解決了視圖部分占用控制器的代碼問題姑尺,通過對M層的正確定義解決了控制器過多的處理業(yè)務(wù)邏輯實(shí)現(xiàn)的問題竟终。我們的控制器中的代碼將會得到很大一部分的改善和精簡。我們已經(jīng)解決完了80%的問題了切蟋⊥炒罚可是即使如此我們的控制器中的邏輯有可能還是很多。

我們在構(gòu)建的某個(gè)視圖控制器中出現(xiàn)代碼膨脹的一個(gè)非常重要的原因有可能是這個(gè)功能的邏輯非常的復(fù)雜或者界面展示非常的復(fù)雜:

  • 一個(gè)界面中同時(shí)集成了眾多小的功能點(diǎn)柄粹,有些界面或者小功能點(diǎn)需要在特殊條件下才能展示出現(xiàn)喘鸟。有些小功能界面是可選出現(xiàn)的。
  • 一個(gè)界面中分成了好幾個(gè)區(qū)塊來展示驻右,每個(gè)區(qū)塊之間相對獨(dú)立什黑,但又因?yàn)槟承┰蛞稍谕粋€(gè)頁面之中。
  • 一個(gè)界面中受到某種狀態(tài)的控制堪夭,在不同狀態(tài)下可能會展示出完全不同的界面和實(shí)現(xiàn)完全不同的功能愕把。

對于這些具有復(fù)雜邏輯的功能來說,如果設(shè)計(jì)的不得當(dāng)就有可能出現(xiàn)控制器中的邏輯非常復(fù)雜和龐大森爽。怎么解決這些問題恨豁? 答案還是分解。至于如何進(jìn)行分解這就要具體問題具體分析了爬迟,這個(gè)就非抽倜郏考驗(yàn)架構(gòu)設(shè)計(jì)人員的技術(shù)和業(yè)務(wù)功底了。我們在這里不探討如何進(jìn)行業(yè)務(wù)拆分付呕,而是討論控制器對業(yè)務(wù)拆分的支持能力计福。
當(dāng)某個(gè)控制器中的邏輯過于龐大和復(fù)雜時(shí)可以考慮將功能拆分為多個(gè)子控制器來實(shí)現(xiàn)

在iOS5以后系統(tǒng)提供了對子控制器的支持能力,子控制器和父控制器一樣具有相似的生命周期內(nèi)的各種方法的回調(diào)處理機(jī)制徽职。子控制器的引入除了能夠?qū)⒁晥D布局進(jìn)行拆分而且能夠?qū)μ幚磉壿嬤M(jìn)行拆分象颖。在這種情況下我們把父視圖控制器稱為容器控制器。容器控制器的作用更多的是對整體進(jìn)行調(diào)度和控制姆钉,它可能不會再具體負(fù)責(zé)業(yè)務(wù)酿傍,具體的業(yè)務(wù)由子控制器來完成乃正。就如上面列出的三種場景我們都可以通過功能拆分的形式將一些邏輯拆分到子控制器來實(shí)現(xiàn)晴埂。我將分別用代碼來舉例上面的第二種和第三種場景的實(shí)現(xiàn):

  • 這個(gè)是一個(gè)復(fù)雜界面由多個(gè)區(qū)域組成的實(shí)現(xiàn)場景
//ContainerVC.m

#import "SubVC1.h"
#import "SubVC2.h"
#import "SubVC3.h"


@interface  ContainerVC()
//這個(gè)功能被分為3個(gè)獨(dú)立的區(qū)域進(jìn)行展示和處理齐遵。
@property(nonatomic, strong)  SubVC1 *vc1;
@property(nonatomic, strong)  SubVC2 *vc2;
@property(nonatomic, strong)  SubVC3 *vc3;
@end

@implementation ContainerVC

- (void)viewDidLoad {
    [super viewDidLoad];
     
    //子視圖控制器的構(gòu)建训挡,您可以在容器視圖控制器的初始化方法init中處理也可以在viewDidLoad里面進(jìn)行處理规辱。
    //這里面先刪除是為了防止有可能整個(gè)界面界面視圖被重新初始化的情況發(fā)生
    [self.vc1 removeFromParentViewController];
    [self.vc2 removeFromParentViewController];
    [self.vc3 removeFromParentViewController];

    self.vc1 = [[SubVC1 alloc] init];
    self.vc2 = [[SubVC2 alloc] init];
    self.vc3 = [[SubVC3 alloc] init];

   //添加子視圖控制器
    [self addChildViewController:self.vc1];
    [self addChildViewController:self.vc2];
    [self addChildViewController:self.vc3];

   //將子視圖控制器里面的視圖添加到容器視圖控制器中的不同位置,當(dāng)然您也可以用autolayout來進(jìn)行布局
    [self.view addSubview:self.vc1.view];
    self.vc1.view.frame = CGRectMake(x, x, x, x);
    
    [self.view addSubview:self.vc2.view];
    self.vc2.view.frame = CGRectMake(x, x, x, x);

    [self.view addSubview:self.vc3.view];
    self.vc3.view.frame = CGRectMake(x, x, x, x);
  
}

@end

  • 這個(gè)是一個(gè)復(fù)雜界面由不同的狀態(tài)變化驅(qū)動的場景

//ContainerVC.m

#import "SubVC1.h"
#import "SubVC2.h"
#import "SubVC3.h"


@interface  ContainerVC()
//這個(gè)功能根據(jù)不同的狀態(tài)進(jìn)行不同的處理

//狀態(tài)
@property(nonatomic, assign) int status;
@property(nonatomic, strong) UIViewController *currentVC;   //當(dāng)前的視圖控制器

@end

@implementation ContainerVC

- (void)viewDidLoad {
    [super viewDidLoad];
    self.status = 1;   //設(shè)置當(dāng)前狀態(tài)赊舶。
}

-(void)setStatus:(int)status
{
    if (_status == status)
        return;
    
    _status = status
    [self.currentVC.view removeFromSuperview];
    [self.currentVC removeFromParentViewController];
    
    self.currentVC = nil;
    Class cls = nil;
    switch (_status) {
        case 1:
            cls = [SubVC1 class];
            break;
        case 2:
           cls =  [SubVC2 class];
            break;
        case 3:
           cls =  [SubVC3 class];
            break;
        default:
           NSAssert(0, @"oops!");
            break;
    }

    self.currentVC = [[cls alloc] init];  //這里可以帶上容器視圖里面的狀態(tài)或者其他業(yè)務(wù)模型的參數(shù)來進(jìn)行初始化
    [self addChildViewController:self.currentVC];
    [self.view addSubview:self.currentVC.view];
    self.currentVC.view.frame = self.view.bounds;
    
}

上面的兩個(gè)場景都用到了子視圖控制器的相關(guān)API派任。我們再來看看iOS中的關(guān)于子視圖控制器的所有相關(guān)的API接口:

@interface UIViewController (UIContainerViewControllerProtectedMethods)

//得到一個(gè)父視圖控制器里面的所有子視圖控制器
@property(nonatomic,readonly) NSArray<__kindof UIViewController *> *childViewControllers;

//添加子視圖控制器
- (void)addChildViewController:(UIViewController *)childController;

//將自己從父視圖控制器中刪除
- (void)removeFromParentViewController;

//如果我們要添加一個(gè)子視圖控制器和刪除一個(gè)子視圖控制器同時(shí)執(zhí)行并且要有動畫效果時(shí)可以采用這個(gè)方法
- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion;

//如果容器控制器想控制子視圖控制器的呈現(xiàn)調(diào)用回調(diào)那么要重載容器控制器的shouldAutomaticallyForwardAppearanceMethods方法并返回NO摸恍。
//然后在適當(dāng)?shù)臅r(shí)候調(diào)用子視圖控制器的下面這兩個(gè)方法來實(shí)現(xiàn)呈現(xiàn)的自定義控制處理悉罕。
//這兩個(gè)方法是對子視圖控制器進(jìn)行的調(diào)用赤屋,并且要成對執(zhí)行。
- (void)beginAppearanceTransition:(BOOL)isAppearing animated:(BOOL)animated;
- (void)endAppearanceTransition;

// Override to return a child view controller or nil. If non-nil, that view controller's status bar appearance attributes will be used. If nil, self is used. Whenever the return values from these methods change, -setNeedsUpdatedStatusBarAttributes should be called.
@property(nonatomic, readonly, nullable) UIViewController *childViewControllerForStatusBarStyle;
@property(nonatomic, readonly, nullable) UIViewController *childViewControllerForStatusBarHidden;

// Call to modify the trait collection for child view controllers.
- (void)setOverrideTraitCollection:(nullable UITraitCollection *)collection forChildViewController:(UIViewController *)childViewController;
- (nullable UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController;

// Override to return a child view controller or nil. If non-nil, that view controller's preferred user interface style will be used. If nil, self is used. Whenever the preferredUserInterfaceStyle for a view controller has changed setNeedsUserInterfaceAppearanceUpdate should be called.
@property (nonatomic, readonly, nullable) UIViewController *childViewControllerForUserInterfaceStyle;

@end

@interface UIViewController (UIContainerViewControllerCallbacks)

//容器控制器可以重載這個(gè)方法來控制子視圖控制器中的視圖在添加到窗口以及從窗口刪除時(shí)子視圖控制器是否會自動調(diào)用viewWillAppear/viewDidAppear/viewWillDisappear/viewDidDisappear這幾個(gè)方法壁袄,默認(rèn)是YES类早。
//如果容器控制器重載這個(gè)方法返回NO時(shí)那么容器控制器就可以手動的讓子視圖控制器執(zhí)行對應(yīng)的呈現(xiàn)回調(diào)方法。
@property(nonatomic, readonly) BOOL shouldAutomaticallyForwardAppearanceMethods 

//子視圖控制器將要移動到父視圖控制器和已經(jīng)移動到父視圖控制器中時(shí)調(diào)用嗜逻,子視圖控制器可以重載這兩個(gè)方法
- (void)willMoveToParentViewController:(nullable UIViewController *)parent;
- (void)didMoveToParentViewController:(nullable UIViewController *)parent;

@end



控制器的派生

對控制邏輯的拆分所用到的設(shè)計(jì)模式是所謂的組合設(shè)計(jì)模式涩僻,其本質(zhì)是將功能分散到各個(gè)子模塊中然后組合起來實(shí)現(xiàn)一個(gè)完整的大功能。并不是所有的場景都適合通過拆分以及組合的方式來解決問題栈顷。我們考慮一下下面的兩個(gè)業(yè)務(wù)場景:

  • 兩個(gè)功能界面相似但是處理邏輯不同或者界面不同但是處理邏輯相似
    一般的情況下因?yàn)槭莾蓚€(gè)不同的功能也就是會用兩個(gè)不同的控制器來實(shí)現(xiàn)逆日,尤其是當(dāng)這個(gè)兩個(gè)功能屬于不同的模塊時(shí)更會如此。雖然兩個(gè)功能之間有很多相似的東西萄凤,我們?nèi)匀挥锌赡芡ㄟ^代碼復(fù)制拷貝的方式來進(jìn)行簡單處理室抽。但這并不是最佳的解決方案,因?yàn)橥ㄟ^代碼復(fù)制的話就有可能會出現(xiàn)更新不一致的情況靡努。我們也可以通過組合的形式來解決這個(gè)問題坪圾,但是組合的使用會在一定程度上增加代碼量以及共享參數(shù)之間的傳遞問題,因此最佳的解決方案就是采用類繼承的方法惑朦。就如當(dāng)功能中界面相同的兩個(gè)視圖控制器只是處理邏輯不相同兽泄,那么我們只需要派生出一個(gè)新的類并覆蓋掉基類的處理邏輯方法即可。
//VC1.h

@interface VC1:UIViewController
@end

......................
//VC1.m

@interface VC1()

@property(nonatomic, weak) UIButton *button;

@end

@implementation VC1

-(void)viewDidLoad
{
   [super viewDidLoad];

   [self.button addTarget:self  action:@selector(handleClick:) forControlEvents:UIControlEventTouchUpInside];
   //內(nèi)部調(diào)用某個(gè)方法
   [self fn1];
}

-(void)handleClick:(id)sender
{
    //... VC1的事件處理邏輯行嗤。
}

-(void)fn1
{
    //VC1的邏輯已日。
}

@end


基類里面的handleClick方法以及fn1方法都是專門用來處理VC1的邏輯和事件的,現(xiàn)在我們要構(gòu)造一個(gè)VC1的派生類VC2栅屏,派生類中界面相同但是事件處理邏輯以及一些方法則完全不同飘千。我們可以覆寫基類的對應(yīng)的方法來實(shí)現(xiàn)邏輯的改變。

//VC2.h

//VC2從VC1處派生
@interface VC2:VC1
@end

.......................................
//VC2.m

//這里的聲明一些派生類可以訪問基類的一些屬性和方法
@interface VC1()
@property(nonatomic, weak) UIButton *button;
@end


@implementation VC2

-(void)handleClick:(id)sender
{
    //... VC2的事件處理邏輯栈雳。
}

-(void)fn1
{
    //VC2的邏輯护奈。因?yàn)榛惖膕elf.button在這里有聲明,所以派生類是可以訪問self.button屬性的哥纫。
}

@end

通過上述的方法我們不用再通過代碼復(fù)制來構(gòu)建兩個(gè)不同的視圖控制器了霉旗,不同的場景啟用不同的視圖控制器即可。當(dāng)然我們也可以讓一個(gè)視圖控制器分別在兩個(gè)不同的場景里面使用蛀骇,使用一個(gè)控制器時(shí)還需要在您的代碼里面根據(jù)不同的場景做if,else的判斷而使用兩個(gè)控制器時(shí)則這些問題可以被規(guī)避厌秒,從而使得您的控制器代碼更加清晰簡單。

  • 兩個(gè)功能界面中其中一個(gè)功能界面除了實(shí)現(xiàn)另外一個(gè)功能界面的所有能力外還有一些附加的功能
    對于新增能力的場景來說也是一樣的擅憔,我們只需要在派生類中添加對應(yīng)的附加界面和處理邏輯即可鸵闪。考慮一個(gè)現(xiàn)實(shí)中的場景:在一般的電商類應(yīng)用中每個(gè)商品都會有一個(gè)商品詳情頁面暑诸,這個(gè)商品詳情一般從商品列表進(jìn)入蚌讼。當(dāng)某個(gè)用戶未登錄時(shí)進(jìn)去看到的商品詳情只是普通的商品詳情展示頁面辟灰,而一旦登錄后再進(jìn)入這個(gè)商品詳情頁面時(shí)就有可能會在商品詳情的某個(gè)部分比如底部出現(xiàn)這個(gè)用戶對這個(gè)商品的購買記錄信息。這個(gè)購買記錄是和用戶相關(guān)并且是可選的篡石,而商品詳情則和用戶無關(guān)芥喇。我們在架構(gòu)設(shè)計(jì)時(shí)就有可能設(shè)計(jì)出商品模塊和用戶模塊兩個(gè)部分。商品詳情屬于商品模塊凰萨,它是獨(dú)立于用戶的继控,我們不可能在商品詳情這個(gè)視圖控制器中帶上具有用戶屬性的一些界面和邏輯。解決的方法是我們建立一個(gè)商品詳情視圖控制器的派生類胖眷,然后在派生類面添加帶有用戶屬性的東西比如用戶的購買記錄信息等湿诊。這樣的設(shè)計(jì)思路也可以降低各個(gè)模塊之間的耦合度。
//GoodsVC.h

//商品詳情視圖控制器
@interface  GoodsVC:UIViewController
@end

...............................................
//GoodsVC.m


@implementation GoodsVC

//這里的邏輯只是商品相關(guān)的邏輯瘦材,里面并不會涉及到任何用戶相關(guān)的東西
@end

........................................
//GoodsWrapperVC.h

//帶用戶購買記錄的商品詳情視圖控制器
@interface GoodsWrapperVC:GoodsVC

  -(id)initWithUser:(User*)user;

@end

.....................................
// GoodsWrapperVC.m

@interface GoodsWrapperVC()
    //用戶購買記錄列表
   @property(weak) UITableView *userRecordTableView;
 
   @property(strong) User *user;
   @property(strong) NSArray<Record*> records;
@end

@implementation GoodsWrapperVC

  -(id)initWithUser:(User*)user
 {
     self = [self init];
     if (self != nil)
     {
         _user = user;
     }
     return self;
 }

-(void)viewDidLoad
{
    [super viewDidLoad];

    //這里添加獲取用戶購買記錄的請求邏輯厅须。
    __weak  GoodsWrapperVC *weakSelf = self;
    [self.user getRecords:^(NSArray<Record*> *records, NSError *error{
         [weakSelf reloadRecordList:records];     
    }];
}

-(void)reloadRecordList:(NSArray<Record*>) *records
{
      //因?yàn)橛行┥唐房赡懿o用戶購買記錄,所以這里特殊處理一下
      //用戶購買記錄列表也是可選并且是懶加載的食棕,這樣當(dāng)商品詳情并無用戶購買記錄時(shí)商品詳情就和基類界面保持一致朗和。
      if (records.count > 0)
      {
          self.records = records;
          if ( _userRecordTableView == nil)
          {
             UITableView *userRecordTableView = [[UITableView alloc] initWithFrame:CGRectMake(x,x,x,x)];
             userRecordTableView.delegate = self;
             userRecordTableView.dataSource = self;
             [self.view addSubview:userRecordTableView];
             _userRecordTableView = userRecordTableView;
         }
     }

    [self.userRecordTableView reloadData];
}

@end

.......................................
//GoodsListVC.m

//這里面是進(jìn)入商品詳情的商品列表視圖控制器中的事件處理代碼
-(void)handleShowGoodsDetail:(id)sender
{
      GoodsVC *goodsVC = nil;
     if (serviceSystem.user != nil && serviceSystem.user.isLogin)
     {
          goodsVC = [[GoodsWrapperVC alloc] initWithUser:serviceSystem.user];
     }
    else
    {
          goodsVC =[ [GoodsVC alloc] init];
    }

    [self.navigationController pushViewController:goodsVC animated:YES];
}

上面的進(jìn)入商品詳情的事件處理一般是在商品列表中進(jìn)行,那我們又會面臨同樣的問題簿晓,就是商品列表其實(shí)和用戶也是無關(guān)的眶拉,但是代碼里面確出現(xiàn)了用戶對象,這樣就出現(xiàn)了商品模塊和用戶模塊之間的耦合問題憔儿。怎么解決這個(gè)問題忆植?答案就是路由,也就是我們在處理界面跳轉(zhuǎn)時(shí)不直接構(gòu)建目標(biāo)視圖控制器而是通過一個(gè)中介者路由來實(shí)現(xiàn)界面的跳轉(zhuǎn)谒臼。關(guān)于路由來進(jìn)行頁面跳轉(zhuǎn)的解決方案網(wǎng)絡(luò)上已經(jīng)有很多的開源庫或者實(shí)現(xiàn)方式了朝刊,這里就不再贅述了。

視圖的更新以及和數(shù)據(jù)模型的交互

最后我們再來說說令人煩惱的UITableViewCell的更新方法蜈缤。UITableView是目前App中使用最多的控件之一拾氓。UITableViewCell是屬于視圖層次的對象。一般情況下某個(gè)UITableViewCell中展示的數(shù)據(jù)又來自于業(yè)務(wù)模型層的數(shù)據(jù)模型底哥。更新一個(gè)UITableViewCell要做的事情其實(shí)就是將數(shù)據(jù)模型的變化反饋到視圖中去咙鞍,這里面同時(shí)涉及了視圖和模型之間的耦合性問題。我們知道MVC中M和V之間是分別獨(dú)立的趾徽,他們之間是通過C來建立關(guān)聯(lián)续滋,因此上面的UITableViewCell的更新就由視圖控制器來完成。但是在實(shí)際中有可能UITableViewCell要顯示的東西非常之多孵奶,而且展示的邏輯也比較復(fù)雜疲酌,如果這些代碼都在視圖控制器來處理的話那么勢必造成控制器代碼膨脹。怎么去解決這個(gè)問題也是我們這一小節(jié)要思考的問題拒课。我將列出6種不同的解決方案來處理視圖數(shù)據(jù)更新的問題:

  1. 視圖提供屬性
    這種方法是UITableViewCell默認(rèn)的方法徐勃,在UITableViewCell中有imageVew、textLabel早像、detailTextLabel等幾個(gè)默認(rèn)的視圖屬性僻肖,一般情況下如果我們不定制UITableViewCell的話那么就可以在UITableView的delegate或者dataSource的回調(diào)處理中直接將數(shù)據(jù)模型的數(shù)據(jù)設(shè)置到這些屬性上。同理如果我們要自定義UITableViewCell時(shí)我們也可以讓UITableViewCell的派生類暴露出視圖屬性來解決問題卢鹦。這種場景一般用于界面不復(fù)雜而且邏輯比較簡單的情況臀脏。
//XXXTableViewCell.h

@interface XXXTableViewCell:UITableViewCell
  @property(weak) UILabel *nameLabel;
  @property(weak) UILabel *ageLabel;
  @property(weak) UILabel *addressLabel;
@end
 
  1. 視圖暴露方法
    在一些應(yīng)用場景中UITableViewCell中視圖屬性除了要更新內(nèi)容外,顯示的效果比如字體顏色等也有可能要更新冀自。如果這部分邏輯特別多的話我們就考慮為UITableViewCell的派生類提供一個(gè)更新視圖的方法來解決問題揉稚。通過提供方法的形式可以讓我們的UITableViewCell不需要暴露里面的視圖層次和視圖屬性給外面,提供的方法的參數(shù)都是一些數(shù)據(jù)即可熬粗,所有的視圖更新和樣式的設(shè)置都在方法內(nèi)部完成搀玖,這樣就可以減少在視圖控制器中的代碼量。也就是這種方法其實(shí)是將更新邏輯從視圖控制器移到視圖里面了驻呐。
//XXXTableViewCell.h

@interface XXXTableViewCell:UITableViewCell

//不再暴露視圖屬性了灌诅,但是提供一個(gè)更新視圖的方法
-(void)update:(NSString*)name  age:(int)age  address:(NSString*)address;

@end

......................................
XXXTableViewCell.m

@interface XXXTableViewCell()
  @property(weak) UILabel *nameLabel;
  @property(weak) UILabel *ageLabel;
  @property(weak) UILabel *addressLabel;
@end

 @implementation XXXTableViewCell

-(void)update:(NSString*)name  age:(int)age  address:(NSString*)address
{
   // 這里將參數(shù)的內(nèi)容更新到對應(yīng)的子視圖中去,并且這里面更新視圖的顯示樣式等等含末。
  self.nameLabel.text = name;
  self.ageLabel.text = [NSString stringWithFormat:@"%d", age];
  self.addressLabel.text = address;
}
@end

..........................................

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
      XXXTableViewCell *cell = ( XXXTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"test" forIndexPath:indexPath];
     //這里面讀取數(shù)據(jù)模型中的數(shù)據(jù)并調(diào)用視圖的update來實(shí)現(xiàn)界面的更新猜拾。
     XXXDataModel *data = ....
     [cell update:data.name age:data.age address:data.address];
     return cell;
}

通過視圖暴露更新方法的方案可以有效的減少視圖控制器中的代碼,而且可以隱藏視圖更新的實(shí)現(xiàn)佣盒,但是缺點(diǎn)是當(dāng)UITableViewCell的界面元素較多時(shí)則方法的參數(shù)將是非常的多挎袜。因此這個(gè)方法適合于界面元素不是很多的場景。

  1. 借助字典
    如果界面元素非常多時(shí)肥惭,但是我們又不想讓視圖和數(shù)據(jù)模型之間產(chǎn)生關(guān)聯(lián)盯仪,那么我們可以將UITableViewCell中的update方法改造為只接收一個(gè)參數(shù): 一個(gè)字典參數(shù)
   -(void)update:(NSDictionary*)params;

通過字典的形式來做數(shù)據(jù)的傳遞可以減少方法中參數(shù)的個(gè)數(shù),而且現(xiàn)在也有非常多的將數(shù)據(jù)模型轉(zhuǎn)化為字典的解決方案蜜葱。采用字典作為參數(shù)時(shí)會增加數(shù)據(jù)轉(zhuǎn)換的步驟磨总,以及在UITableViewCell中的update方法一定要了解字典有哪些數(shù)據(jù),并且外部調(diào)用時(shí)也要了解有哪些數(shù)據(jù)笼沥。在一定程度上字典的引入反而會使得代碼的可維護(hù)性降低蚪燕。

  1. 借助接口
    通過方法參數(shù)和字典是數(shù)據(jù)傳遞的兩種不同的方式。缺點(diǎn)是一旦界面變化時(shí)都需要手動的調(diào)整參數(shù)位置和個(gè)數(shù)奔浅。當(dāng)要更新的界面元素比較多時(shí)馆纳,我們還可以在更新方法中使用接口的形式來解決問題:
//一個(gè)獨(dú)立的接口定義文件
//XXXXItf.h

@protocol  XXXXItf
 @property  NSString *name;
 @property  int age;
 @property  NSString *address;
@end


..............................
定義的數(shù)據(jù)模型實(shí)現(xiàn)接口
//XXXDataModel.h
#import "XXXXItf.h"

//數(shù)據(jù)模型實(shí)現(xiàn)接口
@interface XXXXDataModel:NSObject<XXXXItf>
 @property  NSString *name;
 @property  int age;
 @property  NSString *address;
@end

..................................
XXXXTableViewCell的定義
#import "XXXXItf.h"

@interface XXXXTableViewCell:UITableViewCell

//這里面的入?yún)⑹且粋€(gè)接口協(xié)議。
-(void)update:(id<XXXXItf>)data;

@end

可以看出通過接口協(xié)議的形式可以解決方法參數(shù)過多以及字典作為參數(shù)的難維護(hù)性汹桦,通過接口定義的方法還可以解耦視圖層和模型層之間的強(qiáng)關(guān)聯(lián)問題鲁驶。采用接口的方式的缺點(diǎn)就是需要額外的定義出一個(gè)接口協(xié)議出來。

  1. 視圖持有模型
    通過接口協(xié)議可以解決視圖和數(shù)據(jù)模型的耦合性舞骆,其實(shí)在實(shí)際中我們的某些UITableViewCell就是專門用于展示某種數(shù)據(jù)模型的钥弯,從某種程度上說他們之間其實(shí)是有非常強(qiáng)烈的耦合性的径荔。因此這種情況下我們可以讓這個(gè)UITableViewCell持有這個(gè)數(shù)據(jù)模型也未嘗不是一個(gè)解決方案!脆霎!雖然MVC里面強(qiáng)調(diào)各個(gè)層次之間分離总处,但是在一些實(shí)際的場合中還是可以允許一些耦合場景出現(xiàn)的。當(dāng)我們用視圖持有數(shù)據(jù)模型時(shí)我們就可以不用提供一個(gè)update方法睛蛛,而是直接將數(shù)據(jù)模型賦值給視圖鹦马,視圖內(nèi)則可以重寫數(shù)據(jù)模型屬性的set方法來實(shí)現(xiàn)界面的更新。
//XXXXTableViewCell.h
#import "XXXXDataModel.h"

@interface XXXXTableViewCell:UITableViewCell

@property  XXXXDataModel *data;

@end

...........................
//XXXXTableViewCell.m

#import "XXXXTableViewCell.h"

@implementation XXXXTableViewCell

-(void)setXXXXDataModel:(XXXXDataModel*)data
{
     _data = data;
     //...這里更新界面的內(nèi)容
}

@end

................................

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
      XXXTableViewCell *cell = ( XXXTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"test" forIndexPath:indexPath];
      cell.data =  ...   這里面將數(shù)據(jù)模型賦值給視圖忆肾。
      return cell;
}


6.建立中間綁定類

上面的所有解決方案中要么就是將代碼邏輯放在視圖控制器中處理荸频,要么就將代碼邏輯移植到視圖中處理,并且有可能視圖還會持有數(shù)據(jù)模型的事情發(fā)生客冈。我們還可以將這部分更新的邏輯提取出來讓他即不在視圖中處理也不在視圖控制器中處理而是提供一個(gè)新的數(shù)據(jù)綁定類來解決這個(gè)問題旭从。通過數(shù)據(jù)綁定類來實(shí)現(xiàn)視圖和數(shù)據(jù)模型之間的交互也就是現(xiàn)在我們經(jīng)常說道的MVVM中的VM類所做的事情。

//XXXXTableViewCell.h

@interface XXXXTableViewCell:UITableViewCell

//暴露出視圖所具有的視圖屬性场仲。
@property UILabel *nameLabel;
@property UILabel *addressLabel;

@end

...............................................
//XXXXDataModel.h

@interface XXXXDataModel:NSObject
@property NSString *name;
@property  NSString *address;
@end

.............................................
//XXXXViewModel.h

@interface XXXXViewModel:NSObject

-(id)initView:(XXXXTableViewCell*)cell  withData:(XXXXDataModel*)data;

@end

.......................................
//XXXXViewModel.m

@implementation XXXXViewModel

-(id)initView:(XXXXTableViewCell*)cell  withData:(XXXXDataModel*)data
{
    self = [self init];
    if (self != nil)
    {
         cell.nameLabel.text = data.name;
         cel.addressLabel.text = data.address;
    }
    return self;
}
@end

...................................................
//某個(gè)視圖控制器

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
      XXXTableViewCell *cell = ( XXXTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"test" forIndexPath:indexPath];

      //假設(shè)這里取數(shù)據(jù)模型為data
      XXXXDataModel *data = ....
      //構(gòu)建出一個(gè)視圖模型綁定類遇绞。
      XXXXViewModel *vm = [ [XXXXViewModel alloc]   initView:cell withData:data];

      return cell;
}

上面的例子我們只是實(shí)現(xiàn)了一個(gè)簡單的ViewModel類,他的作用非常的明確就是實(shí)現(xiàn)數(shù)據(jù)到視圖之間的更新和綁定處理燎窘。從而使得視圖部分的代碼摹闽、視圖控制器中的代碼更加存粹和簡單。缺點(diǎn)就是因?yàn)橹虚g類的引入而使得代碼增加和維護(hù)成本增加褐健。

關(guān)于視圖控制器的構(gòu)建所要介紹的就是這些了付鹿,這又是一篇非常長的文章,而且還分為了上下兩個(gè)部分蚜迅,也許您不一定有耐心讀完整個(gè)部分舵匾。但是我期望這些東西在您閱讀后能讓你對視圖控制器和MVC有一個(gè)全新的認(rèn)識。在編碼前谁不,無論工作量有多少坐梯,我們都應(yīng)該要在提前有一個(gè)思路和思考。如何降低耦合性刹帕,如果使得我們的程序更加健壯和容易維護(hù)是我們思考的重點(diǎn)吵血。在移動開發(fā)領(lǐng)域iOS和Android所提供給開發(fā)者的都是基于MVC的框架體系,這么多年來這種框架體系一直沒有被改變那就證明他的生命還是比較頑強(qiáng)以及非常適合于目前移動開發(fā)偷溺。對于一個(gè)公司來說雖然開源的框架非常多蹋辅,而且引入也非常容易,但是我們應(yīng)該清醒的認(rèn)識到挫掏,這些非官方的第三方庫的引入一定要在你整個(gè)系統(tǒng)中的可替換性以及侵入性降到最低侦另!而且越底層的部分對第三方的依賴一定要最低。所以在設(shè)計(jì)整個(gè)應(yīng)用的架構(gòu)時(shí)可替換性以及標(biāo)準(zhǔn)性應(yīng)該成為重點(diǎn)要考慮的事情。


歡迎大家訪問我的github地址簡書地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末褒傅,一起剝皮案震驚了整個(gè)濱河市弃锐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌殿托,老刑警劉巖霹菊,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異碌尔,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)券敌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門唾戚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人待诅,你說我怎么就攤上這事叹坦。” “怎么了卑雁?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵募书,是天一觀的道長。 經(jīng)常有香客問我测蹲,道長莹捡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任扣甲,我火速辦了婚禮篮赢,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘琉挖。我一直安慰自己启泣,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布示辈。 她就那樣靜靜地躺著寥茫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪矾麻。 梳的紋絲不亂的頭發(fā)上纱耻,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機(jī)與錄音险耀,去河邊找鬼膝迎。 笑死,一個(gè)胖子當(dāng)著我的面吹牛胰耗,可吹牛的內(nèi)容都是我干的限次。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼卖漫!你這毒婦竟也來了费尽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤羊始,失蹤者是張志新(化名)和其女友劉穎旱幼,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體突委,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡柏卤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了匀油。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缘缚。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖敌蚜,靈堂內(nèi)的尸體忽然破棺而出桥滨,到底是詐尸還是另有隱情,我是刑警寧澤弛车,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布齐媒,位于F島的核電站,受9級特大地震影響纷跛,放射性物質(zhì)發(fā)生泄漏喻括。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一贫奠、第九天 我趴在偏房一處隱蔽的房頂上張望双妨。 院中可真熱鬧,春花似錦叮阅、人聲如沸刁品。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挑随。三九已至,卻和暖如春勒叠,著一層夾襖步出監(jiān)牢的瞬間兜挨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工眯分, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拌汇,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓弊决,卻偏偏與公主長得像噪舀,于是被迫代替她去往敵國和親魁淳。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 在我前面的兩篇文章里面分別對MVC框架中的M層的定義和構(gòu)建方法進(jìn)行了深入的介紹和探討与倡。這篇文章則是想深入的介紹一下...
    歐陽大哥2013閱讀 6,664評論 21 80
  • 視圖控制器管理著構(gòu)成應(yīng)用程序用戶界面中的一部分視圖界逛,其負(fù)責(zé)加載和處理這些視圖,管理與這些視圖的交互纺座,并協(xié)調(diào)視圖對其...
    漸z閱讀 4,303評論 0 1
  • 翻譯自“Collection View Programming Guide for iOS” 0 關(guān)于iOS集合視...
    lakerszhy閱讀 3,867評論 1 22
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,162評論 25 707
  • 國慶假期回家陪著家人赞别,某個(gè)下午他們夸我:某某從小就最懂事了〉УВ可是在聽到這句話時(shí)氯庆,我不再和小時(shí)候一樣蹭秋,會心里覺得開心...
    蘇木辛閱讀 643評論 12 7