Использование автозапуска в таблицеHeaderView
У меня есть подкласс UIView
, который содержит многострочный UILabel
. В этом представлении используется автозапуск.
Я хотел бы установить это представление как tableHeaderView
для UITableView
(а не для заголовка раздела). Высота этого заголовка будет зависеть от текста метки, которая, в свою очередь, зависит от ширины устройства. Тип автозапуска сценария должен быть отличным.
Я нашел и попытался много , чтобы получить эту работу, но безрезультатно. Некоторые из вещей, которые я пробовал:
- установка
preferredMaxLayoutWidth
на каждой метке во время layoutSubviews
- определение
intrinsicContentSize
- попытка определить требуемый размер для представления и установить кадр
tableHeaderView
вручную.
- добавление ограничения ширины в представление, когда заголовок установлен
- куча других вещей
Некоторые из различных сбоев, с которыми я столкнулся:
-
Метка
- выходит за пределы ширины представления, не обертывает
- высота кадра 0
- сбой приложений с исключением
Auto Layout still required after executing -layoutSubviews
Решение (или решения, если необходимо) должно работать как для iOS 7, так и для iOS 8. Обратите внимание, что все это делается программно. Я установил небольшой образец проекта, если вы хотите взломать его, чтобы увидеть проблему. У меня есть reset мои усилия по следующей начальной точке:
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;
Что мне не хватает?
Ответы
Ответ 1
Мой собственный лучший ответ до сих пор включает установку 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 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 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)
Ответ 2
Для тех, кто все еще ищет решение, это для 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 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
beginUpdates/endUpdates
.
Дело в том, что tableView
не заботится об AutoLayout, когда он обновляет детские кадры. Он использует текущий размер tableHeaderView
, чтобы определить, где должен быть первый заголовок ячейки/секции.
1) Добавьте ограничение ширины, чтобы tableHeaderView
использовала эту ширину всякий раз, когда мы вызываем layoutIfNeeded(). Также добавьте centerX и верхние ограничения, чтобы правильно позиционировать его относительно tableView
.
2) Чтобы сообщить tableView
о последнем размере tableHeaderView
, например, когда устройство повернуто, в viewDidLayoutSubviews мы можем вызвать layoutIfNeeded() на tableHeaderView
. Затем, если размер изменен, вызовите beginUpdates/endUpdates.
Обратите внимание, что я не включаю beginUpdates/endUpdates в одну функцию, так как мы можем отложить вызов позже.
Проверьте пример проекта
Ответ 3
Следующее расширение 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.
-
(Magic!) Мы вставляем поддельный layoutView в headerView:
В настоящий момент нам необходимо удалить все ограничения headerView,
развернуть layoutView в headerView, а затем восстановить начальный заголовок
ограничения. Бывает, что порядок ограничений имеет какой-то смысл!
В способе мы получаем правильный автоматический подсчет высоты заголовка, а также правильный
X-централизация для всех заголовков headerView.
-
Затем нам нужно снова перерисовать headerView, чтобы получить правильный tableView
расчет высоты и headerView позиционирование над разделами без
пересекающихся.
P.S. Он также работает для iOS8. В общем случае невозможно прокомментировать любую кодовую строку.
Ответ 4
Использование расширения в 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))
}
}
Ответ 5
Некоторые из ответов помогли мне приблизиться к тому, что мне было нужно. Но я столкнулся с конфликтами с ограничением "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)
}
Ответ 6
Я добавлю свои 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). То, что 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
, файл nib).
Ответ 7
Ваши ограничения были совсем немного. Взгляните на это и сообщите мне, если у вас есть какие-либо вопросы. По какой-то причине мне было трудно получить фон обзора, чтобы он остался красным? Таким образом, я создал представление наполнителя, которое заполняет промежуток, созданный с высотой 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;
}