Jak ustawić wysokość tableHeaderView (UITableView) z autolayout?

86

Rozbijam się tym głową o ścianę przez ostatnie 3 lub 4 godziny i nie mogę tego rozgryźć. Mam UIViewController z pełnoekranowym UITableView wewnątrz niego (na ekranie jest kilka innych rzeczy, dlatego nie mogę użyć UITableViewController) i chcę, aby moja tableHeaderView zmieniła rozmiar za pomocą autolayout. Nie trzeba dodawać, że nie współpracuje.

Zobacz zrzut ekranu poniżej.

wprowadź opis obrazu tutaj

Ponieważ OverviewLabel (np. Tekst „List Overview information here”) ma dynamiczną zawartość, używam autolayout, aby zmienić jego rozmiar i jego superview. Mam wszystko ładnie zmieniające rozmiar, z wyjątkiem tableHeaderView, który znajduje się tuż poniżej widoku tabeli Paralax w hierarchii.

Jedynym sposobem, w jaki udało mi się zmienić rozmiar tego widoku nagłówka, jest programowe użycie następującego kodu:

CGRect headerFrame = self.headerView.frame;
headerFrame.size.height = headerFrameHeight;
self.headerView.frame = headerFrame;
[self.listTableView setTableHeaderView:self.headerView];

W tym przypadku headerFrameHeight jest ręcznym obliczeniem wysokości tableViewHeader w następujący sposób (innerHeaderView to biały obszar lub drugi „View”, headerView to tableHeaderView) :

CGFloat startingY = self.innerHeaderView.frame.origin.y + self.overviewLabel.frame.origin.y;
CGRect overviewSize = [self.overviewLabel.text
                       boundingRectWithSize:CGSizeMake(290.f, CGFLOAT_MAX)
                       options:NSStringDrawingUsesLineFragmentOrigin
                       attributes:@{NSFontAttributeName: self.overviewLabel.font}
                       context:nil];
CGFloat overviewHeight = overviewSize.size.height;
CGFloat overviewPadding = ([self.overviewLabel.text length] > 0) ? 10 : 0; // If there's no overviewText, eliminate the padding in the overall height.
CGFloat headerFrameHeight = ceilf(startingY + overviewHeight + overviewPadding + 21.f + 10.f);

Ręczne obliczanie działa, ale jest niezgrabne i podatne na błędy, jeśli coś się zmieni w przyszłości. Chcę mieć możliwość automatycznej zmiany rozmiaru tableHeaderView na podstawie podanych ograniczeń, tak jak w każdym innym miejscu. Ale na całe życie nie mogę tego rozgryźć.

Jest kilka postów na SO na ten temat, ale żaden nie jest jasny i ostatecznie bardziej mnie zdezorientował. Oto kilka:

Naprawdę nie ma sensu zmieniać właściwości translatesAutoresizingMaskIntoConstraints na NO, ponieważ powoduje to po prostu błędy i nie ma sensu koncepcyjnego.

Każda pomoc byłaby naprawdę doceniona!

EDYCJA 1: Dzięki sugestii TomSwift udało mi się to rozgryźć. Zamiast ręcznie obliczać wysokość przeglądu, mogę go obliczyć dla mnie w następujący sposób, a następnie ponownie ustawić tableHeaderView jak poprzednio.

[self.headerView setNeedsLayout];
[self.headerView layoutIfNeeded];
CGFloat height = [self.innerHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height + self.innerHeaderView.frame.origin.y; // adding the origin because innerHeaderView starts partway down headerView.

CGRect headerFrame = self.headerView.frame;
headerFrame.size.height = height;
self.headerView.frame = headerFrame;
[self.listTableView setTableHeaderView:self.headerView];

Edycja 2: Jak zauważyli inni, rozwiązanie opublikowane w Edit 1 nie działa w viewDidLoad. Wydaje się jednak, że działa w viewWillLayoutSubviews. Przykładowy kod poniżej:

// Note 1: The variable names below don't match the variables above - this is intended to be a simplified "final" answer.
// Note 2: _headerView was previously assigned to tableViewHeader (in loadView in my case since I now do everything programatically).
// Note 3: autoLayout code can be setup programatically in updateViewConstraints.
- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];

    [_headerWrapper setNeedsLayout];
    [_headerWrapper layoutIfNeeded];
    CGFloat height = [_headerWrapper systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

    CGRect headerFrame = _headerWrapper.frame;
    headerFrame.size.height = height;
    _headerWrapper.frame = headerFrame;
    _tableView.tableHeaderView = _headerWrapper;
}
Andrew Cross
źródło
2
setTableHeaderViewnie działa na Xcode6. Problem polega na tym, że na komórki nakłada się tableHeaderView. Jednak działa na Xcode5
DawnSong
@DawnSong znasz rozwiązanie dla Xcode6, abym mógł zaktualizować odpowiedź?
Andrew Cross,
1
Po wielu próbach używam UITableViewCell zamiast tableHeaderView i działa.
DawnSong
Ja też rozbiłem głowę!
Akshit Zaveri
Prawdziwe kompletne rozwiązanie autoukładu jest tutaj
malex

