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

在個人iOS的MVC框架之控制層的構建(上)一文中介紹了一些控制層的構建方法,而這篇文章則繼續對一些方法進行展開討論。MVC被衆多開發者所詬病的C層的膨脹,究其緣由不外乎有以下幾點:git

  1. 全部視圖的構建和佈局代碼都在控制器中完成。有不少同窗不喜歡系統提供的Storyboard和XIB來構建視圖,而是喜歡經過代碼的形式來完成視圖界面佈局,而且一般這部分代碼都集中在loadView或者viewDidLoad或者經過懶加載的形式分散在各處。經過代碼來構建和佈局視圖的代碼量有可能會超過您視圖控制器總代碼量的50%。
  2. 對服務端的請求,每每就是包裝了一層很是薄的請求層,一般稱之爲APIService。 這部分代碼只是簡單封裝了對服務端URL的請求,同時經過一些報文轉數據模型的第三方框架直接將報文轉化爲數據模型並經過異步回調的形式回吐給控制器或者視圖。APIService的簡單實現卻增長了控制器的負荷,致使控制器除了要構建視圖而且請求網絡服務外還要擔負很是多的一部分業務邏輯的實現。
  3. 對於一些複雜展現邏輯的功能界面沒有進行合理拆解和有效設計致使全部代碼都在一個視圖控制器內完成,從而致使控制器膨脹臃腫。
  4. 在應用中最多使用的UITableView以及UITableViewCell中的數據更新的處理機制使用不恰當致使delegate中的方法實現異常的複雜,尤爲是那些複雜的UITableViewCell的更新處理不得當致使代碼混亂不堪。

能夠看出框架自己沒有問題,問題在於使用的人不瞭解或者不恰當的設計思想致使問題出現了。當出現問題時咱們首先應該反思的是本身哪裏不對而不是去怪別人哪裏不對。(這個雞湯撒得真LOW!!) 怎麼解決上面所說的致使C層膨脹的幾個問題呢?這也是這篇文章所要重點介紹的。github

不一樣代碼的構建時機

控制器類是一個功能的調度總控室,並且他還經過模板方法的設計模式提供給了咱們在控制器的生命週期內各階段事件發生時的處理回調。好比控制器構建時(init)、 視圖構建時(loadView)、視圖構建完成時(viewDidLoad)、視圖將要呈現到窗口前(viewWillAppear)、視圖已經呈現到窗口(viewDidAppear)、視圖將要從窗口刪除(viewWillDisappear)、視圖已經從窗口刪除(viewDidDisappear)、視圖被銷燬(viewDidUnload,這個方法在iOS6.0之後將不起做用了)、控制器被銷燬(dealloc)。爲了實現功能,咱們可能須要在上述的某個地方添加對應的處理代碼。如何添加代碼?以及在上述的模板方法中添加什麼樣的代碼?就很是的關鍵了。在這裏面我想強調一點的是雖然控制器中擁有了一個view的根視圖屬性,可是控制器的生命週期通常要比根視圖的生命週期要長,並且有可能會出現一個功能在不一樣場景下的視圖呈現徹底不同,或者有可能會經過從新構建視圖來實現一些換膚功能的場景。在iOS6之後的控制器中只提供了視圖構建以及構建完成的模板方法,但卻再也不提供視圖被銷燬以前或者以後的模板方法,所以咱們在loadView以及viewDidLoad中添加代碼時就必定要考慮到這麼一點,由於他不像其餘的方法同樣提供了互逆處理的機制。objective-c

  • 控制器初始化(init) 若是你的業務模型對象的生命週期和控制器的生命週期同樣,那麼建議將業務模型對象的構建放在控制器的初始化代碼中,固然前提是你的業務模型對象是一個輕量級的對象,若是你的業務模型對象的構建特別消耗時間那麼不建議放在控制器的初始化中構建而是經過懶加載或者在某個觸摸事件發生時再構建。若是你的控制器由多個子控制器組成,那麼子控制器的初始化工做也在這裏完成最佳。在控制器初始化時咱們還能夠初始化以及建立一些其餘的輕量級的屬性,這些屬性或者變量的生命週期和控制器的生命週期一致。數據庫

  • 視圖構建(loadView) 若是你的視圖是經過SB或者XIB來創建的,那麼恭喜你,你能夠省略這部分代碼。若是你是經過代碼來構建你的視圖,那麼你就有必要在這個地方添加你的視圖構建和佈局代碼。你須要重載loadView的方法,並在最好在這裏完成全部視圖的構建和佈局。若是你想複用默認的根視圖做爲本身的根視圖那麼你須要在構建你的其餘子視圖以前調用基類的loadView方法,而若是你想要徹底構建本身的根視圖以及子視圖體系那麼你就沒必要要調用基類的loadView方法。不少人都喜歡在viewDidLoad裏面進行視圖的構建,其實不是最佳的解決方案,由於根據字面意思viewDidLoad裏面添加的應該是視圖構建並加載完成後的一些處理邏輯。如何在loadView中更加優雅以及合理的構造界面佈局代碼,後面我將會給出一個具體解決方案。設計模式

-(void)loadView
{
   /*
   自定義根視圖的構建,不須要調用基類的方法。你也能夠直接在這裏將UIScrollView或者UITableView做爲根視圖。
   這樣就沒必要在默認的根視圖上再創建滾動視圖或者列表子視圖了。
   */
    self.view = [[UIView alloc] initWithFrame: [UIScreen mainScreen].bounds];

   //...創建其餘子視圖。

}

複製代碼
  • 事件綁定的代碼(viewDidLoad) 當視圖構建完畢後系統會調用viewDidLoad。所以您應該在這裏完成一些業務邏輯初始化的動做、業務模型服務接口的初始請求、一些控件的事件處理綁定的動做、視圖的delegate以及dataSource的設置。也就是這裏通常用來完成視圖和控制器之間的關聯處理以及控制器和業務模型的關聯處理。在viewDidLoad中最適合作的就是實現視圖和控制器之間的綁定以及控制器和業務模型之間的綁定操做。這裏不建議進行視圖的構建,以及一些涉及到整個控制器生命週期相關的處理。bash

  • 視圖的呈現和消失(viewWill/DidAppear,viewWill/DidDisappear) 視圖的呈現和消失有可能會被反覆調用。建議在這裏完成定時器、通知觀察者的添加和銷燬處理。通常來講定時器和觀察者都只是在界面被呈現時產生做用,而界面消失時則不處理,所以在這裏添加定時器和通知觀察者是最合適的。並且還有一個好處就是在這裏實現定時器和觀察者時不會產生循環引用而致使控制器不能被釋放的問題發生。服務器

  • 控制器被銷燬(dealloc) 控制器被銷燬時代表控制器的生命週期已經完結了。通常狀況下不須要添加特殊的代碼,這裏一再強調的就是: 必定要在這裏把各類控件視圖中的delegate以及dataSource設置爲nil! 必定要在這裏把各類控件視圖中的delegate以及dataSource設置爲nil! 必定要在這裏把各類控件視圖中的delegate以及dataSource設置爲nil!網絡

重要的事情說三遍!無論這些delegate是assign仍是weak的。架構

懶加載

懶加載的目的是爲了解決按需建立使用以及可選使用以及耗時建立的場景。在某種狀況下使用懶加載能夠加快展現的速度,懶加載能夠將某些對象的建立時機延後。那麼是否是要將全部的對象的建立都採用懶加載的形式進行建立? 答案是否認的。 有很多同窗都喜歡將控制器中的全部視圖的建立和佈局都經過懶加載的形式來完成,以下面的代碼片斷:app

@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];
         //有些同窗會在這裏添加附加代碼好比佈局相關的代碼
    }
    return _label;
}

-(UITableView*)tableView
{
     if (_tableView == nil)
    {
           _tableView = [UITableView new];
          [self.view addSubview:_tableView];
          _tableView.delegate = self;
         //有些同窗會在這裏添加附加代碼好比佈局相關的代碼
    }
    return _label;
}


