如何解決控制器臃腫(Lighter View Controllers)

weixin_33670713發表於2018-08-28
2368622-06e20e89f3abdb47.png

今天面試某某二線大廠的時候,問到了如何解決控制器臃腫的問題,如何抽離程式碼,感覺答得不是特別好,所以找了一篇國外大神的文章,拜讀翻譯一下,當做總結啦。 原文

檢視控制器通常是iOS專案中最大的檔案,它們通常包含了許多非必須程式碼。檢視控制器幾乎總是是程式碼中可重用性最低的部分。我們將研究如何抽取檢視控制器程式碼,使程式碼可重用以及將程式碼移動到更合適的位置。
此問題的示例專案位於GitHub上。

分離出Data Source和其他Protocols

減小檢視控制器的最重要的一點是獲取UITableViewDataSource程式碼的一部分,並將其移動到自己的類中。如果您不止一次這樣做,您將開始看到模式併為此建立可重用的類。
例如,在我們的示例專案中,有一個類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;
}

很多程式碼都與陣列有關,其中一些特定於檢視控制器管理的照片。因此,讓我們嘗試將與陣列相關的程式碼移動到自己的類中。我們使用一個block來配置單元格,但它也可能是一個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

檢視控制器中的三個方法可以使用,您可以建立此物件的例項並將其設定為表檢視的資料來源。

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;

現在您不必擔心將索引路徑對映到陣列中的位置,並且每次要在表檢視中顯示陣列時都可以重用此程式碼。您還可以實現其他方法,例如tableView:commitEditingStyle:forRowAtIndexPath:在所有表檢視控制器之間共享該程式碼。
好訊息是我們可以單獨測試這個類,而不必擔心再次編寫它。如果您使用除陣列之外的其他內容,則適用相同的原則。
此外,這種方法也擴充套件到其他協議。比如UICollectionViewDataSource。這為您提供了極大的靈活性; 如果在開發過程中由於業務變更需要換用UICollectionView而不是UITableView,那麼您幾乎不需要在檢視控制器中進行任何更改。您甚至可以使您的資料來源支援這兩種協議。

將域邏輯移動到模型中

以下是檢視控制器(來自另一個專案)中的程式碼示例,該程式碼應該為使用者查詢活動優先順序列表:

- (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。然後它看起來像這樣View Controller.m:

- (void)loadPriorities {
  self.priorities = [self.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];
}

有些程式碼不能輕易地移動到模型物件中,但仍然明顯與模型程式碼相關聯,為此,我們可以使用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物件來完成這個任務。通過將其分離出來,我們可以重用該程式碼,單獨測試它並使我們的檢視控制器保持較小。商店可以處理資料載入,快取和設定資料庫堆疊。此儲存通常也稱為服務層或儲存庫。
將Web服務邏輯移動到模型層
這與上面的主題非常相似:不要在檢視控制器中執行Web服務邏輯。相反,將其封裝在不同的類中。然後,您的檢視控制器可以使用回撥處理程式(例如,block)呼叫此類上的方法。好的一點是,您也可以在此類中執行所有快取和錯誤處理。

將檢視程式碼移動到檢視層(xib)

不應在檢視控制器中完成構建複雜的檢視層次結構。使用介面構建器,或將檢視封裝到它們自己的UIView子類中。例如,如果您構建自己的日期選擇器控制元件,將它放入DatePickerView類中比在檢視控制器中建立整個事物更有意義。同樣,這增加了可重用性和簡單性。
如果您喜歡Interface Builder,那麼您也可以在Interface Builder中執行此操作。有些人認為您只能將它用於檢視控制器,但您也可以使用自定義檢視載入單獨的nib檔案。在我們的示例應用程式中,我們建立了一個PhotoCell.xib包含照片單元格佈局的應用程式:

2368622-ba28281444526c19.png

如您所見,我們在檢視上建立了屬性(我們不在此xib中使用File的Owner物件)並將它們連線到特定的子檢視。這種技術對於其他自定義檢視也非常方便。

通訊

在檢視控制器中發生很多其他事情之一是與其他檢視控制器,模型和檢視的通訊。雖然這正是控制器應該做的事情,但它也是我們希望用盡可能少的程式碼實現的。
在檢視控制器和模型物件(例如KVO和獲取的結果控制器)之間進行通訊有很多很好的解釋技術,但是,檢視控制器之間的通訊通常不太清楚。
我們經常遇到一個檢視控制器具有某種狀態並與多個其他檢視控制器通訊的問題。通常,將此狀態放入單獨的物件並將其傳遞給檢視控制器是有意義的,然後檢視控制器都會觀察並修改該狀態。優點是它都在一個地方,我們最終不會陷入巢狀的委託回撥中。這是一個複雜的主題,我們將來可能會將整個問題專門用於此。

結論
我們已經看到了一些建立較小檢視控制器的技術。我們不會盡可能地應用這些技術,因為我們只有一個目標:編寫可維護的程式碼。通過了解這些模式,我們有更好的機會獲取笨重的檢視控制器並使它們更清晰。

相關文章