複数行のUIView
を含むUILabel
サブクラスがあります。このビューは自動レイアウトを使用します。
このビューをtableHeaderView
のUITableView
として設定したいと思います(notセクションヘッダー)。このヘッダーの高さは、ラベルのテキストに依存します。これは、デバイスの幅に依存します。このようなシナリオの自動レイアウトは優れているはずです。
私は manymanysolutions を見つけて試してみましたが、うまくいきませんでした。私が試したことのいくつか:
preferredMaxLayoutWidth
の実行中に各ラベルにlayoutSubviews
を設定するintrinsicContentSize
の定義tableHeaderView
のフレームを手動で設定します。私が遭遇したさまざまな失敗のいくつか:
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;
何が欠けていますか?
これまでの私自身の最良の答えは、tableHeaderView
を1回設定し、レイアウトパスを強制することです。これにより、必要なサイズを測定できます。これを使用して、ヘッダーのフレームを設定します。そして、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);
}
残念ながら、これはまだ必要なようです。これはSwiftレイアウトプロセスのバージョンです。
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
のbeginUpdates/endUpdates
を介して行われます。
実は、tableView
は、子フレームを更新するときにAutoLayoutを気にしません。 currenttableHeaderView
'ssizeを使用して、最初のセル/セクションヘッダーの場所を決定します。
1)幅制約を追加して、layoutIfNeeded()を呼び出すたびにtableHeaderView
がこの幅を使用するようにします。また、centerXおよびtop制約を追加して、tableView
を基準にして正しく配置します。
2)tableView
にtableHeaderView
の最新のサイズを知らせるには、たとえば、デバイスが回転したときに、viewDidLayoutSubviewsでtableHeaderView
のlayoutIfNeeded()を呼び出します。次に、サイズが変更された場合は、beginUpdates/endUpdatesを呼び出します。
beginUpdates/endUpdatesを1つの関数に含めないことに注意してください。後で呼び出しを延期したい場合があるためです。
次の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の幅に結び付けます。これは回転が不足しているため、headerViewのX中心のサブビューが左にずれるのを防ぐのに役立ちます。
(マジック!)headerViewに偽のlayoutViewを挿入します。現時点では、すべてのheaderView制約を削除し、layoutViewをheaderViewに展開して、最初のheaderView制約を復元する必要があります。 制約の順序に意味があるのは偶然です!正しい方法でheaderViewの高さを自動計算し、また正しい
すべてのheaderViewサブビューのX集中。
次に、正しいtableViewを取得するために、headerViewを再レイアウトするだけです。
交差せずにセクションの上に高さ計算とheaderViewを配置。
P.S。iOS8でも動作します。通常、ここではコード文字列をコメント化することはできません。
ここでの回答のいくつかは、私が必要としているものに非常に近づくのに役立ちました。しかし、デバイスを縦向きと横向きの間で前後に回転させると、システムによって設定された「UIView-Encapsulated-Layout-Width」という制約との競合が発生しました。以下の私の解決策は、主にmarcoarment(彼への信用)によるこのGistに基づいています: https://Gist.github.com/marcoarment/1105553afba6b4900c1 。ソリューションは、UILabelを含むヘッダービューに依存しません。 3つの部分があります。
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
}
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)
}
これは、AutoLayoutを使用して、UITableViewのheaderViewまたはfooterViewのトリックを実行する必要があります。
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)
}
}
}
}
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))
}
}