web-dev-qa-db-ja.com

iPhone-各セルの高さが動的なときに、テーブルビューのheightForRowAtIndexPathをいつ計算するのですか?

私はこの質問が何度も尋ねられるのを見ましたが、驚いたことに、一貫した答えを見たことがありませんので、自分で試してみます。

実行時に高さを決定する必要があるUITextViewsとUILabelsを含む独自のカスタムUITableViewCellsを含むテーブルビューがある場合、heightForRowAtIndexPathの各行の高さをどのように決定しますか?

最も明白な最初のアイデアは、cellForRowAtIndexPath内のセル内の各ビューの高さを計算して合計することによって各セルの高さを計算し、後で取得するためにその最終的な合計の高さを格納することです。

ただし、cellForRowAtIndexPathがAFTER heightForRowAtIndexPathと呼ばれるため、これは機能しません。

私が考えることができる唯一のことは、viewDidLoad内ですべての計算を行い、次にすべてのUITableViewCellsを作成し、セルの高さを計算してUITableViewCellサブクラス内のカスタムフィールドに格納し、各セルをindexPathとしてNSMutableDictionaryに配置することですキー、次にcellForRowAtIndexPathおよびheightForRowAtIndexPath内のindexPathを使用してディクショナリからセルを取得し、カスタムの高さ値またはセルオブジェクト自体を返します。

このアプローチは間違っているようですが、dequeueReusableCellWithIdentifierを使用しないため、代わりにすべてのセルをコントローラーのディクショナリに一度にロードし、デリゲートメソッドはディクショナリから正しいセルを取得するだけです。

私はそれをする他の方法を見ません。これは悪い考えですか?そうであれば、これを行う正しい方法は何ですか?

42
JohnRock

AppleがUITableViewを実装する方法は誰にとっても直感的ではなく、_heightForRowAtIndexPath:_の役割を誤解するのは簡単です。一般的な意図は、これがより高速で軽量のメモリ上のメソッドであるということです。これは、テーブル内のすべての行に対して頻繁に呼び出すことができます。これは、低速でメモリを多く消費する_cellForRowAtIndexPath:_とは対照的ですが、実際に常に表示する必要がある行に対してのみ呼び出されます。

Appleをこのように実装するのはなぜですか?理由の一部は、ほとんどの場合、安価である(または安価になる可能性がある)ことです。正しくコーディングすると、行全体の高さを計算して、セル全体を作成して入力するよりも多くのテーブルでは、すべてのセルの高さが同じになるため、多くの場合非常に安くなります。理由の別の部分は、iOSがテーブル全体のサイズを知る必要があるためです:これにより、スクロールバーを作成し、スクロールビューに設定できるようになります。

したがって、すべてのセルの高さが同じでない限り、UITableViewが作成され、reloadDataメッセージを送信するたびに、データソースにはセルごとに1つのheightForRowAtIndexPathメッセージが送信されます。したがって、テーブルに30個のセルがある場合、そのメッセージは30回送信されます。その30個のセルのうち6個だけが画面に表示されているとしましょう。その場合、作成してreloadDataメッセージを送信すると、UITableViewは表示されている行ごとに1つのcellForRowAtIndexPathメッセージを送信します。つまり、そのメッセージは6回送信されます。

一部の人々は、ビューを作成せずにセルの高さを計算する方法について困惑していることがあります。しかし、通常これは簡単です。

たとえば、行の高さがさまざまな量のテキストを保持するためにサイズが異なる場合、関連する文字列で_sizeWithFont:_メソッドの1つを使用して計算を実行できます。これは、ビューを作成して結果を測定するよりも高速です。セルの高さを変更した場合は、テーブル全体を再読み込みする必要があることに注意してください(reloadDataを使用して、デリゲートにすべての高さを要求しますが、表示されているセルのみを要求します)OR =サイズが変更された行を選択的に再読み込みします(これは、前回チェックしたときに、常に_heightForRowAtIndexPath:_を呼び出しますbutも、ある程度のスクロール作業を行います) 。

この質問 およびおそらく これ も参照してください。

