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 = %@";
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 = %@";
NSPredicate* predicate = [NSPredicate predicateWithFormat:formatString, now, now];
return [[self.priorities filteredSetUsingPredicate:predicate] allObjects];
}
複製程式碼
建立 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 物件會關心資料載入、快取和設定資料棧。它也經常被稱為服務層或者倉庫。
把網路請求邏輯移到 Model 層
和上面的主題相似:不要在 view controller 中做網路請求的邏輯。取而代之,你應該將它們封裝到另一個類中。這樣,你的 view controller 就可以在之後通過使用回撥(比如一個 completion 的 block)來請求網路了。這樣的好處是,快取和錯誤控制也可以在這個類裡面完成。
把 View 程式碼移到 View 層
不應該在 view controller 中構建複雜的 view 層次結構。你可以使用 Interface Builder 或者把 views 封裝到一個 UIView
子類當中。例如,如果你要建立一個選擇日期的控制元件,把它放到一個名為 DatePickerView
的類中會比把所有的事情都在 view controller 中做好好得多。再一次,這樣增加了可複用性並保持了簡單。
如果你喜歡 Interface Builder,你也可以在 Interface Builder 中做。有些人認為 IB 只能和 view controllers 一起使用,但事實上你也可以載入單獨的 nib 檔案到自定義的 view 中。在示例程式中,我們建立了一個 PhotoCell.xib
,包含了 photo cell 的佈局:
通訊
其他在 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 變得更整潔。
擴充套件閱讀
- View Controller Programming Guide for iOS
- Cocoa Core Competencies: Controller Object
- Writing high quality view controllers
- Stack Overflow: Model View Controller Store
- Unburdened View Controllers
- Stack Overflow: How to avoid big and clumsy
UITableViewControllers
on iOS
#小編這裡推薦一個群:691040931 裡面有大量的書籍和麵試資料,很多的iOS開發者都在裡面交流技術
原文 Lighter View Controllers