iOS架構師之路:控制器(View Controller)瘦身設計

前言
  古老的MVC架構是容易被iOS開發者理解和接受的設計模式,可是因爲iOS開發的項目功能愈來愈負責龐大,項目代碼也隨之不斷壯大,MVC的模糊定義致使咱們的業務開發工程師很容易把大量的代碼寫到視圖控制器中,行業中對這種控制器有個專業詞彙Massive ViewControler(臃腫的視圖控制器)。代碼臃腫致使可讀性可維護性差,並且這種不清晰的設計還有許多的反作用,好比代碼重用性差。做爲架構師須要關注項目的代碼質量。指導業務開發工程師寫出高質量,高健壯性,高可用的代碼也是很重要的工做。所以須要知道一些爲控制器瘦身的技巧,並在項目中幫助業務開發工程師合理的運用它們。本文翻譯一篇國外優秀文章:Lighter View Controllers
示例代碼下載地址:JackieHoo's GitHubgit

 

分離數據源(Data Source)協議(Protocol)github

         瘦身控制器的有效方法之一就是將實現 UITableViewDataSource 協議相關的代碼封裝成一個類(好比本文中的 ArraryDataSource )。若是你多用幾回這個設計,你就會建立複用性高的封裝類。設計模式

         舉個例子,示例工程中的類 Photos控制器實現以下數據源方法:數組

# pragma mark Pragma - (Photo*)photoAtIndexPath:(NSIndexPath*)indexPath { return photos[(NSUInteger)indexPath.row]; } - (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section { return photos.count; } - (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath { PhotoCell* cell = [tableView dequeueReusableCellWithIdentifier:PhotoCellIdentifier forIndexPath:indexPath]; Photo* photo = [self photoAtIndexPath:indexPath]; cell.label.text = photo.name; return cell; }

  上面示例的數據源的實現都與 NSArray 有關,還有一個方法的實現與 Photo 有關(Photo 與 Cell 呈一一對應關係)。下面讓咱們來把與 NSArray 相關的代碼從 控制器中抽離出來,並改用 block 來設置 cell 的視圖。固然你也能夠用代理來實現,取決於你的我的喜愛。緩存

 

@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 

  如今咱們能夠控制器中的三個數據源代理方法能夠幹掉,而且把 控制器的 dataSource 設置爲 ArrayDataSource 的實例。微信

 

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;

  經過上面的方法,你就能夠把設置 Cell 視圖的工做從 控制器中抽離出來。如今你不須要再關心indexPath如何與 NSArrary 中的元素如何關聯,當你須要將數組中的元素在其它 UITableView 中展現時你能夠重用以上代碼。你也能夠在 ArrayDataSource 中實現更多的方法,好比tableView:commitEditingStyle:forRowAtIndexPath:。架構

  這樣作還能帶來額外的好處,咱們還能夠針對這部分實現編寫單獨的單元測試。不只僅針對NSArray,咱們可使用這種分離思路處理其餘數據容器(好比NSDictionary)。單元測試

  該技巧一樣適用於其餘 Protocol ,好比 UICollectionViewDataSource 。經過該協議,你能夠定義出各類各樣的 UICollectionViewCell 。假若有一天,你須要在代碼在使用到 UICollectionView 來替代當前的 UITableView,你只須要修改幾行 控制器中的代碼便可完成替換。你甚至可以讓你的 DataSource 類同時實現 UICollectionViewDataSource 協議和 UITableViewDataSource 協議。測試

 

業務邏輯移至 Modelfetch

下面是一段位於 控制器中的代碼,做用是找出針對用戶active priority的一個列表。

- (void)loadPriorities {
  NSDate* now = [NSDate date];
  NSString* formatString = @"startDate <= %@ AND endDate >= %@";
  NSPredicate* predicate = [NSPredicate predicateWithFormat:formatString, now, now];
  NSSet* priorities = [self.user.priorities filteredSetUsingPredicate:predicate];
  self.priorities = [priorities allObjects];
}

  然而,假如你把代碼實現移至 User 的 Category 中,控制器中的代碼將會更簡潔、更清晰。

將以上代碼移到User+Extension.m中 

- (NSArray*)currentPriorities {
  NSDate* now = [NSDate date];
  NSString* formatString = @"startDate <= %@ AND endDate >= %@";
  NSPredicate* predicate = [NSPredicate predicateWithFormat:formatString, now, now];
  return [[self.priorities filteredSetUsingPredicate:predicate] allObjects];
}

 