61
Obliquely

したがって、私は考える一度にすべてのセルを作成する必要なくこれを行うことができます(あなたが示唆するように、これは無駄であり、おそらく多数のセル)。

UIKitはNSStringにいくつかのメソッドを追加します。それらはメインのNSStringドキュメントの一部ではないため、見落としている可能性があります。興味のあるものから始めます。

- (CGSize)sizeWithFont...

Apple docs へのリンクです。

理論的には、これらのNSStringの追加は、この正確な問題に対して存在します。テキストブロックが占めるサイズを把握するビュー自体をロードする必要がない 。テーブルビューデータソースの一部として、各セルのテキストにすでにアクセスしていると考えられます。

UITextViewで書式設定を行っている場合、このソリューションによって走行距離が異なる可能性があるため、「理論的に」と言います。しかし、私はそれがあなたに少なくとも部分的にあなたを連れて行くことを望んでいます。この例は Cocoa is My Girlfriend にあります。

10
lxt

これは、UTextViewのテキストの量に基づいてセルの高さを計算する方法です。

#define PADDING  21.0f

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

    if(indexPath.section == 0 && indexPath.row == 0)
    {   
        NSString *practiceText = [practiceItem objectForKey:@"Practice"];
        CGSize practiceSize = [practiceText sizeWithFont:[UIFont systemFontOfSize:14.0f] 
                   constrainedToSize:CGSizeMake(tblPractice.frame.size.width - PADDING * 3, 1000.0f)];
        return practiceSize.height + PADDING * 3;
    }

    return 72;
}

もちろん、必要に応じてPADDINGおよびその他の変数を調整する必要がありますが、これにより、UITextViewが含まれるセルの高さが設定されます。提供されたテキストの量に基づく。したがって、テキストが3行しかない場合、セルはかなり短く、14行のテキストがある場合と同様に、セルの高さはかなり大きくなります。

5
WrightsCS

これまでに使用したアプローチは、テーブルで使用するセルの単一のインスタンスを保持するクラス変数を作成することです(私はこれをプロトタイプセルと呼びます)。次に、カスタムセルクラスに、データを入力し、セルの高さを決定するメソッドがあります。実際にデータを入力するのは、メソッドのより単純なバリアントである可能性があることに注意してください。たとえば、セル内のUILabelを実際にサイズ変更する代わりに、NSStringのheightメソッドを使用して、UILabelの最終的なセル内の高さを決定し、次に、セルの合計高さ(および下部の境界線)とUILabel配置を使用して、実際の高さを決定します。ラベルが44ユニットの高さになるときの意味を理解するために、要素の配置場所を知るためだけにプロトタイプセルを使用します。

heightForRow:次に、そのメソッドを呼び出して高さを返します。

cellForRow:実際にラベルにデータを入力してサイズを変更するメソッドを使用します(UITableViewセルのサイズを自分で変更することはありません)。

凝ったものにしたい場合は、渡したデータに基づいて各セルの高さをキャッシュすることもできます(たとえば、高さが決定されるのがそれだけであれば、1つのNSStringにある可能性があります)。多くの場合同じである多くのデータがある場合、メモリ内だけでなく永続的なキャッシュを使用することは理にかなっています。

文字数または単語数に基づいて行数を推定することもできますが、私の経験ではうまくいきません-うまくいかない場合は、通常、セルとその下のすべてのセルをめちゃくちゃにします。

各セルの計算をtableView:heightForRowAtIndexPath:に移動する場合の問題は、reloadDataが呼び出されるたびにすべてのセルが再計算されることです。少なくとも数百の行がある可能性がある私のアプリケーションでは、速度が遅すぎます。デフォルトの行の高さを使用し、計算時に行の高さをキャッシュする別のソリューションを次に示します。高さが変更されるか、最初に計算されると、テーブルの再読み込みがスケジュールされ、新しい高さがテーブルビューに通知されます。これは、行の高さが変更されたときに行が2回表示されることを意味しますが、それは比較するとマイナーです。

