談?wù)?MVX 中的 Controller

Follow GitHub: Draveness

在前兩篇文章中,我們已經(jīng)對 iOS 中的 Model 層以及 View 層進行了分析,劃分出了它們的具體職責,其中 Model 層除了負責數(shù)據(jù)的持久存儲、緩存工作,還要負責所有 HTTP 請求的發(fā)出等工作;而對于 View 層的職責,我們并沒有做出太多的改變,有的只是細分其內(nèi)部的視圖種類,以及分離 UIView 不應(yīng)該具有的屬性和功能。

如果想要具體了解筆者對 Model 層以及 View 層的理解和設(shè)計,這是前面兩篇文章的鏈接:談?wù)?MVX 中的 Model 層談?wù)?MVX 中的 View 層

這是 MVX 系列的第三篇文章,而這篇文章準備介紹整個 MVX 中無法避免的話題,也就是 X 這一部分。

X 是什么

在進入正題之前,我們首先要知道這里的 X 到底是什么?無論是在 iOS 開發(fā)領(lǐng)域還是其它的領(lǐng)域,造出了一堆又一堆的名詞,除了我們最常見的 MVC 和 MVVM 以及 Android 中的 MVP 還有一些其他的奇奇怪怪的名詞。

MVC-MVVM-MVP

模型層和視圖層是整個客戶端應(yīng)用不可分割的一部分,它們的職責非常清楚,一個用于處理本地數(shù)據(jù)的獲取以及存儲,另一個用于展示內(nèi)容、接受用戶的操作與事件;在這種情況下,整個應(yīng)用中的其它功能和邏輯就會被自然而然的扔到 X 層中。

這個 X 在 MVC 中就是 Controller 層、在 MVVM 中就是 ViewModel 層,而在 MVP 中就是 Presenter 層,這篇文章介紹的就是 MVC 中的控制器層 Controller。

臃腫的 Controller

從 Cocoa Touch 框架使用十年以來,iOS 開發(fā)者就一直遵循框架中的設(shè)計,使用 Model-View-Controller 的架構(gòu)模式開發(fā) iOS 應(yīng)用程序,下面也是對 iOS 中 MVC 的各層交互的最簡單的說明。

Model-View-Controlle

iOS 中的 Model 層大多為 NSObject 的子類,也就是一個簡單的對象;所有的 View 層對象都是 UIView 的子類;而 Controller 層的對象都是 UIViewController 的實例。

我們在這一節(jié)中主要是介紹 UIViewController 作為 Controller 層中的最重要的對象,它具有哪些職責,它與 Model 以及 View 層是如何進行交互的。

總體來說,Controller 層要負責以下的問題(包括但不僅限于):

  1. 管理根視圖的生命周期和應(yīng)用生命周期
  2. 負責將視圖層的 UIView 對象添加到持有的根視圖上;
  3. 負責處理用戶行為,比如 UIButton 的點擊以及手勢的觸發(fā);
  4. 儲存當前界面的狀態(tài);
  5. 處理界面之間的跳轉(zhuǎn);
  6. 作為 UITableView 以及其它容器視圖的代理以及數(shù)據(jù)源;
  7. 負責 HTTP 請求的發(fā)起;

除了上述職責外,UIViewController 對象還可能需要處理業(yè)務(wù)邏輯以及各種復(fù)雜的動畫,這也就是為什么在 iOS 應(yīng)用中的 Controller 層都非常龐大、臃腫的原因了,而 MVVM、MVP 等架構(gòu)模式的目的之一就是減少單一 Controller 中的代碼。

管理生命周期

Controller 層作為整個 MVC 架構(gòu)模式的中樞,承擔著非常重要的職責,不僅要與 Model 以及 View 層進行交互,還有通過 AppDelegate 與諸多的應(yīng)用生命周期打交道。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions;
- (void)applicationWillResignActive:(UIApplication *)application;
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler;

雖然與應(yīng)用生命周期溝通的工作并不在單獨的 Controller 中,但是 self.window.rootController 作為整個應(yīng)用程序界面的入口,還是需要在 AppDelegate 中進行設(shè)置。

除此之外,由于每一個 UIViewController 都持有一個視圖對象,所以每一個 UIViewController 都需要負責這個根視圖的加載、布局以及生命周期的管理,包括:

- (void)loadView;

- (void)viewWillLayoutSubviews;
- (void)viewDidLayoutSubviews;