-(void)viewDidLoad
{
    [super viewDidLoad];

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


@end

複製代碼

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

  • 視圖層次順序被打亂和代碼分散 由於視圖都是懶加載而且分散的,所以你不能從總體看出視圖層次結構是如何的,以及排列的順序是如何的。這就爲咱們的代碼閱讀以及調試和維護增長了困難。

  • 職責不明確 懶加載的主要做用是延遲建立,可是上述的視圖屬性的重寫卻已經超出了單純的建立的範疇了,除了建立視圖以外還實現了視圖添加到父視圖的功能以及進行佈局的功能,更有甚者還有可能實現其餘更加複雜的邏輯。這樣就會致使一個get屬性的實現承載的功能過多,嚴重的超過了一個方法所應承擔的責任。在使用時咱們只是簡單的將其當作一個讀取屬性來使用而且還有可能發生有些代碼重複的問題。

  • 莫名的問題和崩潰 懶加載視圖使得咱們的視圖屬性必需要設置爲strong類型的,並且代碼的實現是隻建立一次。若是由於某些緣由使得咱們的控制器裏面的全部視圖都須要從新建立(好比換膚)時那麼就有可能致使這個懶加載的視圖不會再次被建立而產生界面上莫名其妙的問題。更有甚者由於在懶加載中實現過多的代碼致使在某些地方訪問屬性時產生了崩潰。

所以不建議對一個控制器裏面的全部視圖構建都採用懶加載模式,視圖的構建和佈局應該在loadView中進行統一處理。懶加載的方式不能濫用,尤爲是視圖的構建代碼。咱們應該只對那些可選存在的對象以及那些有可能會影響性能的對象採用懶加載的方式來進行構建,而不是全部的對象都採用懶加載的形式來建立。同時還須要注意的就是若是必定要採用懶加載來實現對象的構建時,在懶加載中的代碼也應該儘可能的簡化,只須要實現建立部分的功能便可,而不要將一些非必要的邏輯代碼放入到懶加載的實現處,越多的邏輯實現,就會對使用着產生越多的限制和不肯定因素的發生。就以上面的例子來講使用者在調用self.label或者self.tableView時通常都只是將它們當作普通的屬性來使用,而不會去考慮它們的內部還進行了如此多的設置和處理(好比完成佈局和添加到父視圖中去)。這樣就可能會形成對這些屬性的使用不當而形成災難的後果。另外雖然你的視圖的構建是經過懶加載的形式來完成的,可是若是你在好比viewDidLoad中大量的訪問這些屬性時同樣的會產生視圖的構建操做,這樣其實和直接建立視圖對象是同樣的,並無起到任何優化性能的做用,並且這樣也是和懶加載的初衷是違背的。

咱們項目中的一個案例就是UITableView的建立使用的懶加載,裏面除了建立UITableView的實例外還在裏面設置了delegate的值以及其餘代碼邏輯。而這個UITableView又恰好是一個可選的顯示視圖。同時咱們又在視圖控制器的dealloc中對這個UITableView的delegate作了置爲nil的處理。結果這段代碼最終在線上出現了crash的狀況了。

簡化控制器中的視圖構建

視圖的構建有兩種方式:一種是經過Storyboard或者XIB以可視化的方式來構建;一種是經過程序代碼的方式來完成構建。兩種方法各有優劣。iOS以及Android系統都提供了強大的可視化界面佈局系統,而且兩者都是採用XML文件的方式來描述佈局。這種方式很是符合MVC中關於V的定義,視圖部分獨立存在而且井井有條。採用這種方式來構建你的視圖在必定程度上不會對你的控制器中的代碼產生污染以及致使你控制器中的代碼的膨脹。經過SB和XIB的使用就能夠簡化咱們對視圖部分的構建。在實踐中你會發現若是你是經過代碼來完成視圖的構建和佈局那麼這部分代碼就有可能超過你控制器50%的代碼行數。所以解決C層臃腫的一個方法就是將你的界面佈局的代碼都統一經過SB或者XIB來實現。有的同窗可能會說經過SB或者XIB的方式不利於協同開發,很容易形成合並時的代碼衝突。其實這是一個僞命題。通常狀況下咱們的功能都會拆分爲一個個視圖控制器來實現,而且一我的負責一個控制器。若是你用XIB來實現本身負責的那個控制器的界面佈局那麼又怎麼可能會產生代碼合併的衝突呢?即便是你用SB的方式來構建你的界面,雖然SB是將大部分界面都放在一個文件中來完成,可是在實踐中咱們的應用是能夠創建多個SB的。咱們能夠從功能類似性的角度出發將相同的功能放在一個SB中,不一樣大模塊創建不一樣的SB文件,這樣就能夠將一個SB根據應用模塊分解爲多個小SB。只要拆分的合理那麼在進行協同開發時就會最大限度的減小衝突的發生。隨着XCODE版本的更新,SB所具備的功能愈來愈強大,經過SB除了能實現界面佈局外包括邏輯的跳轉以及頁面的切換咱們都不須要編寫一行代碼。咱們其實能夠花一點時間靜下心來好好的去研究一下它,而不是一味的去拒絕和抵觸。君不見Android的開發者仍是喜歡經過XML而且基本是經過XML的編寫來完成界面佈局的呢。

也許上面的方式說不服你,你仍是經過代碼來構建佈局那一派的。沒有關係,本文探討的是如何解決控制器代碼膨脹的問題,而不是掀起派系之爭。那麼若是我就是要經過代碼的方式來完成界面佈局呢?畢竟經過代碼佈局的方式更加靈活和可配置性(犧牲了所見即所得性)。咱們知道在iOS的loadView的默認實現邏輯是首先會到SB或者XIB中去根據視圖控制器的類型去搜索是否有匹配的視圖佈局文件,若是有則將這個視圖佈局文件進行解析並構建對應的視圖層次樹並設置視圖控制器中的那些插座變量(IBOutlet)以及綁定視圖控件所關聯的事件處理器(IBAction)。若是沒有找到對應的佈局文件的話就會建立一個空白的根視圖(self.view)。可見loadView的主要目的就是爲了完成視圖的構建和佈局。所以當咱們經過代碼的方式來完成視圖的建立以及佈局時也應該將代碼邏輯放到這裏而不該該放到viewDidLoad中去。視圖的構建和佈局應該在一個地方統一進行而不該該經過懶加載的方式來將代碼分散到對各個視圖屬性進行重寫來完成。 在這裏我提供2種方法來實現視圖構建和佈局從控制器中分離或者歸類處理。

一. 採用分類擴展的方法

顧名思義,採用分類擴展的方法就是爲視圖控制器專門創建一個視圖構建和佈局的分類擴展。爲了將這部分代碼和控制器中其餘代碼分離,咱們能夠將視圖構建的分類擴展代碼單獨放到新文件中來實現。

//爲每一個控制器都創建一個 控制器名字+CreateView的頭文件
//XXXXViewController+CreateView.h
#import "XXXXViewController.h"

//定義一個擴展,擴展裏面定義全部控制器可能要用到的視圖屬性,定義屬性的方式就和經過SB或者XIB的方式一致。
@interface XXXXViewController ()
  @property(nonatomic, weak) IBOutlet UILabel *label;
  @property(nonatomic, weak) IBOutlet UIButton *button;
  @property(nonatomic, weak) IBOutlet UITableView *tableView;
   //...
@end

..................................
//代碼佈局的實現部分
//XXXXViewController+CreateView.m

#import "XXXXViewController+CreateView.h"

//這裏定義一個分類,分類只實現loadView的重載來完成視圖的構建和佈局
@implementation ViewController(CreateView)

-(void)loadView
{
    [super loadView];   //若是你想徹底自定義根視圖就能夠和上面我曾經列出的代碼同樣不調用父類的方法。

  //這裏完成全部子視圖的構建和佈局。由於視圖構建的代碼都是統一寫在一塊兒的,因此這裏面就能夠很方便的經過閱讀代碼的方式來看清怎麼視圖的佈局層次。

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

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

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

   //....

   //你能夠在這裏對上面全部的子視圖經過autolayout的方式來完成代碼佈局的編寫、也能夠在上面每一個視圖建立完成後就進行代碼佈局的編寫,這個沒有限制。

}

@end

複製代碼

上面的代碼能夠看出咱們單獨創建了一個擴展來定義全部視圖屬性,並創建了一個分類而且重載loadView來實現視圖的創建和佈局。代碼中咱們只作構建和佈局,而不作其餘的事情。好比UIButton的事件綁定以及UITableView的delegate和dataSource的設置都不在這裏面進行。這個分類就是一個很是存粹的代碼構建和界面佈局的代碼。這樣咱們看下面的控制器的主要代碼實現部分就很是的乾淨了。

//XXXXViewController.h

@interface  XXXXViewController

@end

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

//這裏導入分類爲了可以訪問其中的視圖屬性
#import XXXXViewController+CreateView.h

@implementation  XXXXViewController

-(void)viewDidLoad
{
    [super viewDidLoad];
    
     //這裏對按鈕綁定事件,對tableView指定委託和數據源,能夠看出在viewDidLoad裏面最適合作的事情就是創建視圖和控制器之間的關聯和綁定。
     [self.button  addTarget:self action:@selector(handleClick:) forControlEvents:UIControlEventTouchUpInside];
     self.tableView.delegate = self;
     self.tableView.dataSource = self;
}

@end

複製代碼

經過分類擴展的方法並不能減小控制器的代碼,可是卻能夠將特定的邏輯進行歸類分解,從而加強代碼的可閱讀性以及可維護性。由於關於視圖構建和佈局部分的代碼都拆分到其餘單獨的地方,而咱們的控制器的主要實現部分就能夠專心編寫控制邏輯了。甚至這種拆分的方法還能夠將工做一分爲二:一人專門負責界面佈局、一人專門負責控制邏輯的編寫。

二. 採用接口和消息轉發

視圖控制器經過對分類擴展來實現視圖構建的拆分,代碼仍是屬於視圖控制器的一部分。若是咱們想徹底實踐MVC中的V獨立存在而且能夠被複用的話,咱們能夠將視圖構建和佈局單獨抽象到一個視圖類中,而且經過接口定義和消息轉發的方法來創建控制器和視圖之間的聯繫。還記得我在上一篇文章裏面所提到的forwarding技術嗎?爲了實現視圖和控制器的分離咱們依然能夠採用這種方法來實現層次的分離。

  • 1.定義視圖屬性接口和視圖佈局類
//定義一個以控制器名開頭加View的協議和實現類。
//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.view addSubview:label];
           _label = label;

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

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

           //若是您用的是AutoLayout那麼您能夠在這裏添加布局約束的代碼。若是您是經過frame來進行佈局那麼請在layoutSubviews中進行子視圖的佈局處理。
   }

    return self;
}

