带有UITATIONVIEW部分标题的自动布局问题



我正在使用UITableViewController。我有一张用户可以删除的项目表,如果他可以进行更多编辑。当他进入编辑模式时,我想显示一个标题,该标头可以提供一个删除所有项目的选项。同时,它应该显示一个标签,提供有关正在使用多少空间的信息。如果设备进入景观模式,我希望它自动调整大小。据我所知,我需要使用AutoLayout来执行此操作。

我很想在故事板设计的UIView中设置标头,但是故事板只允许查看控制器而不是视图。我知道我可以拥有一个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.'

有人对如何摆脱警告有什么建议?

看来,当您的部分重新加载时,在某些时刻,UaithingView都会引用旧部分标题和新标题。如果是相同的观点,则会出现一些问题。因此,您必须始终从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;
}

我在此处为这篇文章创建了一个github回购:https://github.com/bilobatum/animatedtableheaderdemo

此解决方案实现表标头视图,即self.tableView.tableHeaderView,而不是带有单个部分的表视图的部分标题。

表标头视图及其子视图是彩色的,以进行测试。选择任意表标头高度用于测试目的。

当表视图输入编辑模式时,表标头懒惰地实例化,并将其动画到位。当表视图退出编辑模式时,动画隐藏了表标头。

通常,您不应在使用自动布局时设置帧。但是,从某种意义上说,桌子头是一种特殊情况。不要使用自动布局来尺寸或放置表标头。取而代之的是,您必须设置一个表标头的框架(实际上,您只需要设置RECT的高度)即可。反过来,系统将将表标头的框架转换为约束。

但是,可以在表标头的子视图上使用自动布局。这些约束中的一些安装在表标头视图上。

@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

自您提出问题以来很长一段时间,但答案也许对您(或其他人)有帮助。

AutoLayout具有(自动)为整个截面宽度添加了一个约束(调试输出约束列表中的最后一个)。当然,这应该没问题,因为计算子视图的框架时会考虑宽度。但是有时在框架的计算中似乎存在舍入错误...

只需将较低的优先级添加到一个子视图之一的宽度值中即可解决问题:

...@@@| - [deleteallbutton(30.0@999)] - [CurrsizeText] - |"

如果按钮宽度不变...

我尝试使用的解决方法是跳过截面标题,直接转到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;
}

它的动画状态不如截面标题很好,但现在可以做到。

这并不能真正解决实际问题(因此"解决方法"),因此我不会接受它作为解决方案。

最新更新