- (void)viewDidLoad;
- (void)viewWillAppear:(BOOL)animated;
- (void)viewDidAppear:(BOOL)animated;

除了負責應(yīng)用生命周期和視圖生命周期,控制器還要負責展示內(nèi)容和布局。

負責展示內(nèi)容和布局

由于每一個 UIViewController 都持有一個 UIView 的對象,所以視圖層的對象想要出現(xiàn)在屏幕上,必須成為這個根視圖的子視圖,也就是說視圖層完全沒有辦法脫離 UIViewController 而單獨存在,其一方面是因為 UIViewController 隱式的承擔了應(yīng)用中路由的工作,處理界面之間的跳轉(zhuǎn),另一方面就是 UIViewController 的設(shè)計導(dǎo)致了所有的視圖必須加在其根視圖上才能工作。

Controller-RootVie

我們來看一段 UIViewController 中關(guān)于視圖層的簡單代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupUI];
}

- (void)setupUI {
    _backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"backgroundView"]];

    _registerButton = [[UIButton alloc] init];
    [_registerButton setTitle:@"注冊" forState:UIControlStateNormal];
    [_registerButton setTitleColor:UIColorFromRGB(0x00C3F3) forState:UIControlStateNormal];
    [_registerButton addTarget:self action:@selector(registerButtonTapped:) forControlEvents:UIControlEventTouchUpInside];

    [self.view addSubview:_backgroundView];
    [self.view addSubview:_registerButton];

    [_backgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.mas_equalTo(self.view);
    }];
    [_registerButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.mas_equalTo(CGSizeMake(140, 45));
        make.bottom.mas_equalTo(self.view).offset(-25);
        make.left.mas_equalTo(self.view).offset(32);
    }];
}

在這個歡迎界面以及大多數(shù)界面中,由于視圖層的代碼非常簡單,我們很多情況下并不會去寫一個單獨的 UIView 類,而是將全部的視圖層代碼丟到了 UIViewController 中,這種情況下甚至也沒有 Model 層,Controller 承擔了全部的工作。

Controller-Only

上述的代碼對視圖進行了初始化,將需要展示的視圖加到了自己持有的根視圖中,然后對這些視圖進行簡單的布局。

當然我們也可以將視圖的初始化單獨放到一個類中,不過仍然需要處理 DRKBackgroundView 視圖的布局等問題。

- (void)setupUI {
    DRKBackgroundView *backgroundView = [[DRKBackgroundView alloc] init];
    [backgroundView.registerButton addTarget:self action:@selector(registerButtonTapped:) forControlEvents:UIControlEventTouchUpInside];

    [self.view addSubview:backgroundView];

    [backgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.mas_equalTo(self.view);
    }];
}

UIViewController 的這種中心化的設(shè)計雖然簡單,不過也導(dǎo)致了很多代碼沒有辦法真正解耦,視圖層必須依賴于 UIViewController 才能展示。

惰性初始化

當然,很多人在 Controller 中也會使用惰性初始化的方式生成 Controller 中使用的視圖,比如:

@interface ViewController ()

@property (nonatomic, strong) UIImageView *backgroundView;

@end

@implementation ViewController

- (UIImageView *)backgroundView {
    if (!_backgroundView) {
        _backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"backgroundView"]];
    }
    return _backgroundView;
}

@end

這樣在 -viewDidLoad 方法中就可以直接處理視圖的視圖層級以及布局工作:

- (void)viewDidLoad {
    [super viewDidLoad];

    [self.view addSubview:self.backgroundView];

    [self.backgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.mas_equalTo(self.view);
    }];
}

惰性初始化的方法與其他方法其實并沒有什么絕對的優(yōu)劣,兩者的選擇只是對于代碼規(guī)范的一種選擇,我們所需要做的,只是在同一個項目中將其中一種做法堅持到底。

處理用戶行為

UIViewController 中處理用戶的行為是經(jīng)常需要做的事情,這部分代碼不能放到視圖層或者其他地方的原因是,用戶的行為經(jīng)常需要與 Controller 的上下文有聯(lián)系,比如,界面的跳轉(zhuǎn)需要依賴于 UINavigationController 對象:

- (void)registerButtonTapped:(UIButton *)button {
    RegisterViewController *registerViewController = [[RegisterViewController alloc] init];
    [self.navigationController pushViewController:registerViewController animated:YES];
}

而有的用戶行為需要改變模型層的對象、持久存儲數(shù)據(jù)庫中的數(shù)據(jù)或者發(fā)出網(wǎng)絡(luò)請求,主要因為我們要秉承著 MVC 的設(shè)計理念,避免 Model 層和 View 層的直接耦合。