-(void)layoutSubviews
{
    [super layoutSubviews];
    
    //若是你是經過frame來設置佈局那麼就能夠在這裏進行佈局的刷新。。
}


@end

複製代碼

能夠看出上述的代碼和控制器之間沒有任何關係,而且是獨立於控制器而存在的。視圖佈局類的做用就是隻用於視圖的佈局和構建以及展現,這種方式很是符合MVC中V的定義和實現。視圖構建完成後,須要對視圖進行佈局處理,您可使用AutoLayout方式來進行佈局也可使用frame方式來進行佈局。AutoLayout佈局是一種經過視圖之間的約束設置來實現佈局的方式,而frame方式則是蘋果早期的一種佈局方式。AutoLayout進行代碼佈局時,代碼量很是的多和複雜,這個問題在iOS9之後簡化了不少。還好有不少第三方的佈局類庫好比Mansory能夠有效的簡化佈局的難度。若是您的佈局要考慮性能問題以及想更加簡單的完成佈局那麼您能夠考慮使用筆者開源的界面佈局:MyLayout來實現界面佈局。

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


@interface XXXXViewController:UIViewController
@end

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

#import "XXXXViewControllerView.h" //這裏導入對應的佈局視圖類

//視圖控制器也須要實現XXXXViewControllerView接口。這樣視圖控制器中就能夠直接訪問視圖的一些屬性了。
@interface XXXXViewController ()<XXXXViewControllerView>

@end

@implementation XXXXViewController

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

//這個部分是實現的關鍵,來將控制器對視圖屬性協議的訪問分發到佈局視圖中去。
-(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方法進行重載來實現視圖控制器與視圖之間的綁定。而後咱們就能夠在任意位置來訪問視圖接口中的屬性了。綁定操做對於全部視圖控制器類來講都是一致的,因此你能夠經過一個宏定義的形式來實現上面的綁定操做:

//在某個公共的地方定義以下宏

#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"

//視圖控制器也須要實現XXXXViewControllerView接口。這樣視圖控制器中就能夠直接訪問視圖的一些屬性了。
@interface XXXXViewController ()<XXXXViewControllerView>

@end

@implementation XXXXViewController

//這裏直接用宏便可
BINDVIEW(XXXXViewControllerView)

//...這裏添加其餘代碼。

@end

複製代碼

上面的兩種對視圖構建和佈局進行分解的方式均可以解決在控制器中視圖代碼構建致使的膨脹問題。第一種方法本質上只是作了一些代碼拆分,並未實現控制器和視圖的徹底分離;第二種方法則徹底實現了視圖和控制器之間的分離,視圖的構建和佈局再也不依賴於控制器的存在,並且咱們甚至能夠對視圖進行復用,也就是說可讓多個控制器類複用一個視圖類中的代碼。這些控制器所實現的功能的展現效果同樣或者有微小的差異,可是事件處理邏輯則能夠徹底不同。第二種方法的實現機制更加體現了MVC中的層次關係以及V層構建的獨立性。所以無論你是經過SB或者XIB來構建您的視圖仍是經過代碼來構建您的視圖佈局,只要設計得當均可以很是有效的減小視圖控制器中對視圖依賴部分的代碼。

業務邏輯的下沉

視圖的構建部分的問題咱們已經成功解決。咱們再來探討一下薄服務層APIService的問題。在開始我曾經說過不少的架構設計人員都會以和服務器之間交互的全部API接口爲標準而設計出一套服務層API,咱們姑且叫他爲APIService。APIService會爲每個和服務端交互的接口都產生一個簡單的封裝,這個封裝只是完成了對向服務器請求的數據的打包以及URL連接的封裝以及將服務端返回的報文進行反序列化解包後直接經過block回調的方式返回給視圖控制器。

@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

複製代碼

咱們的視圖控制器中的任何一個網絡請求都是直接調用對應的請求方法,並對返回的Model數據模型進行加工處理,好比界面視圖數據刷新、文件處理、某些邏輯的調整等等。在這個過程當中控制器就無形之中承擔了業務邏輯的實現的工做,從而加劇了控制器中代碼的負擔。好比下面的代碼例子:

@ implementation XXXXViewController

//某個控制器的某個事件處理代碼。
-(void)handleClick:(id)sender
{
     //這部分代碼須要根據不一樣的狀態來請求不一樣的服務。假設這個狀態值保存到控制器中

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

                       //將model寫入某個文件中去。
                       // 將model的數據更新到某個視圖中去。
                       self.status = 2;   //更新狀態。
                       //其餘邏輯。。
                 }
                else
                {
                   //..錯誤處理。
                }   
         }];
   }
   else if (status == 2)
   {
        //彈出loading... 等待框,並請求另一個服務,返回的數據模型相同。
       [APIService requestYYY:^(XXXModel *model, NSError *error){
                   //銷燬loading... 框
                  if (error == nil)
                  {

                       //將model寫到文件中或者更新到數據庫中去。
                      // 將model的數據更新到某個視圖中去。
                      self.status = 1;   //更新狀態。
                      //其餘邏輯。。
                 }
                else
                {
                   //..錯誤處理。
                }   
         }];
   }   
}

