読者です 読者をやめる 読者になる 読者になる

hachinoBlog

hachinobuのエンジニアライフ

TableViewのセル表示高速化 〜高さが動的に変わるセル対応編〜


表示するコンテンツの有無に応じて高さが変わるカスタムセルを表示する際にえらく時間が掛かってしまっていたのを改善したメモ。
(カスタムセルはUILabelを縦に4個配置しているもので上から順に詰めていく。UILabelの高さはすべて25.0fで固定)
時間がかかっていた原因としては動的に高さを変える為にUITableViewDelegateの

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;

メソッド内でセルの高さを計算する処理をごりごり書いていたためだった。
このメソッドは

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

のように画面に表示される分だけ呼ばれるのではなく、表示されるセルの件数回呼ばれるということを知らなかった。

以下、表示までに時間の掛かっていたコード

const CGFloat kOneLabelHeight = 25.0f; //セルに表示する1コンテンツの高さ

//セルの高さを動的に計算するメソッド
- (CGFloat)getContentsHeight:(Seminars *)seminar 
{

    NSMutableString *contents = [NSMutableString string];
    NSString *content1 = [seminar content1]; //セル表示コンテンツ1
    NSString *content2 = [seminar content2]; //セル表示コンテンツ2
    NSString *content3 = [seminar content3]; //セル表示コンテンツ3
    NSString *content4 = [seminar content4]; //セル表示コンテンツ4

    int count = 0;
    if (content1 == nil || [content1 length] == 0) {
         count++;
    }    
	
    if (content2 == nil || [content2 length] == 0) {
         count++;
    }

    if (content3 == nil || [content3 length] == 0) {
         count++;
    } 

    if (content4 == nil || [content4 length] == 0) {
         count++;
    }
    //カスタムセルの読み込み
    [[NSBundle mainBundle] loadNibNamed:@"CustomCell" owner:self options:nil];

    //すべて表示する場合は元々のカスタムセルの高さを返す
    if (count == 0) {
      return _customCell.frame.size.height;
    }
    //表示しないデータがある場合はその分の高さを減らす
    return _customCell.frame.size.height - (kOneLabelHeight*i);
}

//このメソッドで毎回セルごとの高さを計算する処理を書いていた。セルの件数が増えれば増えるごとに表示が遅くなる
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath 
{
    
    Seminars *seminar = [_fetchedResultsController objectAtIndexPath:indexPath]; //該当セルのオブジェクト取得
    return [self getContentsHeight:seminar];
    
}


なのでNSFetchedResultsControllerを作成した際に、格納されているオブジェクト分ループしてNSIndexPathをキーとしてセルの高さを格納したディクショナリを前もって作成しておきセルの高さを返すメソッドで利用した。

//テーブルを表示する前に呼ばれるNSFetchedResultsControllerを作成する処理
- (NSFetchedResultsController *)fetchedResultsController
{
    if (_fetchedResultsController != nil) {
        return _fetchedResultsController;
    }
    
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Seminars" inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entity];
    
    // Create the sort descriptors array.
    NSSortDescriptor *authorDescriptor = [[NSSortDescriptor alloc] initWithKey:@"content1" ascending:YES];
    NSSortDescriptor *titleDescriptor = [[NSSortDescriptor alloc] initWithKey:@"content2" ascending:YES];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:authorDescriptor, titleDescriptor, nil];
    [fetchRequest setSortDescriptors:sortDescriptors];
    
    // Create and initialize the fetch results controller.
    _fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:@"content1" cacheName:@"Root"];
    _fetchedResultsController.delegate = self;
    
    //セルの高さを格納したNSDictionary作成
    [self makeCellHeightDic];
    return _fetchedResultsController;
}

//NSFetchedResultsControllerを作成後に呼ばれるメソッド
- (void)makeCellHeightDic
{
    self.cellHeightDic = [NSMutableDictionary dictionary]; 
    [[NSBundle mainBundle] loadNibNamed:@"CustomCell" owner:self options:nil];
    
    //セクション数
    NSUInteger sectionCount = [[_fetchedResultsController sections] count];
    for (int i = 0; i < sectionCount; i++) {
        NSUInteger rowCount = [[[_fetchedResultsController sections] objectAtIndex:i] numberOfObjects];
        for (int j = 0; j < rowCount; j++) {
            NSIndexPath *index = [NSIndexPath indexPathForRow:j inSection:i];
            Seminars *seminar = [_fetchedResultsController objectAtIndexPath:index];
            CGFloat height = [self getContentsHeight:seminar];
            //NSIndexPathとセルの高さを格納
            [_cellHeightDic setObject:[NSNumber numberWithFloat:height] forKey:index];
        }
    }
    
    self.seminarCell = nil;
}


//ここでは該当のNSIndexPathをキーにして高さを取り出すだけ
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath 
{
    
    return [[_cellHeightDic objectForKey:indexPath] floatValue];
    
}

結局、表示オブジェクトの分だけ高さを取得する処理を呼び出すから意味ないと思いがちだが

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath

メソッドで毎回処理するよりも前に取得してあげておくだけで大分違いが出ました。