存儲當前界面的狀態(tài)

在 iOS 中,我們經(jīng)常需要處理表視圖,而在現(xiàn)有的大部分表視圖在加載內(nèi)容時都會進行分頁,使用下拉刷新和上拉加載的方式獲取新的條目,而這就需要在 Controller 層保存當前顯示的頁數(shù):

@interface TableViewController ()

@property (nonatomic, assign) NSUInteger currentPage;

@end

只有保存在了當前頁數(shù)的狀態(tài),才能在下次請求網(wǎng)絡(luò)數(shù)據(jù)時傳入合適的頁數(shù),最后獲得正確的資源,當然哪怕當前頁數(shù)是可以計算出來的,比如通過當前的 Model 對象的數(shù)和每頁個 Model 數(shù),在這種情況下,我們也需要在當前 Controller 中 Model 數(shù)組的值。

@interface TableViewController ()

@property (nonatomic, strong) NSArray<Model *> *models;

@end

在 MVC 的設(shè)計中,這種保存當前頁面狀態(tài)的需求是存在的,在很多復(fù)雜的頁面中,我們也需要維護大量的狀態(tài),這也是 Controller 需要承擔的重要職責之一。

處理界面之間的跳轉(zhuǎn)

由于 Cocoa Touch 提供了 UINavigationControllerUITabBarController 這兩種容器 Controller,所以 iOS 中界面跳轉(zhuǎn)的這一職責大部分都落到了 Controller 上。

UINavigationController-UITabBarControlle

iOS 中總共有三種界面跳轉(zhuǎn)的方式:

  • UINavigationController 中使用 push 和 pop 改變棧頂?shù)?UIViewController 對象;
  • UITabBarController 中點擊各個 UITabBarItem 實現(xiàn)跳轉(zhuǎn);
  • 使用所有的 UIViewController 實例都具有的 -presentViewController:animated:completion 方法;

因為所有的 UIViewController 的實例都可以通過 navigationController 這一屬性獲取到最近的 UINavigationController 對象,所以我們不可避免的要在 Controller 層對界面之間的跳轉(zhuǎn)進行操作。

當然,我們也可以引入 Router 路由對 UIViewController 進行注冊,在訪問合適的 URL 時,通過根 UINavigationController 進行跳轉(zhuǎn),不過這不是本篇文章想要說明的內(nèi)容。

UINavigationController 提供的 API 還是非常簡單的,我們可以直接使用 -pushViewController:animated: 就可以進行跳轉(zhuǎn)。

RegisterViewController *registerViewController = [[RegisterViewController alloc] init];
[self.navigationController pushViewController:registerViewController animated:YES];

作為數(shù)據(jù)源以及代理

很多 Cocoa Touch 中視圖層都是以代理的形式為外界提供接口的,其中最為典型的例子就是 UITableView 和它的數(shù)據(jù)源協(xié)議 UITableViewDataSource 和代理 UITableViewDelegate

這是因為 UITableView 作為視圖層的對象,需要根據(jù) Model 才能知道自己應(yīng)該展示什么內(nèi)容,所以在早期的很多視圖層組件都是用了代理的形式,從 Controller 或者其他地方獲取需要展示的數(shù)據(jù)。

#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.models.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
    Model *model = self.models[indexPath.row];
    [cell setupWithModel:model];
    return cell;
}

上面就是使用 UITableView 時經(jīng)常需要的方法。

很多文章中都提供了一種用于減少 Controller 層中代理方法數(shù)量的技巧,就是使用一個單獨的類作為 UITableView 或者其他視圖的代理:

self.tableView.delegate = anotherObject;
self.tableView.dataSource = anotherObject;

然而在筆者看來這種辦法并沒有什么太大的用處,只是將代理方法挪到了一個其他的地方,如果這個代理方法還依賴于當前 UIViewController 實例的上下文,還要向這個對象中傳入更多的對象,反而讓原有的 MVC 變得更加復(fù)雜了。

負責 HTTP 請求的發(fā)起

當用戶的行為觸發(fā)一些事件時,比如下拉刷新、更新 Model 的屬性等等,Controller 就需要通過 Model 層提供的接口向服務(wù)端發(fā)出 HTTP 請求,這一過程其實非常簡單,但仍然是 Controller 層的職責,也就是響應(yīng)用戶事件,并且更新 Model 層的數(shù)據(jù)。