@end
複製代碼

上面的代碼能夠看出控制器除了保存一些狀態外,而且根據不一樣的狀態還作了不一樣的網絡服務請求、文件的讀寫、狀態的更新、視圖的刷新操做等等其餘邏輯,這樣就致使了控制器的代碼很是的臃腫和難以維護。問題出在哪裏了呢?就是對模型層的理解產生了誤區,以及對服務層的定義產生了錯誤的使用。

真實的MVC中的M模型層所表明的是業務模型而非數據模型、業務模型的做用就是用來完成業務邏輯的具體實現。M層所要作的就是將一些和視圖展示無關以及和控制器無關的東西進行封裝處理,而只是給控制器提供出很是簡單易用的接口來供其調用。APIService的封裝是不符合邏輯和錯誤的封裝的!咱們知道任何系統都有一套完整的業務實現體系,這個實現體系不止在服務器端存在並且在客戶端上也存在,這二者之間是一致的。您能夠將業務實現的體系理解爲服務端實現體系的一個代理,代理和服務器服務之間通訊的紐帶就是接口報文。 咱們不能將客戶端的代理實現簡單理解爲只是對接口報文的簡單封裝,而是應該設計爲和服務端同樣具備完整架構體系的業務邏輯實現層,這我想也就是M層的本質所在吧。因此咱們在設計客戶端的M層時也必定要本着這個思想去設計,不能只是簡單的爲接口報文進行封裝,而且在控制器裏面去實現一些業務邏輯,而是應該將業務邏輯的實現、網絡的請求、報文的處理以一種抽象的以及和業務場景相關的東西統一的放在M模型層。這種理念和設計方法其實在個人另外兩篇介紹模型層構建的文章中都很是詳細的有說明。咱們應該在某種程度上將原先屬於在控制器中的邏輯進行下沉和分解來將邏輯的實現部分下移到模型層,這樣咱們在設計時就不會只是簡單的實現一個一個APIService中的方法。而是構建出一套完整的業務模型框架出來供控制器來使用了。仍是以上面的例子,解決的方法是咱們設計出一個業務模型類好比XXXXService,它內部封裝了狀態以及不用的網絡請求,以及一些文件讀寫的實現:

//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 //報文到模型的第三方轉換工具
                       //這裏寫入文件和數據庫
                       self.status = 2;  //這裏更新狀態。
                   }
                   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 //報文到模型的第三方轉換工具,假設URL2和URL1的數據模型都很是類似
                   //這裏作其餘的非視圖相關的邏輯。
                   self.status = 1;  //這裏更新狀態。
             }
             callback(retModel, error);
         }];
     }
}

@end

複製代碼

上面的業務模型代碼只是純粹的邏輯實現和具體的控制器無關和具體的視圖無關。那麼咱們如何在控制器中使用這個業務模型呢?

//XXXXViewController.m

 #import "XXXXService.h"
      
@interface XXXXViewController()
   @property(strong)   XXXXService *service;  //將業務模型以對象的形式保存起來,這裏咱們將看不到單例對象、也看不到平面的服務請求了,而是一個普通的對象。並且是一個真實的對象!!!
@end
   
 @implementation  XXXXViewController
          
//至於service的建立方式能夠在控制器初始化時建立,也能夠經過懶加載的方式進行建立。這裏咱們經過懶加載的形式進行建立。這裏纔是懶加載的最佳實踐      
 -(XXXService*)service
{
    if (_service == nil){
       _service = [XXXService new];
      } 
   return _service;
}            

//仍是原來的事件處理函數
-(void)handleClick:(id)sender
{
       //彈出loading... 等待框 ,並請求服務
       [self.service  request^(XXXModel* model, NSError *error){
             //銷燬loading... 框
             if (error == nil){   
                  // 將model的數據更新到某個視圖中去。
             }
            else
           {
              //..錯誤處理。
            }       
         }];
}
  
@end
複製代碼

能夠看出上面咱們的視圖控制器中的代碼已經很是的簡潔了,控制器再也不持有狀態,再也不作一些業務實現相關的處理了,只是簡單的調用業務模型提供的服務,並在回調中將數據模型中的數據更新視圖就能夠了。控制器再也不根據狀態去發起不一樣的請求,再也不處理任務業務實現相關的東西,並且業務模型也再也不是向之前那樣乾巴巴的使用單例或者使用類方法的形式提供給控制器調用,而是一個對象!一個真實的對象!一個面向對象中定義的對象來給控制器調用。經過對業務模型層的封裝使得咱們能夠在其餘的視圖控制器中也很是簡單的使用業務模型提供的服務來完成服務。從而精簡了控制器中的代碼和邏輯。在上面的例子中就能夠很明確的看出MVC中M的責任負責業務邏輯的實現,V的責任就是負責視圖的佈局和展現,而C層的責任就是負責將兩者關聯起來。

控制邏輯的拆分

經過對視圖類的封裝和解耦解決了視圖部分佔用控制器的代碼問題,經過對M層的正肯定義解決了控制器過多的處理業務邏輯實現的問題。咱們的控制器中的代碼將會獲得很大一部分的改善和精簡。咱們已經解決完了80%的問題了。但是即便如此咱們的控制器中的邏輯有可能仍是不少。

