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

歐陽大哥2013發表於2018-02-24

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

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

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

不同程式碼的構建時機

控制器類是一個功能的排程總控室,而且他還通過模板方法的設計模式提供給了我們在控制器的生命週期內各階段事件發生時的處理回撥。比如控制器構建時(init)、 檢視構建時(loadView)、檢視構建完成時(viewDidLoad)、檢視將要呈現到視窗前(viewWillAppear)、檢視已經呈現到視窗(viewDidAppear)、檢視將要從視窗刪除(viewWillDisappear)、檢視已經從視窗刪除(viewDidDisappear)、檢視被銷燬(viewDidUnload,這個方法在iOS6.0以後將不起作用了)、控制器被銷燬(dealloc)。為了實現功能,我們可能需要在上述的某個地方新增對應的處理程式碼。如何新增程式碼?以及在上述的模板方法中新增什麼樣的程式碼?就非常的關鍵了。在這裡面我想強調一點的是雖然控制器中擁有了一個view的根檢視屬性,但是控制器的生命週期一般要比根檢視的生命週期要長,而且有可能會出現一個功能在不同場景下的檢視呈現完全不一樣,或者有可能會通過重新構建檢視來實現一些換膚功能的場景。在iOS6以後的控制器中只提供了檢視構建以及構建完成的模板方法,但卻不再提供檢視被銷燬之前或者之後的模板方法,因此我們在loadView以及viewDidLoad中新增程式碼時就一定要考慮到這麼一點,因為他不像其他的方法一樣提供了互逆處理的機制。

  • 控制器初始化(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中最適合做的就是實現檢視和控制器之間的繫結以及控制器和業務模型之間的繫結操作。這裡不建議進行檢視的構建,以及一些涉及到整個控制器生命週期相關的處理。

  • 檢視的呈現和消失(viewWill/DidAppear,viewWill/DidDisappear) 檢視的呈現和消失有可能會被反覆呼叫。建議在這裡完成定時器、通知觀察者的新增和銷燬處理。一般來說定時器和觀察者都只是在介面被呈現時產生作用,而介面消失時則不處理,因此在這裡新增定時器和通知觀察者是最合適的。而且還有一個好處就是在這裡實現定時器和觀察者時不會產生迴圈引用而導致控制器不能被釋放的問題發生。

  • 控制器被銷燬(dealloc) 控制器被銷燬時表明控制器的生命週期已經完結了。一般情況下不需要新增特殊的程式碼,這裡一再強調的就是: 一定要在這裡把各種控制元件檢視中的delegate以及dataSource設定為nil! 一定要在這裡把各種控制元件檢視中的delegate以及dataSource設定為nil! 一定要在這裡把各種控制元件檢視中的delegate以及dataSource設定為nil!

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

懶載入

懶載入的目的是為了解決按需建立使用以及可選使用以及耗時建立的場景。在某種情況下使用懶載入可以加快展示的速度,懶載入可以將某些物件的建立時機延後。那麼是不是要將所有的物件的建立都採用懶載入的形式進行建立? 答案是否定的。 有不少同學都喜歡將控制器中的所有檢視的建立和佈局都通過懶載入的形式來完成,如下面的程式碼片段:

@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

相關文章