Using autolayout in a tableHeaderView

前端 未结 8 2126
谎友^
谎友^ 2020-12-01 06:03

I have a UIView subclass that contains a multi-line UILabel. This view uses autolayout.

相关标签:
8条回答
  • 2020-12-01 06:09

    Some of the answers here helped me get very close to what I needed. But I encountered conflicts with the constraint "UIView-Encapsulated-Layout-Width" which is set by the system, when rotating the device back-and-forth between portrait and landscape. My solution below is largely based on this gist by marcoarment (credit to him): https://gist.github.com/marcoarment/1105553afba6b4900c10. The solution does not rely on the header view containing a UILabel. There are 3 parts:

    1. A function defined in an extension to UITableView.
    2. Call the function from the view controller's viewWillAppear().
    3. Call the function from the view controller's viewWillTransition() in order to handle device rotation.

    UITableView extension

    func rr_layoutTableHeaderView(width:CGFloat) {
        // remove headerView from tableHeaderView:
        guard let headerView = self.tableHeaderView else { return }
        headerView.removeFromSuperview()
        self.tableHeaderView = nil
    
        // create new superview for headerView (so that autolayout can work):
        let temporaryContainer = UIView(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
        temporaryContainer.translatesAutoresizingMaskIntoConstraints = false
        self.addSubview(temporaryContainer)
        temporaryContainer.addSubview(headerView)
    
        // set width constraint on the headerView and calculate the right size (in particular the height):
        headerView.translatesAutoresizingMaskIntoConstraints = false
        let temporaryWidthConstraint = NSLayoutConstraint(item: headerView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 0, constant: width)
        temporaryWidthConstraint.priority = 999     // necessary to avoid conflict with "UIView-Encapsulated-Layout-Width"
        headerView.addConstraint(temporaryWidthConstraint)
        headerView.frame.size = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
    
        // remove the temporary constraint:
        headerView.removeConstraint(temporaryWidthConstraint)
        headerView.translatesAutoresizingMaskIntoConstraints = true
    
        // put the headerView back into the tableHeaderView:
        headerView.removeFromSuperview()
        temporaryContainer.removeFromSuperview()
        self.tableHeaderView = headerView
    }
    

    Use in UITableViewController

    override func viewDidLoad() {
        super.viewDidLoad()
    
        // build the header view using autolayout:
        let button = UIButton()
        let label = UILabel()
        button.setTitle("Tap here", for: .normal)
        label.text = "The text in this header will span multiple lines if necessary"
        label.numberOfLines = 0
        let headerView = UIStackView(arrangedSubviews: [button, label])
        headerView.axis = .horizontal
        // assign the header view:
        self.tableView.tableHeaderView = headerView
    
        // continue with other things...
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.tableView.rr_layoutTableHeaderView(width: view.frame.width)
    }
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        self.tableView.rr_layoutTableHeaderView(width: size.width)
    }
    
    0 讨论(0)
  • 2020-12-01 06:14

    My own best answer so far involves setting the tableHeaderView once and forcing a layout pass. This allows a required size to be measured, which I then use to set the frame of the header. And, as is common with tableHeaderViews, I have to again set it a second time to apply the change.

    - (void)viewDidLoad
    {
        [super viewDidLoad];
    
        self.header = [[SCAMessageView alloc] init];
        self.header.titleLabel.text = @"Warning";
        self.header.subtitleLabel.text = @"This is a message with enough text to span multiple lines. This text is set at runtime and might be short or long.";
    
        //set the tableHeaderView so that the required height can be determined
        self.tableView.tableHeaderView = self.header;
        [self.header setNeedsLayout];
        [self.header layoutIfNeeded];
        CGFloat height = [self.header systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
    
        //update the header's frame and set it again
        CGRect headerFrame = self.header.frame;
        headerFrame.size.height = height;
        self.header.frame = headerFrame;
        self.tableView.tableHeaderView = self.header;
    }
    

    For multiline labels, this also relies on the custom view (the message view in this case) setting the preferredMaxLayoutWidth of each:

    - (void)layoutSubviews
    {
        [super layoutSubviews];
    
        self.titleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.titleLabel.frame);
        self.subtitleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.subtitleLabel.frame);
    }
    

    Update January 2015

    Unfortunately this still seems necessary. Here is a swift version of the layout process:

    tableView.tableHeaderView = header
    header.setNeedsLayout()
    header.layoutIfNeeded()
    let height = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
    var frame = header.frame
    frame.size.height = height
    header.frame = frame
    tableView.tableHeaderView = header
    

    I've found it useful to move this into an extension on UITableView:

    extension UITableView {
        //set the tableHeaderView so that the required height can be determined, update the header's frame and set it again
        func setAndLayoutTableHeaderView(header: UIView) {
            self.tableHeaderView = header
            header.setNeedsLayout()
            header.layoutIfNeeded()
            let height = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
            var frame = header.frame
            frame.size.height = height
            header.frame = frame
            self.tableHeaderView = header
        }
    }
    

    Usage:

    let header = SCAMessageView()
    header.titleLabel.text = "Warning"
    header.subtitleLabel.text = "Warning message here."
    tableView.setAndLayoutTableHeaderView(header)
    
    0 讨论(0)
  • 2020-12-01 06:16

    Using Extension in Swift 3.0

    extension UITableView {
    
        func setTableHeaderView(headerView: UIView?) {
            // set the headerView
            tableHeaderView = headerView
    
            // check if the passed view is nil
            guard let headerView = headerView else { return }
    
            // check if the tableHeaderView superview view is nil just to avoid
            // to use the force unwrapping later. In case it fail something really
            // wrong happened
            guard let tableHeaderViewSuperview = tableHeaderView?.superview else {
                assertionFailure("This should not be reached!")
                return
            }
    
            // force updated layout
            headerView.setNeedsLayout()
            headerView.layoutIfNeeded()
    
            // set tableHeaderView width
            tableHeaderViewSuperview.addConstraint(headerView.widthAnchor.constraint(equalTo: tableHeaderViewSuperview.widthAnchor, multiplier: 1.0))
    
            // set tableHeaderView height
            let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
            tableHeaderViewSuperview.addConstraint(headerView.heightAnchor.constraint(equalToConstant: height))
        }
    
        func setTableFooterView(footerView: UIView?) {
            // set the footerView
            tableFooterView = footerView
    
            // check if the passed view is nil
            guard let footerView = footerView else { return }
    
            // check if the tableFooterView superview view is nil just to avoid
            // to use the force unwrapping later. In case it fail something really
            // wrong happened
            guard let tableFooterViewSuperview = tableFooterView?.superview else {
                assertionFailure("This should not be reached!")
                return
            }
    
            // force updated layout
            footerView.setNeedsLayout()
            footerView.layoutIfNeeded()
    
            // set tableFooterView width
            tableFooterViewSuperview.addConstraint(footerView.widthAnchor.constraint(equalTo: tableFooterViewSuperview.widthAnchor, multiplier: 1.0))
    
            // set tableFooterView height
            let height = footerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
            tableFooterViewSuperview.addConstraint(footerView.heightAnchor.constraint(equalToConstant: height))
        }
    }
    
    0 讨论(0)
  • 2020-12-01 06:19

    The following UITableView extension solves all common problems of autolayouting and positioning of the tableHeaderView without frame-use legacy:

    @implementation UITableView (AMHeaderView)
    
    - (void)am_insertHeaderView:(UIView *)headerView
    {
        self.tableHeaderView = headerView;
    
        NSLayoutConstraint *constraint = 
        [NSLayoutConstraint constraintWithItem: headerView
                                     attribute: NSLayoutAttributeWidth
                                     relatedBy: NSLayoutRelationEqual
                                        toItem: headerView.superview
                                     attribute: NSLayoutAttributeWidth
                                    multiplier: 1.0
                                      constant: 0.0];
        [headerView.superview addConstraint:constraint];    
        [headerView layoutIfNeeded];
    
        NSArray *constraints = headerView.constraints;
        [headerView removeConstraints:constraints];
    
        UIView *layoutView = [UIView new];
        layoutView.translatesAutoresizingMaskIntoConstraints = NO;
        [headerView insertSubview:layoutView atIndex:0];
    
        [headerView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"|[view]|" options:0 metrics:nil views:@{@"view": layoutView}]];
        [headerView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view]|" options:0 metrics:nil views:@{@"view": layoutView}]];
    
        [headerView addConstraints:constraints];
    
        self.tableHeaderView = headerView;
        [headerView layoutIfNeeded];
    }
    
    @end
    

    Explanation of the "strange" steps:

    1. At first we tie the headerView width to the tableView width: it helps as under rotations and prevent from deep left shift of X-centered subviews of the headerView.

    2. (the Magic!) We insert fake layoutView in the headerView: At this moment we STRONGLY need to remove all headerView constraints, expand the layoutView to the headerView and then restore initial headerView constraints. It happens that order of constraints has some sense! In the way we get correct headerView height auto calculation and also correct
      X-centralization for all headerView subviews.

    3. Then we only need to re-layout headerView again to obtain correct tableView
      height calculation and headerView positioning above sections without intersecting.

    P.S. It works for iOS8 also. It is impossible to comment out any code string here in common case.

    0 讨论(0)
  • 2020-12-01 06:25

    This should do the trick for a headerView or a footerView for the UITableView using AutoLayout.

    extension UITableView {
    
      var tableHeaderViewWithAutolayout: UIView? {
        set (view) {
          tableHeaderView = view
          if let view = view {
            lowerPriorities(view)
            view.frameSize = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
            tableHeaderView = view
          }
        }
        get {
          return tableHeaderView
        }
      }
    
      var tableFooterViewWithAutolayout: UIView? {
        set (view) {
          tableFooterView = view
          if let view = view {
            lowerPriorities(view)
            view.frameSize = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
            tableFooterView = view
          }
        }
        get {
          return tableFooterView
        }
      }
    
      fileprivate func lowerPriorities(_ view: UIView) {
        for cons in view.constraints {
          if cons.priority.rawValue == 1000 {
            cons.priority = UILayoutPriority(rawValue: 999)
          }
          for v in view.subviews {
            lowerPriorities(v)
          }
        }
      }
    }
    
    0 讨论(0)
  • 2020-12-01 06:27

    Your constraints were just a little off. Take a look at this and let me know if you have any questions. For some reason I had difficulty getting the background of the view to stay red? So I created a filler view that fills the gap created by having a titleLabel and subtitleLabel height that is greater than the height of the imageView

    - (id)initWithFrame:(CGRect)frame
    {
        self = [super initWithFrame:frame];
        if (self)
        {
            self.backgroundColor = [UIColor redColor];
    
            self.imageView = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:@"Exclamation"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
            self.imageView.tintColor = [UIColor whiteColor];
            self.imageView.translatesAutoresizingMaskIntoConstraints = NO;
            self.imageView.backgroundColor = [UIColor redColor];
            [self addSubview:self.imageView];
            [self.imageView mas_makeConstraints:^(MASConstraintMaker *make) {
                make.left.equalTo(self);
                make.width.height.equalTo(@40);
                make.top.equalTo(self).offset(0);
            }];
    
            self.titleLabel = [[UILabel alloc] init];
            self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
            self.titleLabel.font = [UIFont systemFontOfSize:14];
            self.titleLabel.textColor = [UIColor whiteColor];
            self.titleLabel.backgroundColor = [UIColor redColor];
            [self addSubview:self.titleLabel];
            [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
                make.top.equalTo(self).offset(0);
                make.left.equalTo(self.imageView.mas_right).offset(0);
                make.right.equalTo(self).offset(-10);
                make.height.equalTo(@15);
            }];
    
            self.subtitleLabel = [[UILabel alloc] init];
            self.subtitleLabel.translatesAutoresizingMaskIntoConstraints = NO;
            self.subtitleLabel.font = [UIFont systemFontOfSize:13];
            self.subtitleLabel.textColor = [UIColor whiteColor];
            self.subtitleLabel.numberOfLines = 0;
            self.subtitleLabel.backgroundColor = [UIColor redColor];
            [self addSubview:self.subtitleLabel];
            [self.subtitleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
                make.top.equalTo(self.titleLabel.mas_bottom);
                make.left.equalTo(self.imageView.mas_right);
                make.right.equalTo(self).offset(-10);
            }];
    
            UIView *fillerView = [[UIView alloc] init];
            fillerView.backgroundColor = [UIColor redColor];
            [self addSubview:fillerView];
            [fillerView mas_makeConstraints:^(MASConstraintMaker *make) {
                make.top.equalTo(self.imageView.mas_bottom);
                make.bottom.equalTo(self.subtitleLabel.mas_bottom);
                make.left.equalTo(self);
                make.right.equalTo(self.subtitleLabel.mas_left);
            }];
        }
    
        return self;
    }
    
    0 讨论(0)
提交回复
热议问题