UITableView 部分标题的自动布局问题
Posted
技术标签:
【中文标题】UITableView 部分标题的自动布局问题【英文标题】:Auto Layout issue with a UITableView section header 【发布时间】:2014-02-07 18:31:37 【问题描述】:我正在使用UITableViewController
。我有一个项目表,如果他进行更多编辑,用户可以删除这些项目。当他进入编辑模式时,我想显示一个标题,提供删除所有项目的选项。同时,它应该显示一个标签,提供有关正在使用多少空间的信息。如果设备进入横向模式,我希望它自动调整大小。据我所知,我需要使用自动布局来执行此操作。
我很想在 Storyboard 中设计的 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's implementation of -layoutSubviews needs to call super.'
有没有人建议如何消除警告?
【问题讨论】:
仅供参考,您可以将标题视图直接放在情节提要中的UITableViewController
上。只需将视图拖到表格顶部即可。
这听起来是个好主意@TimothyMoose。不过我确实试过了。它不像我现在的方法那样动画,即使它消失了,也会留下空白。
你的“UITableView header”实际上是一个section header。为了清楚起见,最好将其称为节标题。
很公平。我已编辑问题以反映这一点。
仅供参考,当您使用自动布局时,您不应手动设置组件的框架。
【参考方案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;
【讨论】:
我实际上并没有遵循这条线的重点:self.headerContentView = [self headerContentView];
。应该是self.headerContentView = [self headerView];
?
不,这个方法应该返回视图,然后将显示在节标题中。这取决于您是从 nib 实例化它还是以编程方式创建它,问题是您只执行一次,因为该方法在 viewDidLoad 方法中调用。我会将其重命名为更清晰的名称。
我已经更新了我的答案,所以现在它包含您原始帖子中的代码。
我试用了您的新代码,现在我收到错误消息:“*** Assertion failure in -[UITableView layoutSublayersOfLayer:], /SourceCache/UIKit_Sim/UIKit-2903.23/UIView.m:8540 " 我还更改了您的答案,以修复编译时发生的一些小错误。
毕竟更新了我的答案。现在它完全可以工作了。【参考方案2】:
我在这里为这篇文章创建了一个 GitHub 存储库:https://github.com/bilobatum/AnimatedTableHeaderDemo
此解决方案实现了一个表头视图,即self.tableView.tableHeaderView
,而不是具有单个节的表视图的节头。
表格标题视图及其子视图为测试目的而着色。为测试目的选择任意表头高度。
当表格视图进入编辑模式时,表格标题被延迟实例化并动画到位。当表格视图退出编辑模式时,动画会隐藏表格标题。
一般来说,您不应该在使用自动布局时设置框架。但是,表头在某种意义上是一种特殊情况。不要使用自动布局来调整表头的大小或位置。相反,您必须设置表头的框架(实际上,您只需要设置矩形的高度)。反过来,系统会将表头的框架转换为约束。
但是,可以在表头的子视图上使用自动布局。其中一些约束安装在表头视图上。
@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(30.0@999)]-[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;
它的动画效果不如部分标题,但现在就可以了。
这并不能真正解决实际问题(因此是“解决方法”),所以我不会接受它作为解决方案。
【讨论】:
以上是关于UITableView 部分标题的自动布局问题的主要内容,如果未能解决你的问题,请参考以下文章
具有自动布局的 iOS8 UITableView 在单元格上方有一个空白区域
在自定义 UIView 中为部分自动调整 UITableView