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


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

 

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

         瘦身控制器的有效方法之一就是將實現 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 協議。

 

業務邏輯移至 Model

下面是一段位於 控制器中的代碼,作用是找出針對用戶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日記


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM