Установка tableHeaderView высоты динамически

Мое приложение создает UITableViewController, который содержит пользовательский tableHeaderView, который может иметь произвольную высоту. Я боролся со способом динамической установки этого заголовка, так как кажется, что предложенные способы сокращают этот заголовок. Соответствующий код моего UITableViewController:

import UIKit
import SafariServices

class RedditPostViewController: UITableViewController, NetworkCommunication, SubViewLaunchLinkManager {

    //MARK: UITableViewDataSource
    var post: PostData?
    var tree: CommentTree?
    weak var session: Session! = Session.sharedInstance

    override func viewDidLoad() {
        super.viewDidLoad()

        // Get post info from api
        guard let postData = post else { return }

        //Configure comment table
        self.tableView.registerClass(RedditPostCommentTableViewCell.self, forCellReuseIdentifier: "CommentCell")

       let tableHeader = PostView(withPost: postData, inViewController: self)
       let size = tableHeader.systemLayoutSizeFittingSize(UILayoutFittingExpandedSize)
       let height = size.height
       let width = size.width
       tableHeader.frame = CGRectMake(0, 0, width, height)
       self.tableView.tableHeaderView = tableHeader


       session.getRedditPost(postData) { (post) in
           self.post = post?.post
           self.tree = post?.comments
           self.tableView.reloadData()
       }
    }
}

Это приводит к следующей неправильной компоновке:

Если я изменю строку: tableHeader.frame = CGRectMake(0, 0, width, height) в tableHeader.frame = CGRectMake(0, 0, width, 1000) tableHeaderView выложится правильно:

Я не уверен, что я делаю здесь неправильно. Кроме того, пользовательский класс UIView, если это помогает:

import UIKit
import Foundation

protocol SubViewLaunchLinkManager: class {
    func launchLink(sender: UIButton)
}

class PostView: UIView {

    var body: UILabel?
    var post: PostData?
    var domain: UILabel?
    var author: UILabel?
    var selfText: UILabel?
    var numComments: UILabel?

    required init?(coder aDecoder: NSCoder) {
        fatalError("Not implemented yet")
    }

    init(withPost post: PostData, inViewController viewController: SubViewLaunchLinkManager) {
        super.init(frame: CGRectZero)

        self.post = post
        self.backgroundColor = UIColor.lightGrayColor()

        let launchLink = UIButton()
        launchLink.setImage(UIImage(named: "circle-user-7"), forState: .Normal)
        launchLink.addTarget(viewController, action: "launchLink:", forControlEvents: .TouchUpInside)
        self.addSubview(launchLink)

        selfText = UILabel()
        selfText?.backgroundColor = UIColor.whiteColor()
        selfText?.numberOfLines = 0
        selfText?.lineBreakMode = .ByWordWrapping
        selfText!.text = post.selfText
        self.addSubview(selfText!)
        selfText?.sizeToFit()

        //let attributedString = NSAttributedString(string: "Test"/*post.selfTextHtml*/, attributes: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType])
        //selfText.attributedText = attributedString

        body = UILabel()
        body!.text = post.title
        body!.numberOfLines = 0
        body!.lineBreakMode = .ByWordWrapping
        body!.textAlignment = .Justified
        self.addSubview(body!)

        domain = UILabel()
        domain!.text = post.domain
        self.addSubview(domain!)

        author = UILabel()
        author!.text = post.author
        self.addSubview(author!)

        numComments = UILabel()
        numComments!.text = "\(post.numComments)"
        self.addSubview(numComments!)

        body!.translatesAutoresizingMaskIntoConstraints = false
        domain!.translatesAutoresizingMaskIntoConstraints = false
        author!.translatesAutoresizingMaskIntoConstraints = false
        selfText!.translatesAutoresizingMaskIntoConstraints = false
        launchLink.translatesAutoresizingMaskIntoConstraints = false
        numComments!.translatesAutoresizingMaskIntoConstraints = false

        let views: [String: UIView] = ["body": body!, "domain": domain!, "author": author!, "numComments": numComments!, "launchLink": launchLink, "selfText": selfText!]
        //let selfTextSize = selfText?.sizeThatFits((selfText?.frame.size)!)
        //print(selfTextSize)
        //let metrics = ["selfTextHeight": selfTextSize!.height]

                   self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[body]-[selfText]-[domain]-|", options: [], metrics: nil, views: views))
       self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[body]-[selfText]-[author]-|", options: [], metrics: nil, views: views))
    self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[body]-[selfText]-[numComments]-|", options: [], metrics: nil, views: views))
    self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[launchLink]-[numComments]-|", options: [], metrics: nil, views: views))
    self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[body][launchLink]|", options: [], metrics: nil, views: views))
    self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[selfText][launchLink]|", options: [], metrics: nil, views: views))
    self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[domain][author][numComments][launchLink]|", options: [], metrics: nil, views: views))
}

override func layoutSubviews() {
    super.layoutSubviews()
    body?.preferredMaxLayoutWidth = body!.bounds.width
}
}

12 ответов

Решение

Скопировано из этого поста. (Убедитесь, что вы видите это, если вы ищете более подробную информацию)

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    if let headerView = tableView.tableHeaderView {

        let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
        var headerFrame = headerView.frame

        //Comparison necessary to avoid infinite loop
        if height != headerFrame.size.height {
            headerFrame.size.height = height
            headerView.frame = headerFrame
            tableView.tableHeaderView = headerView
        }
    }
}

Определение размера кадра заголовка с помощью

header.systemLayoutSizeFitting(UILayoutFittingCompressedSize)

как предложено в ответах выше, у меня не сработало, когда мое представление заголовка состояло из одной многострочной метки. Когда режим разрыва строки на ярлыке установлен на перенос, текст просто обрезается:

введите описание изображения здесь

Вместо этого для меня работало использование ширины табличного представления и высоты 0 в качестве целевого размера:

header.systemLayoutSizeFitting(CGSize(width: tableView.bounds.width, height: 0))

введите описание изображения здесь

Собираем все вместе (я предпочитаю использовать расширение):

extension UITableView {
    func updateHeaderViewHeight() {
        if let header = self.tableHeaderView {
            let newSize = header.systemLayoutSizeFitting(CGSize(width: self.bounds.width, height: 0))
            header.frame.size.height = newSize.height
        }
    }
}

И назовите это так:

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    tableView.updateHeaderViewHeight()
}

Более сжатая версия ответа OP, с преимуществом, позволяющим разметку происходить естественным образом (обратите внимание, что это решение использует представлениеWill LayoutSubviews):

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()

    if let header = tableView.tableHeaderView {
        let newSize = header.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
        header.frame.size.height = newSize.height
    }
}

Как видно из вышесказанного, у Apple нет оправдания не поддерживать автоматическую высоту верхнего / нижнего колонтитула табличного представления, как это теперь происходит с ячейками. Разочарование.

Спасибо TravMatth за оригинальный ответ.

Если у вас все еще есть проблемы с компоновкой с приведенным выше примером кода, есть небольшая вероятность, что вы отключили translatesAutoresizingMaskIntoConstraints в настраиваемом представлении заголовка. В этом случае вам нужно установить translatesAutoresizingMaskIntoConstraints вернуться к true после того, как вы установите рамку заголовка.

Вот пример кода, который я использую, и работает правильно на iOS 11.

public override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    guard let headerView = tableView.tableHeaderView else { return }

    let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
    var headerFrame = headerView.frame

    if height != headerFrame.size.height {
        headerFrame.size.height = height
        headerView.frame = headerFrame
        tableView.tableHeaderView = headerView

        if #available(iOS 9.0, *) {
            tableView.layoutIfNeeded()
        }
    }

    headerView.translatesAutoresizingMaskIntoConstraints = true
}

На основе TravMatth @ TravMatth и @ NSExceptional:

Для заголовка Dynamic TableView с несколькими строками текста (независимо от того, есть они или нет)

Мое решение:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    if let footView = tableView.tableFooterView {
        let newSize = footView.systemLayoutSizeFitting(CGSize(width: self.view.bounds.width, height: 0))
        if newSize.height != footView.frame.size.height {
            footView.frame.size.height = newSize.height
            tableView.tableFooterView = footView
        }
    }
}

tableView.tableFooterView = footView, чтобы убедиться, что ваш заголовок или нижний колонтитул tableview обновлен. А такжеif newSize.height != footView.frame.size.height помогает не называть этот метод много раз

Я использую принятый ответ в течение долгого времени, и он всегда работал у меня, до сегодняшнего дня, когда я использовал метку из нескольких строк в сложном представлении заголовка таблицы, я столкнулся с той же проблемой, что и @frank61003:

он создает пустую область с меткой из нескольких строк.

В моем случае вокруг лейбла были большие вертикальные поля. Если текст метки занимает всего 1 строку, то все в порядке. Эта проблема возникает только в том случае, если на этикетке есть несколько строк текста.

Я не знаю точной причины, вызывающей это, но я некоторое время копал и нашел обходной путь для решения проблемы, поэтому я хочу оставить здесь ссылку на случай, если кто-то столкнется с той же проблемой.

Необязательный первый шаг: убедитесь, что ваш многострочный ярлык имеет самый низкий приоритет обработки содержимого в представлении заголовка таблицы, чтобы он мог автоматически увеличиваться, чтобы соответствовать его тексту.

Затем добавьте этот метод вычисления высоты метки в свой контроллер представления.

      private func calculateHeightForString(_ string: String) -> CGFloat {
        let yourLabelWidth = UIScreen.main.bounds.width - 20
        let constraintRect = CGSize(width: yourLabelWidth, height: CGFloat.greatestFiniteMagnitude)
        let rect = string.boundingRect(with: constraintRect,
                                       options: .usesLineFragmentOrigin,
                                       // use your label's font
                                       attributes: [.font: descriptionLabel.font!],
                                       context: nil)
        
        return rect.height + 6  // give a little extra arbitrary space (6), remove it if you don't need
    }

И используйте метод выше, чтобы настроить метку из нескольких строк в viewDidLoad

      let description = "Long long long ... text"
descriptionLabel.text = description
// manually calculate multiple lines label height and add a constraint to avoid extra space bug
descriptionLabel.heightAnchor.constraint(equalToConstant: calculateHeightForString(description)).isActive = true

Это решило мою проблему, надеюсь, это сработает и для вас.

Просто реализация этих двух методов делегата UITableView работала для меня:

-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForHeaderInSection:(NSInteger)section
{
    return 100;
}

-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
    return UITableViewAutomaticDimension;
}

Я создал подкласс, чтобы хранить макет внутри самого представления.

      /// The view gets automatically resized based on it's constraints to fit into parent `UITableView`. Assign to `tableHeaderView`, do not set constraints between `DynamicHeaderView` and the `UITableView`.
class DynamicTableHeaderView: UIView {
    override func layoutSubviews() {
        if let tableView = superview as? UITableView {
            let targetSize = CGSize(width: tableView.contentSize.width, height: UIView.layoutFittingCompressedSize.height)
            let fittingSize = self.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
            if frame.size != fittingSize {
                self.frame.size = fittingSize
                tableView.tableHeaderView = self // Forces the tableView to redraw
            }
        }
        super.layoutSubviews()
    }
}

Вот так «просто» в 2023 году.

Обратите внимание на критический совет от @MrKew.

Перетащите простой UIView в положение заголовка таблицы. Не создайте подкласс и не ссылайтесь на него.

Внутри этого представления добавьте все, что хотите.

Очевидно, соедините вертикальные ограничения сверху вниз. (Если вы не совсем знакомы с этим процессом, обратитесь к базовым руководствам.)

Обратите внимание: вам не нужно и не следует уменьшать/увеличивать какой-либо из трех верхних/хвостовых приоритетов. Обратите внимание, что вертикальное представление стека здесь распространено и работает отлично.

Волшебный код от @MrKew:

      override func viewDidLayoutSubviews() {
    
    super.viewDidLayoutSubviews()
    
    if let thv = table.tableHeaderView {
        
        var f = thv.frame
        
        let targetSize = CGSize(
            width: table.contentSize.width,
            height: UIView.layoutFittingCompressedSize.height)
        let fittingSize = thv.systemLayoutSizeFitting(
            targetSize,
            withHorizontalFittingPriority: .required,
            verticalFittingPriority: .fittingSizeLevel)
        f.size = fittingSize
        
        thv.frame = f
        table.tableHeaderView = thv
    }
}

Это конец.

Другого способа сделать это нет.

Обратите внимание, что ответ @MrKew дает ключевой элемент, необходимый в последних версиях UIKit.

(Обратите внимание, что вам действительно нужно указать ширину (т. е. просто ширину таблицы), а не одну иззначения, как объясняет @MrKew в комментариях.)

Если все ограничения добавлены, это будет работать:

      headerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        if let headerView = self.tableView.tableHeaderView {
            let headerViewFrame = headerView.frame
            let height = headerView.systemLayoutSizeFitting(headerViewFrame.size, withHorizontalFittingPriority: UILayoutPriority.defaultHigh, verticalFittingPriority: UILayoutPriority.defaultLow).height
            var headerFrame = headerView.frame
            if height != headerFrame.size.height {
                headerFrame.size.height = height
                headerView.frame = headerFrame
                self.tableView.tableHeaderView = headerView
            }
        }
    }

Проблема с расчетом размера этикетки при использовании горизонтальной или вертикальной установки

**My Working Solution is:
Add this function in viewcontroller**
public override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        guard let headerView = myTableView.tableHeaderView else { return }
        let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
        var headerFrame = headerView.frame

        if height != headerFrame.size.height {
            headerFrame.size.height = height
            headerView.frame = headerFrame
            myTableView.tableHeaderView = headerView

            if #available(iOS 9.0, *) {
                myTableView.layoutIfNeeded()
            }
        }

        headerView.translatesAutoresizingMaskIntoConstraints = true
} 


 **Add one line in header's view class.**
override func layoutSubviews() {
        super.layoutSubviews()
        bookingLabel.preferredMaxLayoutWidth = bookingLabel.bounds.width

    }
Другие вопросы по тегам