Извлеките высоту пользовательских прототипов из раскадровки?

При использовании "Динамических прототипов" для указания содержимого UITableView на раскадровке существует свойство "Высота строки", которое может быть установлено на "Пользовательский".

При создании экземпляров ячейки эта настраиваемая высота строки не учитывается. Это имеет смысл, поскольку используемая ячейка прототипа определяется моим кодом приложения во время создания ячейки. Чтобы создать экземпляр всех ячеек при расчете макета, вы получите штраф за производительность, поэтому я понимаю, почему это невозможно.

Затем возникает вопрос, можно ли каким-либо образом получить высоту, заданную идентификатором повторного использования соты, например.

[myTableView heightForCellWithReuseIdentifier:@"MyCellPrototype"];

или что-то в этом направлении? Или мне нужно дублировать явные высоты строк в моем коде приложения, с последующим бременем обслуживания?

Решено, с помощью @TimothyMoose:

Высоты сохраняются в самих ячейках, что означает, что единственный способ получить высоту - создать экземпляр прототипов. Один из способов сделать это - предварительно очистить ячейки вне обычного метода обратного вызова. Вот мой небольшой POC, который работает:

#import "ViewController.h"

@interface ViewController () {
    NSDictionary* heights;
}
@end

@implementation ViewController

- (NSString*) _reusableIdentifierForIndexPath:(NSIndexPath *)indexPath
{
    return [NSString stringWithFormat:@"C%d", indexPath.row];
}

- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    if(!heights) {
        NSMutableDictionary* hts = [NSMutableDictionary dictionary];
        for(NSString* reusableIdentifier in [NSArray arrayWithObjects:@"C0", @"C1", @"C2", nil]) {
            CGFloat height = [[tableView dequeueReusableCellWithIdentifier:reusableIdentifier] bounds].size.height;
            hts[reusableIdentifier] = [NSNumber numberWithFloat:height];
        }
        heights = [hts copy];
    }
    NSString* prototype = [self _reusableIdentifierForIndexPath:indexPath];
    return [heights[prototype] floatValue];
}

- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 3;
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (UITableViewCell*) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString* prototype = [self _reusableIdentifierForIndexPath:indexPath];
    UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:prototype];
    return cell;
}

@end

Ответ 1

Для статической (не управляемой данными) высоты вы можете просто разблокировать ячейку один раз и сохранить высоту:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSNumber *height;
    if (!height) {
        UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"MyCustomCell"];
        height = @(cell.bounds.size.height);
    }
    return [height floatValue];
}

Для динамической (управляемой данными) высоты вы можете сохранить ячейку прототипа в контроллере представления и добавить метод к классу ячеек, который вычисляет высоту с учетом содержимого по умолчанию экземпляра прототипа, такого как размещение в режиме просмотра, шрифты и т.д.:

- (MyCustomCell *)prototypeCell
{
    if (!_prototypeCell) {
        _prototypeCell = [self.tableView dequeueReusableCellWithIdentifier:@"MyCustomCell"];
    }
    return _prototypeCell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Data for the cell, e.g. text for label
    id myData = [self myDataForIndexPath:indexPath];

    // Prototype knows how to calculate its height for the given data
    return [self.prototypeCell myHeightForData:myData];
}

Конечно, если вы используете пользовательскую высоту, у вас, вероятно, есть несколько прототипов ячеек, поэтому вы можете хранить их в словаре или что-то в этом роде.

Насколько я могу судить, представление таблицы не пытается повторно использовать прототип, по-видимому, потому, что он был удален из cellForRowAtIndexPath:. Этот подход очень сработал для нас, потому что он позволяет дизайнеру изменять макеты ячеек в раскадровке без каких-либо изменений кода.

Изменить: уточнил смысл примера кода и добавил пример для случая статической высоты.

Ответ 2

Я создал категорию для UITableView некоторое время назад, что может оказаться полезным для этого. Он хранит ячейки 'prototype', используя связанные объекты для повторного использования прототипов и предоставляет удобный метод для получения высоты строки, назначенной в раскадровке. Прототипы освобождаются, когда представление таблицы отменяется.

UITableView + PrototypeCells.h

#import <UIKit/UIKit.h>

@interface UITableView (PrototypeCells)

- (CGFloat)heightForRowWithReuseIdentifier:(NSString*)reuseIdentifier;
- (UITableViewCell*)prototypeCellWithReuseIdentifier:(NSString*)reuseIdentifier;

@end

UITableView + PrototypeCells.m

#import "UITableView+PrototypeCells.h"
#import <objc/runtime.h>

static char const * const key = "prototypeCells";

@implementation UITableView (PrototypeCells)
- (void)setPrototypeCells:(NSMutableDictionary *)prototypeCells {
    objc_setAssociatedObject(self, key, prototypeCells, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSMutableDictionary *)prototypeCells {
    return objc_getAssociatedObject(self, key);
}

- (CGFloat)heightForRowWithReuseIdentifier:(NSString*)reuseIdentifier {
    return [self prototypeCellWithReuseIdentifier:reuseIdentifier].frame.size.height;
}

- (UITableViewCell*)prototypeCellWithReuseIdentifier:(NSString*)reuseIdentifier {
    if (self.prototypeCells == nil) {
        self.prototypeCells = [[NSMutableDictionary alloc] init];
    }

    UITableViewCell* cell = self.prototypeCells[reuseIdentifier];
    if (cell == nil) {
        cell = [self dequeueReusableCellWithIdentifier:reuseIdentifier];
        self.prototypeCells[reuseIdentifier] = cell;
    }
    return cell;
}

@end

Использование

Получение статической высоты, установленной в раскадровке, так же просто:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return [tableView heightForRowWithReuseIdentifier:@"cellIdentifier"];
}

Предполагая просмотр таблицы в нескольких разделах:

enum {
    kFirstSection = 0,
    kSecondSection
};

static NSString* const kFirstSectionRowId = @"section1Id";
static NSString* const kSecondSectionRowId = @"section2Id";

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CGFloat height = tableView.rowHeight; // Default UITableView row height
    switch (indexPath.section) {
        case kFirstSection:
            height = [tableView heightForRowWithReuseIdentifier:kFirstSectionRowId];
            break;
        case kSecondSection:
            height = [tableView heightForRowWithReuseIdentifier:kSecondSectionRowId];
    }
    return height;
}

И, наконец, если высота строки динамическая:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    id thisRowData = self.allData[indexPath.row]; // Obtain the data for this row

    // Obtain the prototype cell
    MyTableViewCell* cell = (MyTableViewCell*)[self prototypeCellWithReuseIdentifier:@"cellIdentifier"];

    // Ask the prototype cell for its own height when showing the specified data
    return [cell heightForData:thisRowData];
}