咱們在構建的某個視圖控制器中出現代碼膨脹的一個很是重要的緣由有多是這個功能的邏輯很是的複雜或者界面展現很是的複雜:

  • 一個界面中同時集成了衆多小的功能點,有些界面或者小功能點須要在特殊條件下才能展現出現。有些小功能界面是可選出現的。
  • 一個界面中分紅了好幾個區塊來展現,每一個區塊之間相對獨立,但又由於某些緣由要集成在同一個頁面之中。
  • 一個界面中受到某種狀態的控制,在不一樣狀態下可能會展現出徹底不一樣的界面和實現徹底不一樣的功能。

對於這些具備複雜邏輯的功能來講,若是設計的不得當就有可能出現控制器中的邏輯很是複雜和龐大。怎麼解決這些問題? 答案仍是分解。至於如何進行分解這就要具體問題具體分析了,這個就很是考驗架構設計人員的技術和業務功底了。咱們在這裏不探討如何進行業務拆分,而是討論控制器對業務拆分的支持能力。 當某個控制器中的邏輯過於龐大和複雜時能夠考慮將功能拆分爲多個子控制器來實現

在iOS5之後系統提供了對子控制器的支持能力,子控制器和父控制器同樣具備類似的生命週期內的各類方法的回調處理機制。子控制器的引入除了可以將視圖佈局進行拆分並且可以對處理邏輯進行拆分。在這種狀況下咱們把父視圖控制器稱爲容器控制器。容器控制器的做用更多的是對總體進行調度和控制,它可能不會再具體負責業務,具體的業務由子控制器來完成。就如上面列出的三種場景咱們均可以經過功能拆分的形式將一些邏輯拆分到子控制器來實現。我將分別用代碼來舉例上面的第二種和第三種場景的實現:

  • 這個是一個複雜界面由多個區域組成的實現場景
//ContainerVC.m

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


@interface  ContainerVC()
//這個功能被分爲3個獨立的區域進行展現和處理。
@property(nonatomic, strong)  SubVC1 *vc1;
@property(nonatomic, strong)  SubVC2 *vc2;
@property(nonatomic, strong)  SubVC3 *vc3;
@end

@implementation ContainerVC

- (void)viewDidLoad {
    [super viewDidLoad];
     
    //子視圖控制器的構建,您能夠在容器視圖控制器的初始化方法init中處理也能夠在viewDidLoad裏面進行處理。
    //這裏面先刪除是爲了防止有可能整個界面界面視圖被從新初始化的狀況發生
    [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];

   //將子視圖控制器裏面的視圖添加到容器視圖控制器中的不一樣位置,固然您也能夠用autolayout來進行佈局
    [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

複製代碼
  • 這個是一個複雜界面由不一樣的狀態變化驅動的場景
//ContainerVC.m

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


@interface  ContainerVC()
//這個功能根據不一樣的狀態進行不一樣的處理

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

@end

@implementation ContainerVC

- (void)viewDidLoad {
    [super viewDidLoad];
    self.status = 1;   //設置當前狀態。
}

-(void)setStatus:(int)status
{
    if (_status == status)
        return;
    
    [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];  //這裏能夠帶上容器視圖裏面的狀態或者其餘業務模型的參數來進行初始化
    [self addChildViewController:self.currentVC];
    [self.view addSubview:self.currentVC.view];
    self.currentVC.view.frame = self.view.bounds;
    
}

複製代碼

上面的兩個場景都用到了子視圖控制器的相關API。咱們再來看看iOS中的關於子視圖控制器的全部相關的API接口:

@interface UIViewController (UIContainerViewControllerProtectedMethods)

//獲得一個父視圖控制器裏面的全部子視圖控制器
@property(nonatomic,readonly) NSArray<__kindof UIViewController *> *childViewControllers;

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

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

//若是咱們要添加一個子視圖控制器和刪除一個子視圖控制器同時執行而且要有動畫效果時能夠採用這個方法
- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion;

//若是容器控制器想控制子視圖控制器的呈現調用回調那麼要重載容器控制器的shouldAutomaticallyForwardAppearanceMethods方法並返回NO。
//而後在適當的時候調用子視圖控制器的下面這兩個方法來實現呈現的自定義控制處理。
//這兩個方法是對子視圖控制器進行的調用,而且要成對執行。
- (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)

//容器控制器能夠重載這個方法來控制子視圖控制器中的視圖在添加到窗口以及從窗口刪除時子視圖控制器是否會自動調用viewWillAppear/viewDidAppear/viewWillDisappear/viewDidDisappear這幾個方法,默認是YES。
//若是容器控制器重載這個方法返回NO時那麼容器控制器就能夠手動的讓子視圖控制器執行對應的呈現回調方法。
@property(nonatomic, readonly) BOOL shouldAutomaticallyForwardAppearanceMethods 

//子視圖控制器將要移動到父視圖控制器和已經移動到父視圖控制器中時調用,子視圖控制器能夠重載這兩個方法
- (void)willMoveToParentViewController:(nullable UIViewController *)parent;
- (void)didMoveToParentViewController:(nullable UIViewController *)parent;

@end



複製代碼

控制器的派生

對控制邏輯的拆分所用到的設計模式是所謂的組合設計模式,其本質是將功能分散到各個子模塊中而後組合起來實現一個完整的大功能。並非全部的場景都適合經過拆分以及組合的方式來解決問題。咱們考慮一下下面的兩個業務場景:

  • 兩個功能界面類似可是處理邏輯不一樣或者界面不一樣可是處理邏輯類似 通常的狀況下由於是兩個不一樣的功能也就是會用兩個不一樣的控制器來實現,尤爲是當這個兩個功能屬於不一樣的模塊時更會如此。雖然兩個功能之間有不少類似的東西,咱們仍然有可能經過代碼複製拷貝的方式來進行簡單處理。但這並非最佳的解決方案,由於經過代碼複製的話就有可能會出現更新不一致的狀況。咱們也能夠經過組合的形式來解決這個問題,可是組合的使用會在必定程度上增長代碼量以及共享參數之間的傳遞問題,所以最佳的解決方案就是採用類繼承的方法。就如當功能中界面相同的兩個視圖控制器只是處理邏輯不相同,那麼咱們只須要派生出一個新的類並覆蓋掉基類的處理邏輯方法便可。
//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];
   //內部調用某個方法
   [self fn1];
}

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

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

@end


複製代碼

基類裏面的handleClick方法以及fn1方法都是專門用來處理VC1的邏輯和事件的,如今咱們要構造一個VC1的派生類VC2,派生類中界面相同可是事件處理邏輯以及一些方法則徹底不一樣。咱們能夠覆寫基類的對應的方法來實現邏輯的改變。

//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的邏輯。由於基類的self.button在這裏有聲明,因此派生類是能夠訪問self.button屬性的。
}