- (void)registerButtonTapped:(UIButton *)button {
    LoginManager *manager = [LoginManager manager];
    manager.countryCode = _registerPanelView.countryCode;
    ...
    [manager startWithSuccessHandler:^(CCStudent *user) {
        self.currentUser = user;
        ...
    } failureHandler:^(NSError *error) {
        ...
    }];
}

當按鈕被點擊時 LoginManager 就會執(zhí)行 -startWithSuccessHandler:failureHandler: 方法發(fā)起請求,并在請求結(jié)束后執(zhí)行回調(diào),更新 Model 的數(shù)據(jù)。

小結(jié)

iOS 中 Controller 層的職責一直都逃不開與 View 層和 Model 層的交互,因為其作用就是視圖層的用戶行為進行處理并更新視圖的內(nèi)容,同時也會改變模型層中的數(shù)據(jù)、使用 HTTP 請求向服務(wù)端請求新的數(shù)據(jù)等作用,其功能就是處理整個應(yīng)用中的業(yè)務(wù)邏輯和規(guī)則。

但是由于 iOS 中 Controller 的眾多職責,單一的 UIViewController 類可能會有上千行的代碼,使得非常難以管理和維護,我們也希望在 iOS 中引入新的架構(gòu)模式來改變 Controller 過于臃腫這一現(xiàn)狀。

幾點建議

Controller 層作為 iOS 應(yīng)用中重要的組成部分,在 MVC 以及類似的架構(gòu)下,筆者對于 Controller 的設(shè)計其實沒有太多立竿見影的想法。作為應(yīng)用中處理絕大多數(shù)邏輯的 Controller 其實很難簡化其中代碼的數(shù)量;我們能夠做的,也是只對其中的代碼進行一定的規(guī)范以提高它的可維護性,在這里,筆者有幾點對于 Controller 層如何設(shè)計的建議,供各位讀者參考。

不要把 DataSource 提取出來

iOS 中的 UITableViewUICollectionView 等需要 dataSource 的視圖對象十分常見,在一些文章中會提議將數(shù)據(jù)源的實現(xiàn)單獨放到一個對象中。

void (^configureCell)(PhotoCell*, Photo*) = ^(PhotoCell* cell, Photo* photo) {
   cell.label.text = photo.name;
};
photosArrayDataSource = [[ArrayDataSource alloc] initWithItems:photos
                                                cellIdentifier:PhotoCellIdentifier
                                            configureCellBlock:configureCell];
self.tableView.dataSource = photosArrayDataSource;

Lighter View Controllers 一文中就建議可以將數(shù)據(jù)源協(xié)議的實現(xiàn)方法放到 ArrayDataSource 對象中:

@implementation ArrayDataSource

- (id)itemAtIndexPath:(NSIndexPath*)indexPath {
    return items[(NSUInteger)indexPath.row];
}

- (NSInteger)tableView:(UITableView*)tableView 
 numberOfRowsInSection:(NSInteger)section {
    return items.count;
}

- (UITableViewCell*)tableView:(UITableView*)tableView 
        cellForRowAtIndexPath:(NSIndexPath*)indexPath {
    id cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier
                                              forIndexPath:indexPath];
    id item = [self itemAtIndexPath:indexPath];
    configureCellBlock(cell,item);
    return cell;
}

@end

做出這種建議的理由是:單獨的 ArrayDataSource 類可以更方便的進行測試,同時,展示一個數(shù)組的對象是表視圖中非常常見的需求,而 ArrayDataSource 能夠?qū)⑦@種需求抽象出來并進行重用,也可以達到減輕視圖控制器負擔的最終目的,但是在筆者看來,上述做法并沒有起到實質(zhì)性效果,只是簡單的將視圖控制器中的一部分代碼移到了別的位置而已,還會因為增加了額外的類使 Controller 的維護變得更加的復(fù)雜。

UITableView-DataSource

讓每一個 Controller 作為 UITableView 對象的代理和數(shù)據(jù)源其實是沒有任何問題的,將這些方法移出 Controller 并不能解決實際的問題。

屬性和實例變量的選擇

文章的前面曾經(jīng)提到過在很多的 iOS 應(yīng)用中,Controller 由于持有一個根視圖 UIView 對象,所以需要負責展示內(nèi)容以及布局,很多 iOS 開發(fā)者都把一些模塊的視圖層代碼放到了控制器中,但是無論是將視圖層代碼放到控制器中,還是新建一個單獨的視圖類都需要對視圖以及子視圖進行初始化和布局。

