更輕量的 View Controllers

轉自:http://tang3w.com/translate/objective-c/objc.io/2013/10/22/更輕量的-view-controllers.html


注:這篇翻譯已經過 objc.io 授權,原文鏈接是:Lighter View Controllers

View controllers 通常是 iOS 項目中最大的文件,因爲它們包含了許多不必要的代碼。所以 View controllers 中的代碼幾乎總是複用率最低的。我們將會看到給 view controllers 瘦身的技術,讓代碼變得可以複用,以及把代碼移動到更合適的地方。

你可以在 Github 上獲取關於這個問題的示例項目

把 Data Source 和其他 Protocols 分離出來

把 UITableViewDataSource 的代碼提取出來放到一個單獨的類中,是爲 view controller 瘦身的強大技術之一。當你多做幾次,你就會發現這種模式,並且創建出可複用的類。

舉個例,在示例項目中,有個 PhotosViewController 類,它有以下幾個方法:

# 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;
}

許多代碼都圍繞數組做一些事情,有些是專門針對 view controller 所管理的 photos 數組的。所以我們可以嘗試把數組相關的代碼移到單獨的類中。我們使用一個 block 來設置 cell,也可以用 delegate 來做這件事,這取決於你的習慣。

@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

現在,你可以把 view controller 中的這 3 個方法去掉了,取而代之,你可以創建一個ArrayDataSource 類的實例作爲 table view 的 data source。

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;

現在你不用擔心把一個 index path 映射到數組中的位置了,每次你想把這個數組顯示到一個 table view 中時,你都可以複用這些代碼。你也可以實現一些額外的方法,比如tableView:commitEditingStyle:forRowAtIndexPath:,在 table view controllers 之間共享。

這樣的好處在於,你可以單獨測試這個類,再也不用寫第二遍。該原則同樣適用於數組之外的其他對象。

在今年我們做的一個應用裏面,我們大量使用了 Core Data。我們創建了相似的類,但和之前使用的數組不一樣,它用一個 fetched results controller 來獲取數據。它實現了所有動畫更新、處理 section headers、刪除操作等邏輯。你可以創建這個類的實例,然後賦予一個 fetch request 和用來設置 cell 的 block,剩下的它都會處理,不用你操心了。

此外,這種方法也可以擴展到其他 protocols 上面。最明顯的一個就是UICollectionViewDataSource。這給了你極大的靈活性;如果,在開發的某個時候,你想用UICollectionView 代替 UITableView,你幾乎不需要對 view controller 作任何修改。你甚至可以讓你的 data source 同時支持這兩個協議。

將業務邏輯移到 Model 中

下面是 view controller(來自其他項目)中的示例代碼,用來查找一個用戶的目前的優先事項的列表:

- (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 中會變得更加清晰。在 View Controller.m 中看起來就是這樣:

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

在 User+Extensions.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];
}

有些代碼不能被輕鬆地移動到 model 對象中,但明顯和 model 代碼緊密聯繫,對於這種情況,我們可以使用一個 Store

創建 Store 類

在我們第一版的示例程序的中,有些代碼去加載文件並解析它。下面就是 view controller 中的代碼:

- (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];
}

但是 view controller 沒必要知道這些,所以我們創建了一個 Store 對象來做這些事。通過分離,我們就可以複用這些代碼,單獨測試他們,並且讓 view controller 保持小巧。Store 對象會關心數據加載、緩存和設置數據棧。它也經常被稱爲 service layer 或 repository

把網絡請求邏輯移到 Model 層

和上面的主題相似:不要在 view controller 中做網絡請求的邏輯。取而代之,你應該將它們封裝到另一個類中,在這個類中通過回調(比如一個 completion block)來調用 view controller 中的方法。這樣的好處是,緩存和錯誤控制也可以在這個類裏面做。

把 View 代碼移到 View 層

不應該在 view controller 中構建複雜的 view 層次結構。你可以使用 interface builder 或者把 views 封裝到一個 UIView 子類當中。例如,如果你要創建一個 date picker 控件,把它放到一個名爲DatePickerView 的類中會比把所有的事情都在 view controller 中做會更有意義。這樣又增加了可複用性和簡單性。

如果你喜歡 Interface Builder,你也可以在 Interface Builder 中做。有些人只和 view controllers 一起使用,但你也可以單獨加載 nib 文件到自定義的 views 中。在示例程序中,我們創建了一個PhotoCell.xib,包含了 photo cell 的佈局:

PhotoCell.xib screenshot

就像你看到的那樣,我們在 view(我們沒有在這個 nib 上使用 File's Owner 對象)上面創建了 properties,然後連接到指定的 subviews。這種技術同樣適用於其他自定義的 views。

消息傳遞

其他在 view controllers 中經常發生的事是與其他 view controllers,model,和 views 之間進行消息傳遞。這當然是 controller 應該做的,但我們要儘可能地減少代碼。

關於 view controllers 和 model 對象之間的消息傳遞,已經有很多闡述得很好的技術(比如 KVO 和 fetched results controllers)。但是 view controllers 之間的消息傳遞稍微不是那麼清晰明瞭。

當一個 view controller 想把某個狀態傳遞給多個其他 view controllers 時,就會出現這樣的問題。較好的做法是把狀態放到一個單獨的對象裏,然後把這個對象傳遞給其它 view controllers,它們觀察和修改這個狀態。這樣的好處是消息傳遞都在一個地方(被觀察的對象)。但是當有嵌套的 delegate 回調時,問題還沒完,這是個複雜的話題,我們可能在未來用一個完整的話題來討論。

總結

我們已經看到用來創建更小巧的 view controllers 的技術。我們並不是想把這些技術應用到每一個角落,只是我們有一個目標:寫可維護的代碼。知道這些模式後,我們就更有可能把那些笨重的 view controllers 變得更整潔。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章