@end

複製代碼

經過上述的方法咱們不用再經過代碼複製來構建兩個不一樣的視圖控制器了,不一樣的場景啓用不一樣的視圖控制器便可。固然咱們也可讓一個視圖控制器分別在兩個不一樣的場景裏面使用,使用一個控制器時還須要在您的代碼裏面根據不一樣的場景作if,else的判斷而使用兩個控制器時則這些問題能夠被規避,從而使得您的控制器代碼更加清晰簡單。

  • 兩個功能界面中其中一個功能界面除了實現另一個功能界面的全部能力外還有一些附加的功能 對於新增能力的場景來講也是同樣的,咱們只須要在派生類中添加對應的附加界面和處理邏輯便可。考慮一個現實中的場景:在通常的電商類應用中每一個商品都會有一個商品詳情頁面,這個商品詳情通常從商品列表進入。當某個用戶未登陸時進去看到的商品詳情只是普通的商品詳情展現頁面,而一旦登陸後再進入這個商品詳情頁面時就有可能會在商品詳情的某個部分好比底部出現這個用戶對這個商品的購買記錄信息。這個購買記錄是和用戶相關而且是可選的,而商品詳情則和用戶無關。咱們在架構設計時就有可能設計出商品模塊和用戶模塊兩個部分。商品詳情屬於商品模塊,它是獨立於用戶的,咱們不可能在商品詳情這個視圖控制器中帶上具備用戶屬性的一些界面和邏輯。解決的方法是咱們創建一個商品詳情視圖控制器的派生類,而後在派生類面添加帶有用戶屬性的東西好比用戶的購買記錄信息等。這樣的設計思路也能夠下降各個模塊之間的耦合度。
//GoodsVC.h

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

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


@implementation GoodsVC

//這裏的邏輯只是商品相關的邏輯,裏面並不會涉及到任何用戶相關的東西
@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
{
      //由於有些商品可能並沒有用戶購買記錄,因此這裏特殊處理一下
      //用戶購買記錄列表也是可選而且是懶加載的,這樣當商品詳情並沒有用戶購買記錄時商品詳情就和基類界面保持一致。
      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

//這裏面是進入商品詳情的商品列表視圖控制器中的事件處理代碼
-(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];
}

複製代碼

上面的進入商品詳情的事件處理通常是在商品列表中進行,那咱們又會面臨一樣的問題,就是商品列表其實和用戶也是無關的,可是代碼裏面確出現了用戶對象,這樣就出現了商品模塊和用戶模塊之間的耦合問題。怎麼解決這個問題?答案就是路由,也就是咱們在處理界面跳轉時不直接構建目標視圖控制器而是經過一箇中介者路由來實現界面的跳轉。關於路由來進行頁面跳轉的解決方案網絡上已經有不少的開源庫或者實現方式了,這裏就再也不贅述了。

視圖的更新以及和數據模型的交互

最後咱們再來講說使人煩惱的UITableViewCell的更新方法。UITableView是目前App中使用最多的控件之一。UITableViewCell是屬於視圖層次的對象。通常狀況下某個UITableViewCell中展現的數據又來自於業務模型層的數據模型。更新一個UITableViewCell要作的事情其實就是將數據模型的變化反饋到視圖中去,這裏面同時涉及了視圖和模型之間的耦合性問題。咱們知道MVC中M和V之間是分別獨立的,他們之間是經過C來創建關聯,所以上面的UITableViewCell的更新就由視圖控制器來完成。可是在實際中有可能UITableViewCell要顯示的東西很是之多,並且展現的邏輯也比較複雜,若是這些代碼都在視圖控制器來處理的話那麼勢必形成控制器代碼膨脹。怎麼去解決這個問題也是咱們這一小節要思考的問題。我將列出6種不一樣的解決方案來處理視圖數據更新的問題:

  1. 視圖提供屬性 這種方法是UITableViewCell默認的方法,在UITableViewCell中有imageVew、textLabel、detailTextLabel等幾個默認的視圖屬性,通常狀況下若是咱們不定製UITableViewCell的話那麼就能夠在UITableView的delegate或者dataSource的回調處理中直接將數據模型的數據設置到這些屬性上。同理若是咱們要自定義UITableViewCell時咱們也可讓UITableViewCell的派生類暴露出視圖屬性來解決問題。這種場景通常用於界面不復雜並且邏輯比較簡單的狀況。
//XXXTableViewCell.h

@interface XXXTableViewCell:UITableViewCell
  @property(weak) UILabel *nameLabel;
  @property(weak) UILabel *ageLabel;
  @property(weak) UILabel *addressLabel;
@end
 
複製代碼
  1. 視圖暴露方法 在一些應用場景中UITableViewCell中視圖屬性除了要更新內容外,顯示的效果好比字體顏色等也有可能要更新。若是這部分邏輯特別多的話咱們就考慮爲UITableViewCell的派生類提供一個更新視圖的方法來解決問題。經過提供方法的形式可讓咱們的UITableViewCell不須要暴露裏面的視圖層次和視圖屬性給外面,提供的方法的參數都是一些數據便可,全部的視圖更新和樣式的設置都在方法內部完成,這樣就能夠減小在視圖控制器中的代碼量。也就是這種方法實際上是將更新邏輯從視圖控制器移到視圖裏面了。
//XXXTableViewCell.h

@interface XXXTableViewCell:UITableViewCell

//再也不暴露視圖屬性了,可是提供一個更新視圖的方法
-(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
{
   // 這裏將參數的內容更新到對應的子視圖中去,而且這裏面更新視圖的顯示樣式等等。
  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];
     //這裏面讀取數據模型中的數據並調用視圖的update來實現界面的更新。
     XXXDataModel *data = ....
     [cell update:data.name age:data.age address:data.address];
     return cell;
}
複製代碼

經過視圖暴露更新方法的方案能夠有效的減小視圖控制器中的代碼,並且能夠隱藏視圖更新的實現,可是缺點是當UITableViewCell的界面元素較多時則方法的參數將是很是的多。所以這個方法適合於界面元素不是不少的場景。

  1. 藉助字典 若是界面元素很是多時,可是咱們又不想讓視圖和數據模型之間產生關聯,那麼咱們能夠將UITableViewCell中的update方法改造爲只接收一個參數: 一個字典參數
-(void)update:(NSDictionary*)params;
複製代碼

經過字典的形式來作數據的傳遞能夠減小方法中參數的個數,並且如今也有很是多的將數據模型轉化爲字典的解決方案。採用字典做爲參數時會增長數據轉換的步驟,以及在UITableViewCell中的update方法必定要了解字典有哪些數據,而且外部調用時也要了解有哪些數據。在必定程度上字典的引入反而會使得代碼的可維護性下降。

  1. 藉助接口 經過方法參數和字典是數據傳遞的兩種不一樣的方式。缺點是一旦界面變化時都須要手動的調整參數位置和個數。當要更新的界面元素比較多時,咱們還能夠在更新方法中使用接口的形式來解決問題:
//一個獨立的接口定義文件
//XXXXItf.h

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


..............................
定義的數據模型實現接口
//XXXDataModel.h
#import "XXXXItf.h"

//數據模型實現接口
@interface XXXXDataModel:NSObject<XXXXItf>
 @property  NSString *name;
 @property  int age;
 @property  NSString *address;
@end

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

@interface XXXXTableViewCell:UITableViewCell

//這裏面的入參是一個接口協議。
-(void)update:(id<XXXXItf>)data;

@end

複製代碼

能夠看出經過接口協議的形式能夠解決方法參數過多以及字典做爲參數的難維護性,經過接口定義的方法還能夠解耦視圖層和模型層之間的強關聯問題。採用接口的方式的缺點就是須要額外的定義出一個接口協議出來。

  1. 視圖持有模型 經過接口協議能夠解決視圖和數據模型的耦合性,其實在實際中咱們的某些UITableViewCell就是專門用於展現某種數據模型的,從某種程度上說他們之間實際上是有很是強烈的耦合性的。所以這種狀況下咱們可讓這個UITableViewCell持有這個數據模型也何嘗不是一個解決方案!!雖然MVC裏面強調各個層次之間分離,可是在一些實際的場合中仍是能夠容許一些耦合場景出現的。當咱們用視圖持有數據模型時咱們就能夠不用提供一個update方法,而是直接將數據模型賦值給視圖,視圖內則能夠重寫數據模型屬性的set方法來實現界面的更新。
//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;
     //...這裏更新界面的內容
}

@end

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

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


複製代碼

6.創建中間綁定類

上面的全部解決方案中要麼就是將代碼邏輯放在視圖控制器中處理,要麼就將代碼邏輯移植到視圖中處理,而且有可能視圖還會持有數據模型的事情發生。咱們還能夠將這部分更新的邏輯提取出來讓他即不在視圖中處理也不在視圖控制器中處理而是提供一個新的數據綁定類來解決這個問題。經過數據綁定類來實現視圖和數據模型之間的交互也就是如今咱們常常說道的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

...................................................
//某個視圖控制器

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

      //假設這裏取數據模型爲data
      XXXXDataModel *data = ....
      //構建出一個視圖模型綁定類。
      XXXXViewModel *vm = [ [XXXXViewModel alloc]   initView:cell withData:data];

      return cell;
}

複製代碼

上面的例子咱們只是實現了一個簡單的ViewModel類,他的做用很是的明確就是實現數據到視圖之間的更新和綁定處理。從而使得視圖部分的代碼、視圖控制器中的代碼更加存粹和簡單。缺點就是由於中間類的引入而使得代碼增長和維護成本增長。

關於視圖控制器的構建所要介紹的就是這些了,這又是一篇很是長的文章,並且還分爲了上下兩個部分,也許您不必定有耐心讀完整個部分。可是我指望這些東西在您閱讀後能讓你對視圖控制器和MVC有一個全新的認識。在編碼前,不管工做量有多少,咱們都應該要在提早有一個思路和思考。如何下降耦合性,若是使得咱們的程序更加健壯和容易維護是咱們思考的重點。在移動開發領域iOS和Android所提供給開發者的都是基於MVC的框架體系,這麼多年來這種框架體系一直沒有被改變那就證實他的生命仍是比較頑強以及很是適合於目前移動開發。對於一個公司來講雖然開源的框架很是多,並且引入也很是容易,可是咱們應該清醒的認識到,這些非官方的第三方庫的引入必定要在你整個系統中的可替換性以及侵入性降到最低!並且越底層的部分對第三方的依賴必定要最低。因此在設計整個應用的架構時可替換性以及標準性應該成爲重點要考慮的事情。


歡迎你們訪問個人github地址, 關注歐陽大哥2013

相關文章
相關標籤/搜索