Ошибка автоматической компоновки с заголовком раздела UITableView
Я работаю с UITableViewController
. У меня есть таблица элементов, которую пользователь может удалить, если он перейдет к редактированию. Когда он переходит в режим редактирования, я хочу показать заголовок, который дает возможность удалить все элементы. В то же время, он должен показывать ярлык, дающий информацию о том, сколько места используется. Я хочу, чтобы это автоматически изменялось, если устройство переходит в альбомный режим. Из того, что я могу сказать, мне нужно использовать автозапуск, чтобы сделать это.
Мне бы очень хотелось настроить заголовок в UIView
, разработанном в Storyboard, но раскадровка позволяет только контроллеры просмотра, а не представления. Я знаю, что у меня может быть XIB файл, но я бы предпочел избежать этого, если бы мог.
Для начала я переопределил свойство editing
, чтобы перерисовать таблицу в режиме редактирования.
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
NSIndexSet *set = [NSIndexSet indexSetWithIndex:0];
[self.tableView reloadSections:set withRowAnimation:UITableViewRowAnimationAutomatic];
}
Я использую этот код для вставки заголовка раздела, если это необходимо:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
if (self.isEditing)
return [self headerView];
else
return nil;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
if (self.isEditing)
return [self headerView].frame.size.height;
else
return 0;
}
Магия происходит в методе - headerView
. Он возвращает UIView *
, получая его из кеша, если это необходимо. Он добавляет кнопку и метку, а затем устанавливает ограничения. Я использовал те же ограничения в раскадровке, и у меня не было никаких проблем.
- (UIView *)headerView
{
if (headerView)
return headerView;
float w = [[UIScreen mainScreen] bounds].size.width;
UIButton *deleteAllButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[deleteAllButton setTitle:@"Delete All" forState:UIControlStateNormal];
CGRect deleteAllButtonFrame = CGRectMake(8.0, 8.0, 30.0, 30); // The autolayout should resize this.
[deleteAllButton setFrame:deleteAllButtonFrame];
deleteAllButton.translatesAutoresizingMaskIntoConstraints = NO;
[deleteAllButton setContentHuggingPriority:252 forAxis:UILayoutConstraintAxisHorizontal];
[deleteAllButton setContentCompressionResistancePriority:751 forAxis:UILayoutConstraintAxisHorizontal];
CGRect textFrame = CGRectMake(47.0, 8.0, 30.0, 30); // The autolayout should resize this.
UILabel *currSizeText = [[UILabel alloc] initWithFrame:textFrame];
currSizeText.text = @"You have a lot of text here telling you that you have stuff to delete";
currSizeText.translatesAutoresizingMaskIntoConstraints = NO;
currSizeText.adjustsFontSizeToFitWidth = YES;
CGRect headerViewFrame = CGRectMake(0, 0, w, 48);
headerView = [[UIView alloc] initWithFrame:headerViewFrame];
//headerView.autoresizingMask = UIViewAutoresizingNone;//UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
//headerView.translatesAutoresizingMaskIntoConstraints = NO;
[headerView addSubview:deleteAllButton];
[headerView addSubview:currSizeText];
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(deleteAllButton, currSizeText);
[headerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-[deleteAllButton]-[currSizeText]-|"
options:0
metrics:nil
views:viewsDictionary]];
[headerView addConstraint:[NSLayoutConstraint constraintWithItem:deleteAllButton
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:headerView
attribute:NSLayoutAttributeHeight
multiplier:0.5
constant:0]];
[headerView addConstraint:[NSLayoutConstraint constraintWithItem:currSizeText
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:headerView
attribute:NSLayoutAttributeHeight
multiplier:0.5
constant:0]];
return headerView;
}
Сейчас все работает красиво. Кнопка сохраняет постоянный размер (поскольку сопротивление обниманию и сжатию выше, чем у метки), и этикетка изменит свой текст, чтобы он соответствовал свободному пространству. При повороте устройства он изменяется. На ярлыке кажется вертикальное центрирование, но на данный момент я готов упустить это.
Однако, когда я сначала настраиваю заголовок раздела, я получаю раздражающее предупреждение автоопределения.
2014-02-07 11:25:19.770 ErikApp[10704:70b] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSLayoutConstraint:0xb9a4ad0 H:|-(NSSpace(20))-[UIButton:0xb99e220] (Names: '|':UIView:0xb9a4680 )>",
"<NSLayoutConstraint:0xb9a4bf0 H:[UIButton:0xb99e220]-(NSSpace(8))-[UILabel:0xb99f530]>",
"<NSLayoutConstraint:0xb9a4c20 H:[UILabel:0xb99f530]-(NSSpace(20))-| (Names: '|':UIView:0xb9a4680 )>",
"<NSAutoresizingMaskLayoutConstraint:0xa2d1680 h=--& v=--& H:[UIView:0xb9a4680(0)]>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0xb9a4bf0 H:[UIButton:0xb99e220]-(NSSpace(8))-[UILabel:0xb99f530]>
Break on objc_exception_throw to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
Моя первая мысль заключалась в том, чтобы изменить возвращаемое свойство UIView
translatesAutoresizingMaskIntoConstraints
на NO
. Когда я это делаю, я получаю сбой вместо предупреждения. Не совсем улучшение.
2014-02-07 10:49:13.041 ErikApp[10597:70b] *** Assertion failure in -[UITableView layoutSublayersOfLayer:], /SourceCache/UIKit_Sim/UIKit-2903.23/UIView.m:8540
2014-02-07 10:49:13.383 ErikApp[10597:70b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Auto Layout still required after executing -layoutSubviews. UITableView implementation of -layoutSubviews needs to call super.'
Есть ли у кого-нибудь предложение о том, что делать, чтобы избавиться от предупреждения?
Ответы
Ответ 1
Кажется, что когда ваш раздел перезагружается, UITableView в какой-то момент имеет ссылку как на старый заголовок раздела, так и на новый. И если это та же точка зрения, появляются некоторые проблемы. Поэтому вы всегда должны предоставлять другое представление из метода tableView:viewForHeaderInSection:
.
Иногда очень полезно, чтобы один экземпляр был представлен в заголовке раздела. Для этого вам нужно создать новое представление каждый раз, когда вас просят заголовок раздела и помещают в него свой пользовательский вид, соответствующим образом настраивая ограничения. Вот пример:
@property (strong, nonatomic) UIView *headerContentView;
- (void)viewDidLoad {
// Create the view, which is to be presented inside the section header
self.headerContentView = [self loadHeaderContentView];
// Note that we have to set the following property to NO to prevent the unsatisfiable constraints
self.headerContentView.translatesAutoresizingMaskIntoConstraints = NO;
}
- (UIView *)loadHeaderContentView {
// Here you instantiate your custom view from a nib
// or create it programmatically. Speaking in terms
// of the OP, it should look like the following. (Note:
// I have removed all the frame-related code as your are
// not supposed to deal with frames directly with auto layout.
// I have also removed the line setting translatesAutoresizingMaskIntoConstraints property
// to NO of the headerContentView object as we do it explicitly in viewDidLoad.
UIButton *deleteAllButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[deleteAllButton setTitle:@"Delete All" forState:UIControlStateNormal];
deleteAllButton.translatesAutoresizingMaskIntoConstraints = NO;
[deleteAllButton setContentHuggingPriority:252 forAxis:UILayoutConstraintAxisHorizontal];
[deleteAllButton setContentCompressionResistancePriority:751 forAxis:UILayoutConstraintAxisHorizontal];
UILabel *currSizeText = [[UILabel alloc] init];
currSizeText.text = @"You have a lot of text here telling you that you have stuff to delete";
currSizeText.translatesAutoresizingMaskIntoConstraints = NO;
currSizeText.adjustsFontSizeToFitWidth = YES;
UIView *headerContentView = [[UIView alloc] init];
[headerContentView addSubview:deleteAllButton];
[headerContentView addSubview:currSizeText];
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(deleteAllButton, currSizeText);
// In the original post you used to have an ambigious layout
// as the Y position of neither button nor label was set.
// Note passing NSLayoutFormatAlignAllCenterY as an option
[headerContentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-[deleteAllButton]-[currSizeText]-|"
options:NSLayoutFormatAlignAllCenterY
metrics:nil
views:viewsDictionary]];
[headerContentView addConstraint:[NSLayoutConstraint constraintWithItem:deleteAllButton
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:headerContentView
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0]];
// Here setting the heights of the subviews
[headerContentView addConstraint:[NSLayoutConstraint constraintWithItem:deleteAllButton
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:headerContentView
attribute:NSLayoutAttributeHeight
multiplier:0.5
constant:0]];
[headerContentView addConstraint:[NSLayoutConstraint constraintWithItem:currSizeText
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:headerContentView
attribute:NSLayoutAttributeHeight
multiplier:0.5
constant:0]];
return headerContentView;
}
- (UIView *)headerView {
UIView *headerView = [[UIView alloc] init];
[headerView addSubview:self.headerContentView];
NSDictionary *views = @{@"headerContentView" : self.headerContentView};
NSArray *hConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[headerContentView]|" options:0 metrics:nil views:views];
NSArray *vConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[headerContentView]|" options:0 metrics:nil views:views];
[headerView addConstraints:hConstraints];
[headerView addConstraints:vConstraints];
return headerView;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
if (self.isEditing)
return [self headerView];
return nil;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
// You need to return a concrete value here
// and not the current height of the header.
if (self.isEditing)
return 48;
return 0;
}
Ответ 2
Я создал репозиторий GitHub для этого сообщения здесь: https://github.com/bilobatum/AnimatedTableHeaderDemo
Это решение реализует представление заголовка таблицы, т.е. self.tableView.tableHeaderView
, вместо заголовков разделов для представления таблицы с одним разделом.
Просмотр заголовка таблицы и ее подпункты окрашены для целей тестирования. Для целей тестирования выбирается произвольная высота заголовка таблицы.
Заголовок таблицы лениво создается и создается на месте, когда представление таблицы переходит в режим редактирования. Анимация скрывает заголовок таблицы, когда представление таблицы выходит из режима редактирования.
В общем, вы не должны устанавливать фреймы при использовании автоматического макета. Однако заголовок таблицы является особым случаем в некотором смысле. Не используйте Auto Layout для размера или размещения заголовка таблицы. Вместо этого вы должны установить кадр заголовка таблицы (на самом деле вам нужно только установить высоту прямоугольника). В свою очередь, система переведет кадр заголовка таблицы в ограничения.
Однако, хорошо использовать Auto Layout в подзаголовках заголовка таблицы. Некоторые из этих ограничений устанавливаются в виде заголовка таблицы.
@interface ViewController ()
@property (nonatomic, strong) NSArray *mockData;
@property (nonatomic, strong) UIButton *deleteAllButton;
@property (nonatomic, strong) UILabel *label;
@property (nonatomic, strong) UIView *headerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.title = @"Fruit";
self.mockData = @[@"Orange", @"Apple", @"Pear", @"Banana", @"Cantalope"];
self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
- (UIButton *)deleteAllButton
{
if (!_deleteAllButton) {
_deleteAllButton = [[UIButton alloc] init];
_deleteAllButton.backgroundColor = [UIColor grayColor];
[_deleteAllButton setTitle:@"Delete All" forState:UIControlStateNormal];
_deleteAllButton.translatesAutoresizingMaskIntoConstraints = NO;
[_deleteAllButton addTarget:self action:@selector(handleDeleteAll) forControlEvents:UIControlEventTouchUpInside];
}
return _deleteAllButton;
}
- (UILabel *)label
{
if (!_label) {
_label = [[UILabel alloc] init];
_label.backgroundColor = [UIColor yellowColor];
_label.text = @"Delete all button prompt";
_label.translatesAutoresizingMaskIntoConstraints = NO;
}
return _label;
}
- (UIView *)headerView
{
if (!_headerView) {
_headerView = [[UIView alloc] init];
// WARNING: do not set translatesAutoresizingMaskIntoConstraints to NO
_headerView.backgroundColor = [UIColor orangeColor];
_headerView.clipsToBounds = YES;
[_headerView addSubview:self.label];
[_headerView addSubview:self.deleteAllButton];
[_headerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_deleteAllButton]-[_label]-|" options:NSLayoutFormatAlignAllCenterY metrics:0 views:NSDictionaryOfVariableBindings(_label, _deleteAllButton)]];
[_headerView addConstraint:[NSLayoutConstraint constraintWithItem:self.deleteAllButton attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:_headerView attribute:NSLayoutAttributeCenterY multiplier:1.0f constant:0.0f]];
}
return _headerView;
}
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
if (self.editing) {
self.tableView.tableHeaderView = self.headerView;
[self.tableView layoutIfNeeded];
}
[UIView animateWithDuration:1.0 animations:^{
CGRect rect = self.headerView.frame;
if (editing) {
rect.size.height = 60.0f; // arbitrary; for testing purposes
} else {
rect.size.height = 0.0f;
}
self.headerView.frame = rect;
self.tableView.tableHeaderView = self.headerView;
[self.tableView layoutIfNeeded];
} completion:^(BOOL finished) {
if (!editing) {
self.tableView.tableHeaderView = nil;
}
}];
}
- (void)handleDeleteAll
{
NSLog(@"handle delete all");
}
#pragma mark - Table view data source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.mockData count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.textLabel.text = self.mockData[indexPath.row];
return cell;
}
@end
Ответ 3
Достаточно то время, когда вы задали вопрос, но, может быть, ответ вам полезен (или другим).
Autolayout (автоматически) добавляет ограничение для ширины заголовка всего раздела (последний в списке ограничений ограничений отладки). Это, конечно, не должно быть проблемой, так как ширина учитывается при расчете кадров подзонов.
Но иногда, по-видимому, возникают ошибки округления при расчете кадров...
Просто добавьте более низкий приоритет к одному из значений ширины подпункта, чтобы решить проблему:
... @ "| - [deleteAllButton ([email protected])] - [currSizeText] - |"
Если ширина кнопки не постоянна, используйте... deleteAllButton ( >= 30 @999)...
Ответ 4
Обходной путь, который я пытался использовать, - пропустить заголовок раздела и перейти непосредственно к tableHeaderView
. Я заменил свойство editing
следующим образом:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
if (editing)
self.tableView.tableHeaderView = [self headerView];
else
self.tableView.tableHeaderView = nil;
}
Он не анимируется так хорошо, как заголовок раздела, но теперь это будет делать.
Это действительно не касается реальной проблемы (следовательно, "обходной путь" ), поэтому я не буду принимать это как решение.