Использование autolayout в tableHeaderView
У меня есть UIView
подкласс, который содержит многострочный UILabel
, Это представление использует autolayout.
Я хотел бы установить эту точку зрения как tableHeaderView
из UITableView
(не заголовок раздела). Высота этого заголовка будет зависеть от текста метки, который, в свою очередь, зависит от ширины устройства. Разновидность сценария autolayout должна быть хороша в.
Я нашел и попытался найти множество решений, чтобы это работало, но безрезультатно. Некоторые из вещей, которые я пробовал:
- установка
preferredMaxLayoutWidth
на каждом ярлыке во времяlayoutSubviews
- определяя
intrinsicContentSize
- пытаясь выяснить необходимый размер для представления и установки
tableHeaderView
кадр вручную. - добавление ограничения ширины к представлению, когда установлен заголовок
- куча других вещей
Некоторые из различных сбоев, с которыми я столкнулся:
- метка выходит за пределы ширины вида, не переносится
- высота рамки 0
- приложение вылетает за исключением
Auto Layout still required after executing -layoutSubviews
Решение (или решения, если необходимо) должно работать как для iOS 7, так и для iOS 8. Обратите внимание, что все это делается программно. Я создал небольшой пример проекта на случай, если вы захотите взломать его, чтобы увидеть проблему. Я сбросил свои усилия до следующей начальной точки:
SCAMessageView *header = [[SCAMessageView alloc] init];
header.titleLabel.text = @"Warning";
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.";
self.tableView.tableHeaderView = header;
Что мне не хватает?
9 ответов
Мой лучший ответ на данный момент включает в себя настройку tableHeaderView
один раз и заставляя проход макета. Это позволяет измерить требуемый размер, который я затем использую для установки рамки заголовка. И, как обычно с tableHeaderView
s, я должен снова установить его во второй раз, чтобы применить изменения.
- (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;
}
Для многострочных меток это также зависит от пользовательского представления (в данном случае представления сообщения), устанавливающего preferredMaxLayoutWidth
каждого:
- (void)layoutSubviews
{
[super layoutSubviews];
self.titleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.titleLabel.frame);
self.subtitleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.subtitleLabel.frame);
}
Обновление январь 2015
К сожалению, это все еще кажется необходимым. Вот быстрая версия процесса верстки:
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
Я нашел полезным перенести это в расширение 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
}
}
Использование:
let header = SCAMessageView()
header.titleLabel.text = "Warning"
header.subtitleLabel.text = "Warning message here."
tableView.setAndLayoutTableHeaderView(header)
Для тех, кто все еще ищет решение, это для Swift 3 и iOS 9+. Вот один, использующий только AutoLayout. Также корректно обновляется при повороте устройства.
extension UITableView {
// 1.
func setTableHeaderView(headerView: UIView) {
headerView.translatesAutoresizingMaskIntoConstraints = false
self.tableHeaderView = headerView
// ** Must setup AutoLayout after set tableHeaderView.
headerView.widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true
headerView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
headerView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
}
// 2.
func shouldUpdateHeaderViewFrame() -> Bool {
guard let headerView = self.tableHeaderView else { return false }
let oldSize = headerView.bounds.size
// Update the size
headerView.layoutIfNeeded()
let newSize = headerView.bounds.size
return oldSize != newSize
}
}
Использовать:
override func viewDidLoad() {
...
// 1.
self.tableView.setTableHeaderView(headerView: customView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 2. Reflect the latest size in tableHeaderView
if self.tableView.shouldUpdateHeaderViewFrame() {
// **This is where table view's content (tableHeaderView, section headers, cells)
// frames are updated to account for the new table header size.
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
}
Суть в том, что вы должны позволить tableView
управлять кадром tableHeaderView
так же, как ячейки табличного представления. Это делается через tableView
"s beginUpdates/endUpdates
,
Дело в том, что tableView
не заботится об AutoLayout, когда он обновляет дочерние кадры. Использует текущий tableHeaderView
Размер, чтобы определить, где должен быть первый заголовок ячейки / раздела.
1) Добавьте ограничение ширины, чтобы tableHeaderView
использует эту ширину всякий раз, когда мы вызываем layoutIfNeeded(). Также добавьте centerX и верхние ограничения, чтобы правильно расположить его относительно tableView
,
2) чтобы позволить tableView
знает о последнем размере tableHeaderView
Например, когда устройство поворачивается, в viewDidLayoutSubviews мы можем вызвать layoutIfNeeded () на tableHeaderView
, Затем, если размер изменяется, вызовите beginUpdates / endUpdates.
Обратите внимание, что я не включаю beginUpdates / endUpdates в одну функцию, так как мы можем отложить вызов на более поздний.
Следующие UITableView
расширение решает все распространенные проблемы автопрокладки и позиционирования tableHeaderView
без использования фреймов:
@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
Объяснение "странных" шагов:
Сначала мы привязываем ширину headerView к ширине tableView: это помогает при поворотах и предотвращает глубокий сдвиг влево X-центрированных подпредставлений headerView.
(Волшебство!) Мы вставляем поддельный layoutView в headerView: в этот момент нам СТРОГО нужно удалить все ограничения headerView, расширить layoutView до headerView и затем восстановить начальные ограничения headerView. Бывает, что порядок ограничений имеет какой-то смысл! В способе мы получаем правильный автоматический расчет высоты headerView, а также правильный
X-централизация для всех подвидов headerView.Тогда нам нужно только изменить макет headerView снова, чтобы получить правильный tableView
вычисление высоты и расположение заголовка в виде над секциями без пересечения.
PS Работает и для iOS8. Здесь невозможно закомментировать любую строку кода в общем случае.
Некоторые ответы здесь помогли мне приблизиться к тому, что мне было нужно. Но я столкнулся с конфликтами с ограничением "UIView-Encapsulated-Layout-Width", которое устанавливается системой, при повороте устройства назад и вперед между портретным и альбомным режимом. Мое решение, приведенное ниже, в значительной степени основано на этой сущности по маркорументу (кредит ему): https://gist.github.com/marcoarment/1105553afba6b4900c10. Решение не опирается на представление заголовка, содержащее UILabel. Есть 3 части:
- Функция, определенная в расширении UITableView.
- Вызовите функцию из viewWillAppear() контроллера представления.
- Вызовите функцию из viewWillTransition() контроллера представления для обработки вращения устройства.
Расширение UITableView
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
}
Используйте в 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)
}
Это должно сделать трюк для headerView или footerView для UITableView с использованием 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)
}
}
}
}
Это действительно так просто:
Преамбула. Быть в курсе, что
Более того, в UIKit парадигма, обработка и обработка цикла представления украшений «таблицы» и «раздела» совершенно, совершенно различны, и, следовательно, вся проблема совершенно запутанна. Добавьте к этому совершенно странную проблему, описанную здесь, в пункте «2» (которая совершенно не похожа на любую другую обработку ограничений во всем UIKit), и можно увидеть, как возникла массовая путаница в отношении нижних колонтитулов таблиц.
Самый главный секрет заключается в следующем:
1. Ваш xib ДОЛЖЕН ИМЕТЬ <1000 вертикальный приоритет.
В вашем файле xib ваша вертикальная цепочка ограничений ДОЛЖНА быть меньше 1000. Вот и все, что нужно. Загрузите файл xib обычным способом.
class DemoFooter: UIIView {
class func build() -> UIView {
return UINib(nibName: "DemoFooter", bundle: nil)
.instantiate(withOwner: nil, options: nil)[0] as! UIView
}
}
Это ошеломляющий «секрет» работы верхних и нижних колонтитулов таблиц.
2. Из-за проблемы Apple вы ДОЛЖНЫ «сбросить» tableFooterView. Это просто так. Совершенно бессмысленно пытаться обойти эту проблему Apple.
Вы ожидаете, что код будет выглядеть так
tableFooterView.frame = .. the size
Простой факт: вы не можете этого сделать, вы должны сделать ЭТО:
temp = tableFooterView
temp.frame = .. the size
tableFooterView = temp
Совершенно бессмысленно обсуждать, является ли это ошибкой Apple, программной ошибкой, глупостью, странным поведением, ошибкой или чем-то еще. Дело в том, что вам нужно «сбросить» tableFooterView.
(В вашем коде просто попробуйте по-другому, и вы увидите, что сначала нижний колонтитул просто «плавает в середине экрана». Абсолютно не имеет значения, где вы попробуете это сделать в цикле просмотра, потратите время. Пробуйте, как хотите! Это просто типичный провал UIKit.)
3. Обратите внимание на неправильный пример кода цикла просмотра, который можно найти в Интернете.
override func viewDidLoad() {
super.viewDidLoad()
tableView.tableFooterView = DemoFooter.build()
}
override func viewWillLayoutSubviews() {
if let f = tableView.tableFooterView {
f.frame.size = f.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
tableView.tableFooterView = f
print("Recall, you MUST HAVE <1000 priority vert in your xib.")
}
super.viewWillLayoutSubviews()
}
Ты делаешь это в
Вы НЕ «проверяете, изменилась ли высота», как это видно во многих примерах в сети. Очевидно, что это невероятно бессмысленная/очень плохая практика по причинам, на которые не стоит вдаваться здесь.
Для ясности: если это тривиальное статическое представление, вы можете просто сделать это, чтобы не вводить «viewDidLoad»:
override func viewWillLayoutSubviews() {
let f = QuickFoot.build()
f.frame.size = f.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
tableView.tableFooterView = f
super.viewWillLayoutSubviews()
}
4. Перепроверьте точку (1).
Или просто ничего не будет работать (и вы будете получать спам с ошибками в консоли).
Использование расширения в 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))
}
}
Твои ограничения были совсем немного. Посмотрите на это и дайте мне знать, если у вас есть какие-либо вопросы. По какой-то причине у меня были трудности с получением фона вида, чтобы остаться красным? Поэтому я создал представление заполнителя, которое заполняет пробел, созданный titleLabel
а также subtitleLabel
высота, которая больше, чем высота 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;
}
Я добавлю свои 2 цента, так как этот вопрос высоко проиндексирован в Google. Я думаю, что вы должны использовать
self.tableView.sectionHeaderHeight = UITableViewAutomaticDimension
self.tableView.estimatedSectionHeaderHeight = 200 //a rough estimate, doesn't need to be accurate
в вашем ViewDidLoad
, Также для загрузки кастома UIView
к Header
вы действительно должны использовать viewForHeaderInSection
метод делегата. Вы можете иметь кастом Nib
файл для вашего заголовка (UIView
СИБ). Тот Nib
должен иметь класс контроллера, который подклассы UITableViewHeaderFooterView
лайк-
class YourCustomHeader: UITableViewHeaderFooterView {
//@IBOutlets, delegation and other methods as per your needs
}
Убедитесь, что имя вашего Nib-файла совпадает с именем класса, чтобы не запутаться и им было легче управлять. лайк YourCustomHeader.xib
а также YourCustomHeader.swift
(содержащий class YourCustomHeader
). Затем просто назначьте YourCustomHeader
в ваш Nib-файл с помощью инспектора идентификации в конструкторе интерфейсов.
Затем зарегистрируйте Nib
файл в качестве заголовка в главном контроллере вида viewDidLoad
лайк-
tableView.register(UINib(nibName: "YourCustomHeader", bundle: nil), forHeaderFooterViewReuseIdentifier: "YourCustomHeader")
И тогда в вашем heightForHeaderInSection
просто вернись UITableViewAutomaticDimension
, Вот так должны выглядеть делегаты:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "YourCustomHeader") as! YourCustomHeader
return headerView
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return UITableViewAutomaticDimension
}
Это гораздо более простой и подходящий способ обойтись без "хакерских" способов, предложенных в принятом ответе, поскольку множественные принудительные макеты могут повлиять на производительность вашего приложения, особенно если у вас есть несколько пользовательских заголовков в табличном представлении. Как только вы сделаете вышеуказанный метод, как я предлагаю, вы заметите Header
(и или Footer
) представление расширяется и сокращается волшебным образом в зависимости от размера содержимого пользовательского представления (при условии, что вы используете AutoLayout в настраиваемом представлении, т.е. YourCustomHeader
Ниб файл).