在對視圖進行初始化和布局時,我們有兩種選擇,一種是使用實例變量的方式主動對視圖對象進行初始化,另一種是使用屬性 @property 對視圖對象進行惰性初始化。

Eager-Lazy-Initialization

雖然上述兩種代碼在結(jié)果上幾乎是等價的,但是筆者更加偏好兩者之中的后者,它將各個視圖屬性的初始化放到了各個屬性的 getter 方法中,能夠?qū)⒋a在邏輯上分塊還是比較清晰的。這兩種方法其實只是不同的 taste,有些人會堅持將不需要暴露的變量都寫成 _xxx 的形式,有些人更喜歡后者這種分散的寫法,這些都不是什么太大的問題,而且很多人擔心的性能問題其實也根本不是問題,重要的是我們要在同一個項目中堅持同一種寫法,并且保證只有同一個風格的代碼合入主分支。

把業(yè)務(wù)邏輯移到 Model 層

控制器中有很多代碼和邏輯其實與控制器本身并沒有太多的關(guān)系,比如:

@implementation ViewController

- (NSString *)formattedPostCreatedAt {
    NSDateFormatter *format = [[NSDateFormatter alloc] init];
    [format setDateFormat:@"MMM dd, yyyy HH:mm"];
    return [format stringFromDate:self.post.createdAt];
}

@end

談?wù)?MVX 中的 Model 層 一文中,我們曾經(jīng)分析過,上述邏輯其實應(yīng)該屬于 Model 層,作為 Post 的一個實例方法:

@implementation Post

- (NSString *)formattedCreatedAt {
    NSDateFormatter *format = [[NSDateFormatter alloc] init];
    [format setDateFormat:@"MMM dd, yyyy HH:mm"];
    return [format stringFromDate:self.createdAt];
}

@end

這一條建議是從一些經(jīng)典的后端 MVC 框架中學習的,Rails 提倡 Fat Model, Skinny Controller 就是希望開發(fā)者將 Model 相關(guān)的業(yè)務(wù)邏輯都放到 Model 層中,以減輕 Controller 層的負擔。

把視圖層代碼移到 View 層

因為 UIKit 框架設(shè)計的原因,Controller 和 View 層是強耦合的,每一個 UIViewController 都會持有一個 UIView 視圖對象,這也是導(dǎo)致我們將很多的視圖層代碼直接放在 Controller 層的原因。

MVC-in-iOS

這種做法在當前模塊的視圖層比較簡單時,筆者覺得沒有任何的問題,雖然破壞了經(jīng)典的 MVC 的架構(gòu)圖,但是也不是什么問題;不過,當視圖層的視圖對象非常多的時候,大量的配置和布局代碼就會在控制器中占據(jù)大量的位置,我們可以將整個視圖層的代碼都移到一個單獨的 UIView 子類中。

// RegisterView.h
@interface RegisterView : UIView

@property (nonatomic, strong) UITextField *phoneNumberTextField;
@property (nonatomic, strong) UITextField *passwordTextField;

@end

// RegisterView.m
@implementation RegisterView 

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        [self addSubview:self.phoneNumberTextField];
        [self addSubview:self.passwordTextField];

        [self.phoneNumberTextField mas_makeConstraints:^(MASConstraintMaker *make) {
            ...
        }];
        [self.passwordTextField mas_makeConstraints:^(MASConstraintMaker *make) {
            ...
        }];
    }
    return self;
}

- (UITextField *)phoneNumberTextField {
    if (!_phoneNumberTextField) {
        _phoneNumberTextField = [[UITextField alloc] init];
        _phoneNumberTextField.font = [UIFont systemFontOfSize:16];
    }
    return _phoneNumberTextField;
}

- (UITextField *)passwordTextField {
    if (!_passwordTextField) {
        _passwordTextField = [[UITextField alloc] init];
        ...
    }
    return _passwordTextField;
}

@end

而 Controller 需要持有該視圖對象,并將自己持有的根視圖替換成該視圖對象:

@interface ViewController ()

@property (nonatomic, strong) RegisterView *view;

@end

@implementation ViewController

@dynamic view;