@interface MyTableViewController : UITableViewController {
    NSMutableDictionary *heightForRowCache;
    BOOL reloadRequested;
    NSInteger maxElementBottom;
    NSInteger minElementTop;
}

tableView:heightForRowAtIndexPath:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // If we've calculated the height for this cell before, get it from the height cache.  If
    // not, return a default height.  The actual size will be calculated by cellForRowAtIndexPath
    // when it is called.  Do not set too low a default or UITableViewController will request
    // too many cells (with cellForRowAtIndexPath).  Too high a value will cause reloadData to
    // be called more times than needed (as more rows become visible).  The best value is an
    // average of real cell sizes.
    NSNumber *height = [heightForRowCache objectForKey:[NSNumber numberWithInt:indexPath.row]];
    if (height != nil) {
        return height.floatValue;
    }

    return 200.0;
}

tableView:cellForRowAtIndexPath:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Get a reusable cell
    UITableViewCell *currentCell = [tableView dequeueReusableCellWithIdentifier:_filter.templateName];
    if (currentCell == nil) {
        currentCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:_filter.templateName];
    }

    // Configure the cell
    // +++ unlisted method sets maxElementBottom & minElementTop +++
    [self configureCellElementLayout:currentCell withIndexPath:indexPath];

    // Calculate the new cell height
    NSNumber *newHeight = [NSNumber numberWithInt:maxElementBottom - minElementTop];

    // When the height of a cell changes (or is calculated for the first time) add a
    // reloadData request to the event queue.  This will cause heightForRowAtIndexPath
    // to be called again and inform the table of the new heights (after this refresh
    // cycle is complete since it's already been called for the current one).  (Calling
    // reloadData directly can work, but causes a reload for each new height)
    NSNumber *key = [NSNumber numberWithInt:indexPath.row];
    NSNumber *oldHeight = [heightForRowCache objectForKey:key];
    if (oldHeight == nil || newHeight.intValue != oldHeight.intValue) {
        if (!reloadRequested) {
            [self.tableView performSelector:@selector(reloadData) withObject:nil afterDelay:0];
            reloadRequested = TRUE;
        }
    }

    // Save the new height in the cache
    [heightForRowCache setObject:newHeight forKey:key];

    NSLog(@"cellForRow: %@ height=%@ >> %@", indexPath, oldHeight, newHeight);

    return currentCell;
}
3
Symmetric

私が見た中で、これの最良の実装は、Three20 TTTableViewクラスがそれを行う方法です。

基本的に、それらにはheightForRowAtIndexPath:メソッドをTTTableCellクラスのクラスメソッドに委譲するUITableViewControllerから派生したクラスがあります。

次に、そのクラスは、常にdrawメソッドで行うのと同じ種類のレイアウト計算を行うことによって、適切な高さを返します。これをクラスに移動することにより、セルインスタンスに依存するコードの記述を回避できます。

本当に他に選択肢はありません-パフォーマンス上の理由から、フレームワークは高さを要求する前にセルを作成しません。多くの行が存在する可能性がある場合も、実際にはそれを行いたくありません。

3
Malcolm Box

本当に良い質問です。これについての洞察も求めています。

問題の明確化:

  1. 行の高さは(cellForRowAtIndexPath)の前に呼び出されます
  2. ほとんどの人はCELL(cellForRowAtIndexPath)内の高さタイプ情報を計算します。

いくつかのソリューションは驚くほどシンプル/効果的です:

  • 解決策1:セルのスペックを計算するようにheightForRowAtIndexPathを強制します。マッシモカファロ9月9日

  • 解決策2:セルの最初のパス「標準サイズ」を実行し、セルの高さがある場合は結果をキャッシュしてから、新しい高さを使用してテーブルを再読み込み-対称

  • 解決策3:もう1つの興味深い答えは、関係するスリー20のようですが、答えに基づいて、この「問題」をはるかに解決しやすくするストーリーボード/ xibに描画されたセルがないようです。

2
Andrew Chung