Odpowiedzi:

35

Musisz użyć tej UIView systemLayoutSizeFittingSize:metody, aby uzyskać minimalny rozmiar obwiedni widoku nagłówka.

Dalszą dyskusję na temat korzystania z tego interfejsu API przedstawię w tym pytaniu / odpowiedzi:

Jak zmienić rozmiar Superview, aby dopasować wszystkie podglądy do autoukładu?

TomSwift
źródło
Wraca z wysokością = 0, chociaż nie jestem w 100% pewien, że skonfigurowałem go poprawnie, ponieważ nie jest to UITableViewCell, więc nie ma contentView, który mógłby wywołać "systemLayoutSizeFittingSize". Co próbowałem: [self.headerView setNeedsLayout]; [self.headerView layoutIfNeeded]; CGFloat height = [self.headerView systemLayoutSizeFittingSize: UILayoutFittingCompressedSize] .height; CGRect headerFrame = self.headerView.frame; headerFrame.size.height = height; self.headerView.frame = headerFrame; [self.listTableView setTableHeaderView: self.headerView]; Potwierdziłem również, że PreferowanyMax ... jest ważny.
Andrew Cross,
Aha! Rozgryzłem to, musiałem zrobić [self.innerHeaderView systemLayoutSizeFittingSize ...] zamiast self.headerView, ponieważ to właśnie ma rzeczywistą zawartość dynamiczną. Wielkie dzięki!
Andrew Cross,
Działa, ale w mojej aplikacji występuje niewielka rozbieżność w wysokości. Prawdopodobnie dlatego, że moje etykiety używają przypisanego tekstu i typu dynamicznego.
biografia
23

Naprawdę walczyłem z tym i podłączenie konfiguracji do widoku DidLoad nie działało dla mnie, ponieważ ramka nie jest ustawiona w viewDidLoad, skończyło się też na tonach niechlujnych ostrzeżeń, w których zamknięta wysokość automatycznego układu nagłówka została zmniejszona do 0 Zauważyłem problem tylko na iPadzie podczas prezentowania tableView w prezentacji formularza.

Rozwiązaniem dla mnie było ustawienie tableViewHeader w viewWillLayoutSubviews, a nie w viewDidLoad.

func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        if tableView.tableViewHeaderView == nil {
            let header: MyHeaderView = MyHeaderView.createHeaderView()
            header.setNeedsUpdateConstraints()
            header.updateConstraintsIfNeeded()
            header.frame = CGRectMake(0, 0, CGRectGetWidth(tableView.bounds), CGFloat.max)
            var newFrame = header.frame
            header.setNeedsLayout()
            header.layoutIfNeeded()
            let newSize = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
            newFrame.size.height = newSize.height
            header.frame = newFrame
            self.tableView.tableHeaderView = header
        }
    }
Daniel Galasko
źródło
aby uzyskać zaktualizowane rozwiązanie z minimalnym kodem, wypróbuj to: stackoverflow.com/a/63594053/3933302
Sourabh Sharma
23

Znalazłem elegancki sposób wykorzystania automatycznego układu do zmiany rozmiaru nagłówków tabeli, z animacją i bez niej.

Po prostu dodaj to do kontrolera widoku.

func sizeHeaderToFit(tableView: UITableView) {
    if let headerView = tableView.tableHeaderView {
        let height = headerView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
        var frame = headerView.frame
        frame.size.height = height
        headerView.frame = frame
        tableView.tableHeaderView = headerView
        headerView.setNeedsLayout()
        headerView.layoutIfNeeded()
    }
}

Aby zmienić rozmiar zgodnie z dynamicznie zmieniającą się etykietą:

@IBAction func addMoreText(sender: AnyObject) {
    self.label.text = self.label.text! + "\nThis header can dynamically resize according to its contents."
}

override func viewDidLayoutSubviews() {
    // viewDidLayoutSubviews is called when labels change.
    super.viewDidLayoutSubviews()
    sizeHeaderToFit(tableView)
}

Aby animować zmianę rozmiaru zgodnie ze zmianami w wiązaniu:

@IBOutlet weak var makeThisTallerHeight: NSLayoutConstraint!

@IBAction func makeThisTaller(sender: AnyObject) {

    UIView.animateWithDuration(0.3) {
        self.tableView.beginUpdates()
        self.makeThisTallerHeight.constant += 20
        self.sizeHeaderToFit(self.tableView)
        self.tableView.endUpdates()
    }
}

Zobacz projekt AutoResizingHeader, aby zobaczyć, jak to działa. https://github.com/p-sun/Swift2-iOS9-UI

AutoResizingHeader

p-sun
źródło
Hej p-sun. Dzięki za wspaniały przykład. Wydaje się, że mam problem z tableViewHeader. Próbowałem mieć takie same ograniczenia jak w twoim przykładzie, ale nie działały. Jak dokładnie powinienem dodać ograniczenia do etykiety, którą chcę zwiększyć wysokość? Dzięki za wszelkie sugestie
Bonnke
1
Upewnij się, że zaczepiłeś etykietę, do której chcesz podnieść @IBOutlet makeThisTalleri @IBAction fun makeThisTallerjak w przykładzie. Ponadto ogranicz wszystkie boki etykiety do elementu tableViewHeader (np. Góra, dół, lewa i prawa).
p-niedz
Wielkie dzięki! Ostatecznie rozwiązano, dodając tę ​​linię: lblFeedDescription.preferredMaxLayoutWidth = lblFeedDescription.bounds.widthgdzie etykieta to ta, którą chcę zwiększyć rozmiar. Dzięki !
Bonnke,
Dzięki! Jeśli robisz to wewnątrz View zamiast ViewController, zamiast nadpisywania viewDidLayoutSubviews, możesz nadpisać layoutSubviews
Andy Weinstein
4

To rozwiązanie zmienia rozmiar tableHeaderView i pozwala uniknąć nieskończonej pętli w viewDidLayoutSubviews()metodzie, którą miałem z niektórymi innymi odpowiedziami tutaj:

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
        }
    }
}

Zobacz także ten post: https://stackoverflow.com/a/34689293/1245231

petrsyn
źródło
1
Ja też używam tej metody. Pomyśl, że jest najlepszy, ponieważ nie ma majstrowania przy ograniczeniach. Jest również bardzo kompaktowy.
biografia
3

Twoje rozwiązanie wykorzystujące systemLayoutSizeFittingSize: działa, jeśli widok nagłówka jest aktualizowany tylko raz przy każdym pojawieniu się widoku. W moim przypadku widok nagłówka był wielokrotnie aktualizowany, aby odzwierciedlić zmiany stanu. Ale systemLayoutSizeFittingSize: zawsze zgłaszał ten sam rozmiar. Oznacza to, że rozmiar odpowiadający pierwszej aktualizacji.

Aby uzyskać systemLayoutSizeFittingSize: aby zwrócić prawidłowy rozmiar po każdej aktualizacji, musiałem najpierw usunąć widok nagłówka tabeli przed aktualizacją i ponownym dodaniem:

self.listTableView.tableHeaderView = nil;
[self.headerView removeFromSuperview];
Kim André Sand
źródło
2

To zadziałało dla mnie na ios10 i Xcode 8

func layoutTableHeaderView() {

    guard let headerView = tableView.tableHeaderView else { return }
    headerView.translatesAutoresizingMaskIntoConstraints = false

    let headerWidth = headerView.bounds.size.width;
    let temporaryWidthConstraints = NSLayoutConstraint.constraintsWithVisualFormat("[headerView(width)]", options: NSLayoutFormatOptions(rawValue: UInt(0)), metrics: ["width": headerWidth], views: ["headerView": headerView])

    headerView.addConstraints(temporaryWidthConstraints)

    headerView.setNeedsLayout()
    headerView.layoutIfNeeded()

    let headerSize = headerView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
    let height = headerSize.height
    var frame = headerView.frame

    frame.size.height = height
    headerView.frame = frame

    self.tableView.tableHeaderView = headerView

    headerView.removeConstraints(temporaryWidthConstraints)
    headerView.translatesAutoresizingMaskIntoConstraints = true

}
Ravi Randeria
źródło
2

Działa zarówno w widoku nagłówka, jak i stopce, po prostu zastąp nagłówek stopką

func sizeHeaderToFit() {
    if let headerView = tableView.tableHeaderView {

        headerView.setNeedsLayout()
        headerView.layoutIfNeeded()

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

        tableView.tableHeaderView = headerView
    }
}
Sai kumar Reddy
źródło
1