ViewController.m 中的代碼能夠改爲這個鬼樣子,是否是明顯要簡潔許多,可讀性強不少呢。

 

- (void)loadPriorities {
  self.priorities = [self.user currentPriorities];
}

 

  實際開發中,有些代碼很難移至 model 對象中,可是很明顯這些代碼與 model 對象有關。針對這種狀況,咱們能夠建立一個 store 類,並把相關代碼遷移進去。

 

建立 Store

在這個示例項目工程中,咱們有一段用於從本地文件加載數據並解析的代碼:

- (void)readArchive {
    NSBundle* bundle = [NSBundle bundleForClass:[self class]];
    NSURL *archiveURL = [bundle URLForResource:@"photodata"
                                 withExtension:@"bin"];
    NSAssert(archiveURL != nil, @"Unable to find archive in bundle.");
    NSData *data = [NSData dataWithContentsOfURL:archiveURL
                                         options:0
                                           error:NULL];
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
    _users = [unarchiver decodeObjectOfClass:[NSArray class] forKey:@"users"];
    _photos = [unarchiver decodeObjectOfClass:[NSArray class] forKey:@"photos"];
    [unarchiver finishDecoding];
}

 

  控制器不該該負責以上的工做,控制器只要負責數據調度就能夠了,數據獲取的工做咱們徹底能夠交給 store 對象來負責。經過將這些代碼從 控制器中抽離出來,咱們能夠更容易複用、測試這些方法、同時讓控制器變得更輕巧( Store 對象通常負責數據的加載、緩存、持久化。Store 對象也常常被稱做 Service Layer 對象,或者 Repository 對象)。

Web Service 邏輯移至 Model

這與上一個主題很是類似:別把 Web Service 相關的代碼寫在 控制器中,應該把這部分代碼抽離出來。並經過方法的回調對數據進行處理。

不只如此,你還能夠把處理異常狀況的工做也轉交給 Store 對象負責。

把視圖相關的代碼移至 View

  一樣構建視圖(尤爲是複雜視圖)的代碼也不該該寫在 View Controller (關我毛事啊,我只負責調度和通訊啊)中。要麼使用Interface Builder ,要麼封裝一個 Vew 的子類來完成這部分工做。假設如今須要實現自定義一個日期選擇器。咱們應該新建一個 DatePickerView 的子類來完成構建視圖的工做,而不是把這部分工做放在 View Controller 中完成。一樣的,這將是你的代碼更簡潔,複用性更強。

除了用 code 的形式來實現自定義視圖,你也可使用 Interface Builder 來完成構建自定義視圖的工做。不少人都認爲 Interface Builder 只能用於爲 View Controller 構建視圖,其實否則,你能夠經過單獨的 nib 文件來加載在 Interface Builder中構建的自定義視圖。在示例工程當中,咱們建立了一個包含了 Photo Cell 視圖的 PhotoCell.xib 文件。

 

如圖所示,咱們在 view 中建立了屬性(無需設置 File’s Owner 對象)並把它們與 Interface Builder 中的視圖關聯起來。這個方法一樣適用於構建其它自定義視圖。

通信

咱們在控制器中常常須要與其它控制器ModelView 進行通信。雖然這原本就是 控制器應該負責處理的事情,但咱們依然能夠用盡量少的代碼完成咱們控制器的負責的工做。

如今已經有不少成熟的方案來創建 控制器View 的通信(例如 KVOfetched results controllers)。然而 控制器之間的通信目前尚未相似的方案能夠借鑑。

在實際開發中,咱們常常須要把 遇到須要把控制器持有的一些狀態信息,傳遞到 多個 控制器的需求。一般咱們會將這些狀態信息保存在一個對象中而後傳遞給其餘的視圖控制器。這部分的瘦身技巧比較複雜,我留在之後再專門講解吧。

結論

咱們已經展現了一些瘦身控制器的方法。做爲架構師咱們不可能徹底照搬這些設計技巧,但咱們須要清楚咱們這麼作的目的,咱們只有一個目標:使得代碼更易於維護,只要架構師在review代碼時時刻關注這個目標,咱們能夠就能夠擴展這些技巧,靈活運用到項目中。經過了解這些方法,咱們可以更好的處理好複雜的視圖控制器,而且讓這些視圖控制器的代碼更整潔,更清晰。

 歡迎關注個人微信公衆號:丁丁的coding日記

相關文章
相關標籤/搜索