私が最初に提案したアイデアを採用しました。これはうまく機能しているようで、viewDidLoadですべてのカスタムセルを事前にロードし、インデックスをキーとしてNSMutableDictionaryに格納します。私は関連するコードを投稿しており、このアプローチについて誰もが持っている批評や意見を気に入っています。具体的には、viewDidLoadのnibからUITableViewCellsを作成する方法にメモリリークの問題があるかどうかはわかりません。解放していないためです。

@interface RecentController : UIViewController <UITableViewDelegate, UITableViewDataSource> {

NSArray *listData;
NSMutableDictionary *cellBank;

}

@property (nonatomic, retain) NSArray *listData;
@property (nonatomic, retain) NSMutableDictionary *cellBank;
@end



@implementation RecentController

@synthesize listData;
@synthesize cellBank;

---

- (void)viewDidLoad {

---

self.cellBank = [[NSMutableDictionary alloc] init];

---

//create question objects…

--- 

NSArray *array = [[NSArray alloc] initWithObjects:question1,question2,question3, nil];

self.listData = array;

//Pre load all table row cells
int count = 0;
for (id question in self.listData) {

    NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"QuestionHeaderCell" 
                                                 owner:self 
                                               options:nil];
    QuestionHeaderCell *cell;

    for (id oneObject in nib) {
        if([oneObject isKindOfClass:[QuestionHeaderCell class]])
            cell = (QuestionHeaderCell *) oneObject;

            NSNumber *key = [NSNumber numberWithInt:count];
            [cellBank setObject:[QuestionHeaderCell makeCell:cell 
                                                  fromObject:question] 
                         forKey:key];
            count++;

    }
}

[array release];
[super viewDidLoad];
}



#pragma mark -
#pragma mark Table View Data Source Methods

-(NSInteger) tableView: (UITableView *) tableView
numberOfRowsInSection: (NSInteger) section{

return [self.listData count];

}

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

NSNumber *key = [NSNumber numberWithInt:indexPath.row];
return [cellBank objectForKey:key];


}

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

NSNumber *key = [NSNumber numberWithInt:indexPath.row];
return [[cellBank objectForKey:key] totalCellHeight];

}

@end



@interface QuestionHeaderCell : UITableViewCell {

UITextView *title;
UILabel *createdBy;
UILabel *category;
UILabel *questionText;
UILabel *givenBy;
UILabel *date;
int totalCellHeight;

}

@property (nonatomic, retain) IBOutlet UITextView *title;
@property (nonatomic, retain) IBOutlet UILabel *category;
@property (nonatomic, retain) IBOutlet UILabel *questionText;
@property (nonatomic, retain) IBOutlet UILabel *createdBy;
@property (nonatomic, retain) IBOutlet UILabel *givenBy;
@property (nonatomic, retain) IBOutlet UILabel *date;
@property int totalCellHeight;

+(UITableViewCell *) makeCell:(QuestionHeaderCell *) cell 
               fromObject:(Question *) question;

@end



@implementation QuestionHeaderCell
@synthesize title;
@synthesize createdBy;
@synthesize givenBy;
@synthesize questionText;
@synthesize date;
@synthesize category;
@synthesize totalCellHeight;







- (void)dealloc {
[title release];
[createdBy release];
[givenBy release];
[category release];
[date release];
[questionText release];
[super dealloc];
}