W przypadku systemu iOS 12 i nowszych poniższe kroki zapewnią, że autoukład działa poprawnie zarówno w tabeli, jak iw nagłówku.

  1. Najpierw utwórz tableView, a następnie nagłówek.
  2. Na końcu kodu tworzenia nagłówka zadzwoń:
[headerV setNeedsLayout];
[headerV layoutIfNeeded];
  1. Po zmianie orientacji ponownie zaznacz nagłówek układu i ponownie załaduj tabelę, musi to nastąpić nieco po zgłoszeniu zmiany orientacji:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 *NSEC_PER_SEC), dispatch_get_main_queue(), ^{

  [tableV.tableHeaderView setNeedsLayout];
  [tableV.tableHeaderView layoutIfNeeded];
  [tableV reloadData];});
RunLoop
źródło
0

W moim przypadku viewDidLayoutSubviewszadziałało lepiej. viewWillLayoutSubviewspowoduje pojawienie się białych linii litery a tableView. Dodałem również sprawdzenie, czy mój headerViewobiekt już istnieje.

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];

    if ( ! self.userHeaderView ) {
        // Setup HeaderView
        self.userHeaderView = [[[NSBundle mainBundle] loadNibNamed:@"SSUserHeaderView" owner:self options:nil] objectAtIndex:0];
        [self.userHeaderView setNeedsLayout];
        [self.userHeaderView layoutIfNeeded];
        CGFloat height = [self.userHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
        CGRect headerFrame = self.userHeaderView.frame;
        headerFrame.size.height = height;
        self.userHeaderView.frame = headerFrame;
        self.tableView.tableHeaderView = self.userHeaderView;

        // Update HeaderView with data
        [self.userHeaderView updateWithProfileData];
    }
}
Denis Kutlubaev
źródło
0

Całkiem możliwe jest użycie ogólnego układu opartego na autoukładzie UIViewz dowolną wewnętrzną strukturą widoku podrzędnego AL jako pliku tableHeaderView.

Jedyne, czego potrzebujesz, to ustawić tableFooterViewwcześniej proste !

Niech self.headerViewjest oparta na ograniczeniach UIView.

- (void)viewDidLoad {

    ........................

    self.tableView.tableFooterView = [UIView new];

    [self.headerView layoutIfNeeded]; // to set initial size

    self.tableView.tableHeaderView = self.headerView;

    [self.tableView.leadingAnchor constraintEqualToAnchor:self.headerView.leadingAnchor].active = YES;
    [self.tableView.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor].active = YES;
    [self.tableView.topAnchor constraintEqualToAnchor:self.headerView.topAnchor].active = YES;

    // and the key constraint
    [self.tableFooterView.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor].active = YES;
}

W przypadku self.headerViewzmiany wysokości w ramach rotacji UI należy zaimplementować

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];

    [coordinator animateAlongsideTransition: ^(id<UIViewControllerTransitionCoordinatorContext> context) {
        // needed to resize header height
        self.tableView.tableHeaderView = self.headerView;
    } completion: NULL];
}

W tym celu można wykorzystać kategorię ObjC

@interface UITableView (AMHeaderView)
- (void)am_insertHeaderView:(UIView *)headerView;
@end

@implementation UITableView (AMHeaderView)

- (void)am_insertHeaderView:(UIView *)headerView {

    NSAssert(self.tableFooterView, @"Need to define tableFooterView first!");

    [headerView layoutIfNeeded];

    self.tableHeaderView = headerView;

    [self.leadingAnchor constraintEqualToAnchor:headerView.leadingAnchor].active = YES;
    [self.trailingAnchor constraintEqualToAnchor:headerView.trailingAnchor].active = YES;
    [self.topAnchor constraintEqualToAnchor:headerView.topAnchor].active = YES;

    [self.tableFooterView.trailingAnchor constraintEqualToAnchor:headerView.trailingAnchor].active = YES;
}
@end

A także szybkie rozszerzenie

extension UITableView {

    func am_insertHeaderView2(_ headerView: UIView) {

        assert(tableFooterView != nil, "Need to define tableFooterView first!")

        headerView.layoutIfNeeded()

        tableHeaderView = headerView

        leadingAnchor.constraint(equalTo: headerView.leadingAnchor).isActive = true
        trailingAnchor.constraint(equalTo: headerView.trailingAnchor).isActive = true
        topAnchor.constraint(equalTo: headerView.topAnchor).isActive = true

        tableFooterView?.trailingAnchor.constraint(equalTo: headerView.trailingAnchor).isActive = true
    }
}
malex
źródło
0

Skopiowano z tego posta

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    
    if let headerView = tableView.tableHeaderView {

        let height = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).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
        }
    }
}
Sourabh Sharma
źródło