- (void)loadView {
    self.view = [[RegisterView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
}

- (void)viewDidLoad {
    [super viewDidLoad];
}

@end

UIViewController 對象中,我們可以通過覆寫 -loadView 方法改變其本身持有的視圖對象,并使用新的 @property 聲明以及 @dynamic 改變 Controller 持有的根視圖,這樣我們就把視圖層的配置和布局代碼從控制器中完全分離了。

使用 pragma 或 extension 分割代碼塊

在很多時候,我們對于 Controller 中上千行的代碼是非常絕望的,不熟悉這個模塊的開發(fā)者想要在里面快速找到自己想要的信息真的是非常的麻煩,尤其是如果一個 UIViewController 中的代碼沒有被組織好的話,那分析起來更是異常頭疼。

我們既然沒有把上千行的代碼瞬間變沒的方法,那就只能想想辦法在現(xiàn)有的代碼上進行美化了,辦法其實很簡單,就是將具有相同功能的代碼分塊并使用 pragma 預(yù)編譯指定或者 MARK 加上 extension 對代碼塊進行分割。

這里給一個簡單的例子,

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupUI];
}

- (void)layoutSubviews { }

#pragma mark - UI

- (void)setupUI {}

#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 
    return 1; 
}
...

#pragma mark - UITableViewDelegate

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 100.0;
}
...

#pragma mark - Callback

- (void)buttonTapped:(UIButton *)button {}
- (void)gestureTriggered:(UIGestureRecognizer *)gesture {}
- (void)keyboardWillShow:(NSNotification *)noti {}

#pragma mark - Getter/Setter

- (NSString *)string { return _string; }
- (void)setString:(NSString*)string { _string = string; }

#pragma mark - Helper

- (void)helperMethod {}

@end

一個 UIViewController 大體由上面這些部分組成:

  • 生命周期以及一些需要 override 的方法
  • 視圖層代碼的初始化
  • 各種數(shù)據(jù)源和代理協(xié)議的實現(xiàn)
  • 事件、手勢和通知的回調(diào)
  • 實例變量的存取方法
  • 一些其他的 Helper 方法

在 Objective-C 的工程中,我們使用 pragma 預(yù)編譯指令來對 UIViewController 中的;在 Swift 中,我們可以使用 extension 加上 MARK 來對代碼進行分塊:

class ViewController: UIViewController {}

// MARK: - UI
extension ViewController {}

// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {}

// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {}

// MARK: - Callback
extension ViewController {}

// MARK: - Getter/Setter
extension ViewController {}

// MARK: - Helper
extension ViewController {}

上述方法是一種在控制器層分割代碼塊的方法,它們的順序并不是特別的重要,最重要的還是要在不同的控制器中保持上述行為的一致性,將合理的方法放到合適的代碼塊中。

耦合的 View 和 Model 層

很多的 iOS 項目中都會為 UIView 添加一個綁定 Model 對象的方法,比如說:

@implementation UIView (Model)

- (void)setupWithModel:(id)model {}

@end

這個方法也可能叫做 -bindWithModel: 或者其他名字,其作用就是根據(jù)傳入的 Model 對象更新當前是視圖中的各種狀態(tài),比如 UILabel 中的文本、UIImageView 中的圖片等等。

有了上述分類,我們可以再任意的 UIView 的子類中覆寫該方法:

- (void)setupWithModel:(Model *)model {
    self.imageView.image = model.image;
    self.label.text = model.name;
}

這種做法其實是將原本 Controller 做的事情放到了 View 中,由視圖層來負責如何展示模型對象;雖然它能夠減少 Controller 中的代碼,但是也導(dǎo)致了 View 和 Model 的耦合。

Coupling-View-And-Mode

對于 MVC 架構(gòu)模式中,Model、View 和 Controller 之間的交互沒有明確的規(guī)則,但是視圖和模型之間的耦合會導(dǎo)致視圖層代碼很難復(fù)用;因為這樣設(shè)計的視圖層都依賴于外部的模型對象,所以如果同一個視圖需要顯示多種類型的模型時就會遇到問題。

視圖和模型之間解耦是通過控制器來處理的,控制器獲取模型對象并取出其中的屬性一一裝填到視圖中,也就是將 -setupWithModel: 方法中的代碼從視圖層移到控制器層中,并在視圖類中暴露合適的接口。

總結(jié)

本文雖然對 Controller 層的職責進行了分析,但是由于 Controller 在 MVC 中所處的位置,如果不脫離 MVC 架構(gòu)模式,那么 Controller 的職責很難簡化,只能在代碼規(guī)范和職責劃分上進行限制,而在下一篇文章中我們會詳細討論 MVC 以及衍化出來的 MVP 以及 MVVM 到底是什么、以及它們有什么樣的差異。

Reference

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容