View controllers 通常是 iOS 項(xiàng)目中最大的文件,并且它們包含了許多不必要的代碼。所以 View controllers 中的代碼幾乎總是復(fù)用率最低的。我們將會(huì)看到給 view controllers 瘦身的技術(shù),讓代碼變得可以復(fù)用,以及把代碼移動(dòng)到更合適的地方。
你可以在 Github 上獲取關(guān)于這個(gè)問題的示例項(xiàng)目。
把 UITableViewDataSource
的代碼提取出來放到一個(gè)單獨(dú)的類中,是為 view controller 瘦身的強(qiáng)大技術(shù)之一。當(dāng)你多做幾次,你就能總結(jié)出一些模式,并且創(chuàng)建出可復(fù)用的類。
舉個(gè)例,在示例項(xiàng)目中,有個(gè) PhotosViewController
類,它有以下幾個(gè)方法:
# 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;
}
這些代碼基本都是圍繞數(shù)組做一些事情,更針對(duì)地說,是圍繞 view controller 所管理的 photos 數(shù)組做一些事情。我們可以嘗試把數(shù)組相關(guān)的代碼移到單獨(dú)的類中。我們使用一個(gè) block 來設(shè)置 cell,也可以用 delegate 來做這件事,這取決于你的習(xí)慣。
@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
現(xiàn)在,你可以把 view controller 中的這 3 個(gè)方法去掉了,取而代之,你可以創(chuàng)建一個(gè) ArrayDataSource
類的實(shí)例作為 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;
現(xiàn)在你不用擔(dān)心把一個(gè) index path 映射到數(shù)組中的位置了,每次你想把這個(gè)數(shù)組顯示到一個(gè) table view 中時(shí),你都可以復(fù)用這些代碼。你也可以實(shí)現(xiàn)一些額外的方法,比如 tableView:commitEditingStyle:forRowAtIndexPath:
,在 table view controllers 之間共享。
這樣的好處在于,你可以單獨(dú)測(cè)試這個(gè)類,再也不用寫第二遍。該原則同樣適用于數(shù)組之外的其他對(duì)象。
在今年我們做的一個(gè)應(yīng)用里面,我們大量使用了 Core Data。我們創(chuàng)建了相似的類,但和之前使用的數(shù)組不一樣,它用一個(gè) fetched results controller 來獲取數(shù)據(jù)。它實(shí)現(xiàn)了所有動(dòng)畫更新、處理 section headers、刪除操作等邏輯。你可以創(chuàng)建這個(gè)類的實(shí)例,然后賦予一個(gè) fetch request 和用來設(shè)置 cell 的 block,剩下的它都會(huì)處理,不用你操心了。
此外,這種方法也可以擴(kuò)展到其他 protocols 上面。最明顯的一個(gè)就是 UICollectionViewDataSource
。這給了你極大的靈活性;如果,在開發(fā)的某個(gè)時(shí)候,你想用 UICollectionView
代替 UITableView
,你幾乎不需要對(duì) view controller 作任何修改。你甚至可以讓你的 data source 同時(shí)支持這兩個(gè)協(xié)議。
下面是 view controller(來自其他項(xiàng)目)中的示例代碼,用來查找一個(gè)用戶的目前的優(yōu)先事項(xiàng)的列表:
- (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];
}
把這些代碼移動(dòng)到 User
類的 category 中會(huì)變得更加清晰,處理之后,在 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];
}
有些代碼不能被輕松地移動(dòng)到 model 對(duì)象中,但明顯和 model 代碼緊密聯(lián)系,對(duì)于這種情況,我們可以使用一個(gè) 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 沒必要知道這些,所以我們創(chuàng)建了一個(gè) Store 對(duì)象來做這些事。通過分離,我們就可以復(fù)用這些代碼,單獨(dú)測(cè)試他們,并且讓 view controller 保持小巧。Store 對(duì)象會(huì)關(guān)心數(shù)據(jù)加載、緩存和設(shè)置數(shù)據(jù)棧。它也經(jīng)常被稱為服務(wù)層或者倉庫。
和上面的主題相似:不要在 view controller 中做網(wǎng)絡(luò)請(qǐng)求的邏輯。取而代之,你應(yīng)該將它們封裝到另一個(gè)類中。這樣,你的 view controller 就可以在之后通過使用帶有回調(diào)(比如一個(gè) completion 的 block)來請(qǐng)求網(wǎng)絡(luò)了。這樣的好處是,緩存和錯(cuò)誤控制也可以在這個(gè)類里面完成。
不應(yīng)該在 view controller 中構(gòu)建復(fù)雜的 view 層次結(jié)構(gòu)。你可以使用 Interface Builder 或者把 views 封裝到一個(gè) UIView
子類當(dāng)中。例如,如果你要?jiǎng)?chuàng)建一個(gè)選擇日期的控件,把它放到一個(gè)名為 DatePickerView
的類中會(huì)比把所有的事情都在 view controller 中做好好得多。再一次,這樣增加了可復(fù)用性并保持了簡單。
如果你喜歡 Interface Builder,你也可以在 Interface Builder 中做。有些人認(rèn)為 IB 只能和 view controllers 一起使用,但事實(shí)上你也可以加載單獨(dú)的 nib 文件到自定義的 view 中。在示例程序中,我們創(chuàng)建了一個(gè) PhotoCell.xib
,包含了 photo cell 的布局:
http://wiki.jikexueyuan.com/project/objc/images/1-1.png" alt="" />
就像你看到的那樣,我們?cè)?view(我們沒有在這個(gè) nib 上使用 File's Owner 對(duì)象)上面創(chuàng)建了 properties,然后連接到指定的 subviews。這種技術(shù)同樣適用于其他自定義的 views。
其他在 view controllers 中經(jīng)常發(fā)生的事是與其他 view controllers,model,和 views 之間進(jìn)行通訊。這當(dāng)然是 controller 應(yīng)該做的,但我們還是希望以盡可能少的代碼來完成它。
關(guān)于 view controllers 和 model 對(duì)象之間的消息傳遞,已經(jīng)有很多闡述得很好的技術(shù)(比如 KVO 和 fetched results controllers)。但是 view controllers 之間的消息傳遞稍微就不是那么清晰了。
當(dāng)一個(gè) view controller 想把某個(gè)狀態(tài)傳遞給多個(gè)其他 view controllers 時(shí),就會(huì)出現(xiàn)這樣的問題。較好的做法是把狀態(tài)放到一個(gè)單獨(dú)的對(duì)象里,然后把這個(gè)對(duì)象傳遞給其它 view controllers,它們觀察和修改這個(gè)狀態(tài)。這樣的好處是消息傳遞都在一個(gè)地方(被觀察的對(duì)象)進(jìn)行,而且我們也不用糾結(jié)嵌套的 delegate 回調(diào)。這其實(shí)是一個(gè)復(fù)雜的主題,我們可能在未來用一個(gè)完整的話題來討論這個(gè)主題。
我們已經(jīng)看到一些用來創(chuàng)建更小巧的 view controllers 的技術(shù)。我們并不是想把這些技術(shù)應(yīng)用到每一個(gè)可能的角落,只是我們有一個(gè)目標(biāo):寫可維護(hù)的代碼。知道這些模式后,我們就更有可能把那些笨重的 view controllers 變得更整潔。