+(UITableViewCell *) makeCell:(QuestionHeaderCell *) cell 
                 fromObject:(Question *) question{


NSUInteger currentYpos = 0;

cell.title.text = question.title;

CGRect frame = cell.title.frame;
frame.size.height = cell.title.contentSize.height;
cell.title.frame = frame;
currentYpos += cell.title.frame.size.height + 2;


NSMutableString *tempString = [[NSMutableString alloc] initWithString:question.categoryName];
[tempString appendString:@"/"];
[tempString appendString:question.subCategoryName];

cell.category.text = tempString;
frame = cell.category.frame;
frame.Origin.y = currentYpos;
cell.category.frame = frame;
currentYpos += cell.category.frame.size.height;

[tempString setString:@"Asked by "];
[tempString appendString:question.username];
cell.createdBy.text = tempString;

frame = cell.createdBy.frame;
frame.Origin.y = currentYpos;
cell.createdBy.frame = frame;
currentYpos += cell.createdBy.frame.size.height;


cell.questionText.text = question.text;
frame = cell.questionText.frame;
frame.Origin.y = currentYpos;
cell.questionText.frame = frame;
currentYpos += cell.questionText.frame.size.height;


[tempString setString:@"Advice by "];
[tempString appendString:question.lastNexusUsername];
cell.givenBy.text = tempString;
frame = cell.givenBy.frame;
frame.Origin.y = currentYpos;
cell.givenBy.frame = frame;
currentYpos += cell.givenBy.frame.size.height;


cell.date.text = [[[MortalDataStore sharedInstance] dateFormat] stringFromDate: question.lastOnDeck];
frame = cell.date.frame;
frame.Origin.y = currentYpos-6;
cell.date.frame = frame;
currentYpos += cell.date.frame.size.height;

//Set the total height of cell to be used in heightForRowAtIndexPath
cell.totalCellHeight = currentYpos;

[tempString release];
return cell;

}

@end
0
JohnRock

このトピックについて何度も検索したところ、ようやくこのロジックが思いついた。単純なコードですが、おそらく十分に効率的ではありませんが、これまでのところ、私が見つけることができる最高のコードです。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  NSDictionary * Object=[[NSDictionary alloc]init];
  Object=[Rentals objectAtIndex:indexPath.row];
  static NSString *CellIdentifier = @"RentalCell";
  RentalCell *cell = (RentalCell *)[tableView
                                  dequeueReusableCellWithIdentifier:CellIdentifier];
  if (cell == nil)
  {
      cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
  }
   NSString* temp=[Object objectForKey:@"desc"];
   int lines= (temp.length/51)+1;
   //so maybe here, i count how many characters that fit in one line in this case 51
   CGRect correctSize=CGRectMake(cell.infoLabel.frame.Origin.x, cell.infoLabel.frame.Origin.y,    cell.infoLabel.frame.size.width, (15*lines));
   //15 (for new line height)
   [cell.infoLabel setFrame:correctSize];
   //manage your cell here
}

そしてここに残りのコードがあります

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

    NSDictionary * Object=[[NSDictionary alloc]init];
    Object=[Rentals objectAtIndex:indexPath.row];
    static NSString *CellIdentifier = @"RentalCell";
    RentalCell *cells = (RentalCell *)[tableView
                                  dequeueReusableCellWithIdentifier:CellIdentifier];
    NSString* temp=[Object objectForKey:@"desc"];
    int lines= temp.length/51;

    return (CGFloat) cells.bounds.size.height + (13*lines);
}
0
xeravim

これは、ラベルに保持されたメモを含むセルという非常に単純なケースで私が行うことです。メモ自体は、課す最大長に制限されているため、複数行のUILabelを使用し、次の例に示すように、各セルに対して正しい8を動的に計算します。 UITextViewはほとんど同じように処理できます。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
    }

    // Configure the cell...
    Note *note = (Note *) [fetchedResultsController objectAtIndexPath:indexPath];
    cell.textLabel.text = note.text;
    cell.textLabel.numberOfLines = 0; // no limits

    DateTimeHelper *dateTimeHelper = [DateTimeHelper sharedDateTimeHelper];
    cell.detailTextLabel.text = [dateTimeHelper mediumStringForDate:note.date];

    cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;


    return cell;
}


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

    //NSLog(@"heightForRowAtIndexPath: Section %d Row %d", indexPath.section, indexPath.row);
    UITableViewCell *cell = [self tableView: self.tableView cellForRowAtIndexPath: indexPath];
    NSString *note = cell.textLabel.text;
    UIFont *font = [UIFont fontWithName:@"Helvetica" size:14.0];
    CGSize constraintSize = CGSizeMake(280.0f, MAXFLOAT);
    CGSize bounds = [note sizeWithFont:font constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];
    return (CGFloat) cell.bounds.size.height + bounds.height;

}
0
Massimo Cafaro