Динамические высоты/ширины UILabel в UITableViewCell во всех ориентациях
Мой вопрос по существу сводится к наилучшему способу поддержки динамических высот UILabel (и, я полагаю, других элементов) в UITableCell, а также правильно изменить размер/высоту и высоту метки высоты при вращении.
Я знаю, как получить ожидаемую высоту UILabels, и, соответственно, размер высоты, но кажется, что вы слишком сложны, когда поддерживаете ландшафтную ориентацию.
Я думаю, что это может пойти по пути layoutSubviews, но я не уверен, как вы могли бы объединить это с таблицей, необходимой для вычисления высоты ячеек, Еще один интересный пост устанавливает фрейм вручную на init, чтобы убедиться, что он выполняет вычисления с известной величины, но это касается только части проблемы.
Итак, вот образ того, с чем я имею дело, с красными стрелками, указывающими на ярлыки динамической высоты, и синие стрелки, указывающие на ячейки, которые будут изменять высоту.
![Dynamic UILabel/Cell Height/Width]()
Мне удалось заставить его работать правильно, но не уверен, что его правильный метод.
Вот несколько вещей, которые я узнал:
- Элемент cell.frame в
cellForRowAtIndexPath
всегда дает свой размер в портретном режиме. (т.е. для приложения iphone, он всегда сообщает 320).
- Сохранение ячейки прототипа для ссылки в свойстве имеет ту же проблему, всегда в портретном режиме
- Авторезистирующие маски для ширины меток кажутся бесполезными в этом прецеденте и фактически вызывают проблемы с расчетной высотой при вращении
- Таблица table.frame в
cellForRowAtIndexPath
дает размер в правильной ориентации
- Вам нужно вызвать
[tableView reloadData]
при вращении. Я не мог найти другого способа обновить высоты ячеек и ярлыков.
Вот шаги, которые я предпринял, чтобы заставить это работать для меня:
- Отключите любые маски autoresize на UILabels, так как они по какой-то причине мешают получить правильную высоту метки.
-
В viewDidLoad
Я беру ссылку на каждый шрифт метки и поплавок для процента ширины метки по сравнению с табличным изображением в портрете
CWProductDetailDescriptionCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"DescriptionCell"];
self.descriptionLabelWidthPercentage = cell.descriptionLabel.frame.size.width / 320;
self.descriptionLabelFont = cell.descriptionLabel.font;
-
В heightForRowAtIndexPath
Я вычисляю высоту ячейки, используя ширину tableView и процент, который я уже захватил:
case TableSectionDescription:
CGFloat labelWidth = self.tableView.frame.size.width * self.descriptionLabelWidthPercentage;
CGSize newLabelFrameSize = [self sizeForString:self.product.descriptionText WithConstraint:CGSizeMake(labelWidth, MAXFLOAT) AndFont:self.descriptionLabelFont];
return newLabelFrameSize.height + kTextCellPadding;
-
В cellForRowAtIndexPath
Я вычисляю фрейм для фрейма метки и обновляю его
cell = [tableView dequeueReusableCellWithIdentifier:@"DescriptionCell"];
((CWProductDetailDescriptionCell *)cell).descriptionLabel.text = self.product.descriptionText;
CGRect oldFrame = ((CWProductDetailDescriptionCell *)cell).descriptionLabel.frame;
CGFloat labelWidth = self.tableView.frame.size.width * self.descriptionLabelWidthPercentage;
CGSize newLabelFrameSize = [self sizeForString:self.product.descriptionText WithConstraint:CGSizeMake(labelWidth, MAXFLOAT) AndFont:((CWProductDetailDescriptionCell *)cell).descriptionLabel.font];
((CWProductDetailDescriptionCell *)cell).descriptionLabel.frame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, labelWidth, newLabelFrameSize.height);
-
В didRotateFromInterfaceOrientation
вам нужно [self.tableView reloadData]
Проблемы с этим методом:
- Вы не можете использовать авторезистирующие маски и поля, которые вы могут быть установлены в IB
- они не срабатывают до тех пор, пока вы не вернете ячейку в
cellForRowAtIndexPath
(не уверены, верно ли это)
- они каким-то образом испортили расчет высоты, а ваши метки заканчиваются выше, чем желаемый в ландшафте.
- Если ваша ширина ярлыка не равна проценту в портрет и пейзаж, вам нужно знать оба процента.
- Вам нужно знать/рассчитать процентные доли ваших меток. В идеале вы сможете рассчитать их на лету после того, как маска с автоматическим изменением сделана.
- Это просто неудобно. У меня есть чувство, что я столкнусь с болью в режиме редактирования с отступом.
Так. Каков правильный способ сделать это? Я видел много вопросов SO, но никто этого не делает.
Ответы
Ответ 1
По-видимому, потребовалось выписать мой гигантский вопрос и сделать перерыв с компьютера, чтобы понять это.
Итак, решение:
Основные шаги для динамического вычисления высот UILabel "на лету" и использования авторезистирующих масок:
- Сохраняйте ячейку прототипа для каждой ячейки, которая должна быть настроена по высоте.
- В
heightForRowAtIndexPath
- отрегулируйте ширину ссылочной ячейки для ориентации, в которой вы находитесь.
- тогда вы можете надежно использовать содержащуюся ширину рамки UILabel для расчета высоты
- Не выполняйте никаких корректировок кадров в
cellForRowAtIndexPath
, кроме начальной настройки
- UITableViewCells выполняет все свои макеты сразу после того, как это вызвано, поэтому ваши усилия в лучшем случае бесполезны, в худшем случае они прикручивают вещи.
- Корректировка кадров в
willDisplayCell forRowAtIndexPath
- Я в ярости Я забыл об этом
- Это происходит после внутреннего макета UITableViewCell и прямо перед тем, как он рисует
- Ваши метки с авторезистирующими масками на них уже будут иметь правильную ширину, которую вы можете использовать для вычислений
В viewDidLoad захватывают ссылки на ячейки, которые будут нуждаться в настройках высоты
Обратите внимание, что они сохраняются/сильно (ARC) и никогда не добавляются в таблицу
self.descriptionReferenceCell = [self.tableView dequeueReusableCellWithIdentifier:@"DescriptionCell"];
self.attributeReferenceCell = [self.tableView dequeueReusableCellWithIdentifier:@"AttributeCell"];
В heightForRowAtIndexPath
выполните вычисления для ячеек переменной высоты
я сначала обновить ширину моей эталонной ячейки так, что она выкладывает свои подвидов (UILabels), которые я затем можно использовать для вычислений. Затем я использую обновленную ширину меток, чтобы определить высоту моих меток, и добавьте необходимое заполнение ячеек для вычисления высоты моей ячейки. (Помните, что эти вычисления необходимы, только если ваша ячейка меняет высоты вместе с вашей меткой).
UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
float width = UIDeviceOrientationIsPortrait(orientation) ? 320 : 480;
case TableSectionDescription:
CGRect oldFrame = self.descriptionReferenceCell.frame;
self.descriptionReferenceCell.frame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, width, oldFrame.size.height);
CGFloat referenceWidth = self.descriptionReferenceCell.descriptionLabel.frame.size.width;
CGSize newLabelFrameSize = [self sizeForString:self.product.descriptionText WithConstraint:CGSizeMake(referenceWidth, MAXFLOAT) AndFont:self.descriptionReferenceCell.descriptionLabel.font];
return newLabelFrameSize.height + kTextCellPadding;
В cellForRowAtIndexPath
Не выполняйте динамические макеты, связанные с ориентацией
В willDisplayCell forRowAtIndexPath
Обновить высоту ярлыков
Поскольку ячейки таблицы выполнили layoutSubviews
, метки уже имеют правильную ширину, которую я использую для вычисления точной высоты меток. Я могу безопасно обновлять высоту ярлыков.
case TableSectionDescription:
CGRect oldFrame = ((CWProductDetailDescriptionCell *)cell).descriptionLabel.frame;
CGSize newLabelFrameSize = [self sizeForString:self.product.descriptionText WithConstraint:CGSizeMake(oldFrame.size.width, MAXFLOAT) AndFont:((CWProductDetailDescriptionCell *)cell).descriptionLabel.font];
((CWProductDetailDescriptionCell *)cell).descriptionLabel.frame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, oldFrame.size.width, newLabelFrameSize.height);
break;
Обновить данные при вращении
Ваши видимые ячейки не будут обновлять свои высоты и метки, если вы не перезагружаете данные.
-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{
[self.tableView reloadData];
}
Единственное, что нужно отметить, это то, что я использую вспомогательный класс для вычисления высоты ярлыка, который просто обертывает основной вызов. На самом деле он больше не сохраняет большую часть кода (у меня там были другие вещи), поэтому просто используйте обычный метод прямо из вашей строки.
- (CGSize) sizeForString:(NSString*)string WithConstraint:(CGSize)constraint AndFont:(UIFont *)font {
CGSize labelSize = [string sizeWithFont:font
constrainedToSize:constraint
lineBreakMode:UILineBreakModeWordWrap];
return labelSize;
}
Ответ 2
Я согласен с большинством ваших наблюдений. Несколько мыслей:
Вы говорите:
Элемент cell.frame в cellForRowAtIndexPath всегда дает свой размер в портретный режим. (т.е. для приложения iphone, он всегда сообщает 320).
Это не мой опыт. В моем tableView:cellForRowAtIndexPath
я просто положил
NSLog(@"Cell frame.size.width=%f", cell.frame.size.width);
И он сообщает 320 или 480 в зависимости от ориентации. Итак, я не вижу никакой пользы, переместив логику вычисления кадров в willDisplayCell.
Вы говорите:
В heightForRowAtIndexPath
Я вычисляю высоту ячейки, используя tableView width и процент, который я уже захватил:
Да, вы можете это сделать. Если ваши описания на правой стороне ячейки, вероятно, будут очень длинными, вам может потребоваться сохранить ярлык на левой стороне ячейки фиксированной ширины и воспользоваться более широким экраном, чтобы действительно увеличить длинное описание, которое у вас есть на правая сторона ячейки. Но если вы часто имеете более короткие описания с правой стороны, то правильная перестройка на основе целевого процента ширины экрана является хорошей идеей. Лично, вместо того, чтобы конвертировать пиксели на основе процентов, я мог бы просто использовать фиксированные отношения (например, метка слева всегда занимает 33% от ширины, метка справа всегда занимает 66% ячейки, и это может упростить процент логика, хотя она функционально эквивалентна).
Вы говорите:
Вы не можете использовать маски и поля авторезинирования, которые вы можете установить в IB
Да, я никогда не пробовал (до сих пор, и я вижу то же поведение, которое вы описываете), но даже если бы это сработало, это была бы плохая идея, потому что вам действительно нужно пересчитать высоту, основанную на слове, 're использование, так что, хотя он может приблизиться к вам, вам все равно нужно пересчитать высоты в любом случае, чтобы все было правильно. (Честно говоря, мне просто хотелось бы сказать "авторезистировать высоту, основанную на шрифте, ширине и тексте ярлыка, но этого просто не существует.) Тем не менее, я согласен с вами в том, что странно, что он не работает лучше. обрабатывайте это более грациозно.
Ты сказал:
Ваши видимые ячейки не будут обновлять свои высоты и метки, если вы не перезагрузите данные.
Затем вы предложили вызвать reloadData
. Но уже не didRotateFromInterfaceOrientation
, потому что я вижу, что мой портрет UILabels вспыхивает сразу после того, как он идет в пейзаж, но до того, как здесь происходит перерисовка ландшафта. Я не хочу подклассифицировать UITableViewCell и переопределять drawRect
, но я не уверен, как еще раз переформатировать мои кадры UILabel до их повторного рисования в ландшафте. И моя первоначальная попытка переместить это на willRotateToInterfaceOrientation
не сработала, но, возможно, я ничего не сделал там.
Ответ 3
Спасибо, это было очень полезно. Тем не менее, я не хотел жестко кодировать любую ширину. Я хорошо провел время, рассматривая эту проблему, и вот что я нашел.
В принципе, существует проблема с курицей и яйцом, поскольку представление таблицы должно знать высоту ячеек, чтобы сделать свой собственный макет, но ячейки необходимо настроить для геометрии табличного представления таблицы перед их высоты могут быть определены.
Один из способов разбить эту круговую зависимость - это жестко закодировать ширину, равную 320 или 480, что предлагается в другом месте на этой странице. Однако мне не нравилась идея жесткого кодирования.
Другая идея - рассчитать все высоты вверх, используя ячейку прототипа. Я пробовал это, но все равно столкнулся с ошибками, потому что создание UITableViewCell
оставляет вас только с ячейкой, которая не настроена на фактический стиль и размер представления таблицы. По-видимому, нет способа вручную настроить ячейку для определенного вида таблицы; вы должны позволить UITableView
сделать это.
Стратегия, о которой я остановился, - позволить представлению таблицы делать все, как обычно, включая создание и настройку собственных ячеек, а затем, когда все ячейки настроены, вычислите фактические высоты строк и обновите Table view. Недостатком является то, что представление таблицы загружается дважды (один раз с высотами по умолчанию и снова с правильными высотами), но вверху является то, что это должно быть довольно чистое и надежное решение.
В этом примере предполагается, что у вас есть переменная экземпляра _cellHeights
, которая представляет собой массив массивов, которые содержат рассчитанные высоты строк в разделе просмотра таблицы:
@interface MyViewController () {
NSArray *_cellHeights; // array of arrays (for each table view section) of numbers (row heights)
}
Напишите способ вычисления всех высот ячейки на основе текущего содержимого ячейки:
- (void)_recalculateCellHeights
{
NSMutableArray *cellHeights = [[NSMutableArray alloc] init];
for (NSUInteger section=0; section<[self.tableView numberOfSections]; section++) {
NSMutableArray *sectionCellHeights = [[NSMutableArray alloc] init];
for (NSUInteger row=0; row<[self.tableView numberOfRowsInSection:section]; row++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
// Set cell width to match table view width
CGRect cellFrame = cell.frame;
cellFrame.size.width = CGRectGetWidth(self.tableView.frame);
cell.frame = cellFrame;
CGFloat cellHeight = self.tableView.rowHeight; // default
if (cell.textLabel.numberOfLines == 0 || (cell.detailTextLabel && cell.detailTextLabel.numberOfLines == 0)) {
[cell layoutIfNeeded]; // this is necessary on iOS 7 to give the cell text labels non-zero frames
// Calculate the height of the cell based on the text
[cell.textLabel sizeToFit];
[cell.detailTextLabel sizeToFit];
cellHeight = CGRectGetHeight(cell.textLabel.frame) + CGRectGetHeight(cell.detailTextLabel.frame);
// This is the best I can come up with for this :(
if (self.tableView.separatorStyle != UITableViewCellSeparatorStyleNone) {
cellHeight += 1.0;
}
}
[sectionCellHeights addObject:[NSNumber numberWithFloat:cellHeight]];
}
[cellHeights addObject:sectionCellHeights];
}
_cellHeights = cellHeights;
}
В -tableView:cellForRowAtIndexPath:
создайте ячейку как обычно и заполните textLabel, не выполняя никаких макетов (как сказал Боб в другом месте на этой странице: "UITableViewCells выполняет все свои макеты сразу после того, как это вызвано, поэтому ваши усилия в лучшем случае бесполезны, в худшем случае они заворачивают вещи" ). Важно установить для свойства label numberOfLines
значение 0, чтобы разрешить динамическую высоту.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
// Assign any fonts or other adjustments which may affect height here
// Enable multiple line support so textLabel can grow vertically
cell.textLabel.numberOfLines = 0;
}
// Configure the cell...
cell.textLabel.text = [self.texts objectAtIndex:indexPath.section];
return cell;
}
В -tableView:heightForRowAtIndexPath:
верните предварительно рассчитанные высоты ячеек:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (_cellHeights) {
NSArray *sectionCellHeights = [_cellHeights objectAtIndex:indexPath.section];
return [[sectionCellHeights objectAtIndex:indexPath.row] floatValue];
}
return self.tableView.rowHeight; // default
}
В -viewDidAppear:
запускается пересчет высот ячейки и обновляется таблица. Обратите внимание, что это должно произойти в -viewDidAppear:
времени, а не -viewWillAppear:
времени, потому что в момент -viewWillAppear:
ячейки еще не настроены для геометрии табличного представления.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self _recalculateCellHeights];
[self.tableView reloadData];
}
В -didRotateFromInterfaceOrientation:
выполните одно и то же:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[self _recalculateCellHeights];
[self.tableView reloadData];
}
Нет необходимости делать что-либо особенное в -tableView:willDisplayCell:forRowAtIndexPath:
.