Wie kann ich die Größe der Übersicht ändern, um sie an alle Unteransichten mit Autolayout anzupassen?

142

Mein Verständnis von Autolayout ist, dass es die Größe der Übersicht verwendet und auf Einschränkungen und intrinsischen Größen basiert, die Positionen von Unteransichten berechnen.

Gibt es eine Möglichkeit, diesen Prozess umzukehren? Ich möchte die Größe der Übersicht auf der Grundlage von Einschränkungen und Eigengrößen ändern. Was ist der einfachste Weg, dies zu erreichen?

Ich habe eine in Xcode entworfene Ansicht, für die ich sie als Header verwende UITableView. Diese Ansicht enthält eine Beschriftung und eine Schaltfläche. Die Größe des Etiketts hängt von den Daten ab. Abhängig von den Einschränkungen drückt das Etikett die Schaltfläche erfolgreich nach unten, oder wenn zwischen der Schaltfläche und dem unteren Rand der Übersicht eine Einschränkung besteht, wird das Etikett komprimiert.

Ich habe einige ähnliche Fragen gefunden, aber sie haben keine guten und einfachen Antworten.

DAK
quelle
28
Sie sollten eine der folgenden Antworten auswählen, da Tom Swifts Ihre Frage mit Sicherheit beantwortet hat. Alle Poster haben viel Zeit aufgewendet, um Ihnen zu helfen. Jetzt sollten Sie Ihren Beitrag leisten und die Antwort auswählen, die Ihnen am besten gefällt.
David H

Antworten:

149

Die richtige API ist UIView systemLayoutSizeFittingSize:, entweder UILayoutFittingCompressedSizeoder zu übergeben UILayoutFittingExpandedSize.

Bei einer normalen UIViewVerwendung von Autolayout sollte dies nur funktionieren, solange Ihre Einschränkungen korrekt sind. Wenn Sie es für a verwenden möchten UITableViewCell(um beispielsweise die Zeilenhöhe zu bestimmen), sollten Sie es gegen Ihre Zelle aufrufen contentViewund die Höhe ermitteln.

Weitere Überlegungen bestehen, wenn Sie ein oder mehrere UILabels in Ihrer Ansicht haben, die mehrzeilig sind. Für diese ist es unbedingt erforderlich, dass die preferredMaxLayoutWidthEigenschaft korrekt festgelegt wird, sodass die Beschriftung eine korrekte Eigenschaft enthält intrinsicContentSize, die bei der systemLayoutSizeFittingSize'sBerechnung verwendet wird.

BEARBEITEN: Auf Anfrage wird ein Beispiel für die Höhenberechnung für eine Tabellenansichtszelle hinzugefügt

Die Verwendung von Autolayout für die Berechnung der Tabellenzellenhöhe ist nicht sehr effizient, aber sicher praktisch, insbesondere wenn Sie eine Zelle mit einem komplexen Layout haben.

Wie oben erwähnt, ist es bei Verwendung einer mehrzeiligen UILabelDatei unbedingt erforderlich, die preferredMaxLayoutWidthmit der Etikettenbreite zu synchronisieren . Ich benutze dazu eine benutzerdefinierte UILabelUnterklasse:

@implementation TSLabel

- (void) layoutSubviews
{
    [super layoutSubviews];

    if ( self.numberOfLines == 0 )
    {
        if ( self.preferredMaxLayoutWidth != self.frame.size.width )
        {
            self.preferredMaxLayoutWidth = self.frame.size.width;
            [self setNeedsUpdateConstraints];
        }
    }
}

- (CGSize) intrinsicContentSize
{
    CGSize s = [super intrinsicContentSize];

    if ( self.numberOfLines == 0 )
    {
        // found out that sometimes intrinsicContentSize is 1pt too short!
        s.height += 1;
    }

    return s;
}

@end

Hier ist eine erfundene UITableViewController-Unterklasse, die heightForRowAtIndexPath demonstriert:

#import "TSTableViewController.h"
#import "TSTableViewCell.h"

@implementation TSTableViewController

- (NSString*) cellText
{
    return @"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
}

#pragma mark - Table view data source

- (NSInteger) numberOfSectionsInTableView: (UITableView *) tableView
{
    return 1;
}

- (NSInteger) tableView: (UITableView *)tableView numberOfRowsInSection: (NSInteger) section
{
    return 1;
}

- (CGFloat) tableView: (UITableView *) tableView heightForRowAtIndexPath: (NSIndexPath *) indexPath
{
    static TSTableViewCell *sizingCell;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        sizingCell = (TSTableViewCell*)[tableView dequeueReusableCellWithIdentifier: @"TSTableViewCell"];
    });

    // configure the cell
    sizingCell.text = self.cellText;

    // force layout
    [sizingCell setNeedsLayout];
    [sizingCell layoutIfNeeded];

    // get the fitting size
    CGSize s = [sizingCell.contentView systemLayoutSizeFittingSize: UILayoutFittingCompressedSize];
    NSLog( @"fittingSize: %@", NSStringFromCGSize( s ));

    return s.height;
}

- (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath
{
    TSTableViewCell *cell = (TSTableViewCell*)[tableView dequeueReusableCellWithIdentifier: @"TSTableViewCell" ];

    cell.text = self.cellText;

    return cell;
}

@end

Eine einfache benutzerdefinierte Zelle:

#import "TSTableViewCell.h"
#import "TSLabel.h"

@implementation TSTableViewCell
{
    IBOutlet TSLabel* _label;
}

- (void) setText: (NSString *) text
{
    _label.text = text;
}

@end

Und hier ist ein Bild der im Storyboard definierten Einschränkungen. Beachten Sie, dass das Etikett keine Höhen- / Breitenbeschränkungen enthält. Diese werden aus den folgenden Angaben abgeleitet intrinsicContentSize:

Geben Sie hier die Bildbeschreibung ein

TomSwift
quelle
1
Können Sie ein Beispiel geben, wie eine Implementierung von heightForRowAtIndexPath: aussehen würde, wenn diese Methode mit einer Zelle verwendet wird, die eine mehrzeilige Beschriftung enthält? Ich habe ziemlich viel damit herumgespielt und es nicht zum Laufen gebracht. Wie erhält man eine Zelle (insbesondere wenn die Zelle in einem Storyboard eingerichtet ist)? Welche Einschränkungen benötigen Sie, damit es funktioniert?
Rdelmar
@rdelmar - sicher. Ich habe meiner Antwort ein Beispiel hinzugefügt.
TomSwift
7
Dies funktionierte für mich nicht, bis ich eine letzte vertikale Einschränkung vom unteren Rand der Zelle zum unteren Rand der untersten Unteransicht in der Zelle hinzufügte. Es scheint, dass die vertikalen Einschränkungen den oberen und unteren vertikalen Abstand zwischen der Zelle und ihrem Inhalt enthalten müssen, damit die Berechnung der Zellenhöhe erfolgreich ist.
Eric Baker
1
Ich brauchte nur noch einen Trick, um es zum Laufen zu bringen. Und @EricBakers Tipp hat es endlich geschafft. Danke, dass du diesen Mann geteilt hast. Ich habe jetzt eine Größe ungleich Null, entweder gegen die sizingCelloder gegen die contentView.
MkVal
1
Wenn Sie Probleme haben, nicht die richtige Höhe zu finden, stellen Sie sicher, dass Ihre sizingCellBreite mit Ihrer Breite übereinstimmt tableView.
MkVal
30

Der Kommentar von Eric Baker hat mich auf die Kernidee hingewiesen, dass der Inhalt einer Ansicht eine explizite Beziehung zur enthaltenen Ansicht haben muss, damit ihre Größe durch den darin enthaltenen Inhalt bestimmt wird, um ihre Höhe zu bestimmen (oder Breite) dynamisch . "Unteransicht hinzufügen" erstellt diese Beziehung nicht, wie Sie vielleicht annehmen. Sie müssen auswählen, welche Unteransicht die Höhe und / oder Breite des Containers bestimmen soll ... am häufigsten das UI-Element, das Sie in der unteren rechten Ecke Ihrer gesamten UI platziert haben. Hier sind einige Code- und Inline-Kommentare, um den Punkt zu veranschaulichen.

Beachten Sie, dass dies für diejenigen, die mit Bildlaufansichten arbeiten, von besonderem Wert sein kann, da es üblich ist, eine einzelne Inhaltsansicht zu entwerfen, die ihre Größe dynamisch bestimmt (und dies der Bildlaufansicht mitteilt), basierend auf dem, was Sie darin eingeben. Viel Glück, hoffe das hilft jemandem da draußen.

//
//  ViewController.m
//  AutoLayoutDynamicVerticalContainerHeight
//

#import "ViewController.h"

@interface ViewController ()
@property (strong, nonatomic) UIView *contentView;
@property (strong, nonatomic) UILabel *myLabel;
@property (strong, nonatomic) UILabel *myOtherLabel;
@end

@implementation ViewController

- (void)viewDidLoad
{
    // INVOKE SUPER
    [super viewDidLoad];

    // INIT ALL REQUIRED UI ELEMENTS
    self.contentView = [[UIView alloc] init];
    self.myLabel = [[UILabel alloc] init];
    self.myOtherLabel = [[UILabel alloc] init];
    NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_contentView, _myLabel, _myOtherLabel);

    // TURN AUTO LAYOUT ON FOR EACH ONE OF THEM
    self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
    self.myLabel.translatesAutoresizingMaskIntoConstraints = NO;
    self.myOtherLabel.translatesAutoresizingMaskIntoConstraints = NO;

    // ESTABLISH VIEW HIERARCHY
    [self.view addSubview:self.contentView]; // View adds content view
    [self.contentView addSubview:self.myLabel]; // Content view adds my label (and all other UI... what's added here drives the container height (and width))
    [self.contentView addSubview:self.myOtherLabel];

    // LAYOUT

    // Layout CONTENT VIEW (Pinned to left, top. Note, it expects to get its vertical height (and horizontal width) dynamically based on whatever is placed within).
    // Note, if you don't want horizontal width to be driven by content, just pin left AND right to superview.
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_contentView]" options:0 metrics:0 views:viewsDictionary]]; // Only pinned to left, no horizontal width yet
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_contentView]" options:0 metrics:0 views:viewsDictionary]]; // Only pinned to top, no vertical height yet

    /* WHATEVER WE ADD NEXT NEEDS TO EXPLICITLY "PUSH OUT ON" THE CONTAINING CONTENT VIEW SO THAT OUR CONTENT DYNAMICALLY DETERMINES THE SIZE OF THE CONTAINING VIEW */
    // ^To me this is what's weird... but okay once you understand...

    // Layout MY LABEL (Anchor to upper left with default margin, width and height are dynamic based on text, font, etc (i.e. UILabel has an intrinsicContentSize))
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_myLabel]" options:0 metrics:0 views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[_myLabel]" options:0 metrics:0 views:viewsDictionary]];

    // Layout MY OTHER LABEL (Anchored by vertical space to the sibling label that comes before it)
    // Note, this is the view that we are choosing to use to drive the height (and width) of our container...

    // The LAST "|" character is KEY, it's what drives the WIDTH of contentView (red color)
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_myOtherLabel]-|" options:0 metrics:0 views:viewsDictionary]];

    // Again, the LAST "|" character is KEY, it's what drives the HEIGHT of contentView (red color)
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_myLabel]-[_myOtherLabel]-|" options:0 metrics:0 views:viewsDictionary]];

    // COLOR VIEWS
    self.view.backgroundColor = [UIColor purpleColor];
    self.contentView.backgroundColor = [UIColor redColor];
    self.myLabel.backgroundColor = [UIColor orangeColor];
    self.myOtherLabel.backgroundColor = [UIColor greenColor];

    // CONFIGURE VIEWS

    // Configure MY LABEL
    self.myLabel.text = @"HELLO WORLD\nLine 2\nLine 3, yo";
    self.myLabel.numberOfLines = 0; // Let it flow

    // Configure MY OTHER LABEL
    self.myOtherLabel.text = @"My OTHER label... This\nis the UI element I'm\narbitrarily choosing\nto drive the width and height\nof the container (the red view)";
    self.myOtherLabel.numberOfLines = 0;
    self.myOtherLabel.font = [UIFont systemFontOfSize:21];
}

@end

So ändern Sie die Größe der Übersicht, um sie an alle Unteransichten mit autolayout.png anzupassen

John Erck
quelle
3
Dies ist ein großartiger Trick und nicht bekannt. Um es zu wiederholen: Wenn die inneren Ansichten eine intrinsische Höhe haben und oben und unten fixiert sind, muss die äußere Ansicht ihre Höhe nicht angeben und wird tatsächlich ihren Inhalt umarmen. Möglicherweise müssen Sie die Komprimierung und Umarmung von Inhalten anpassen, damit die inneren Ansichten die gewünschten Ergebnisse erzielen.
Phatmann
Das ist Geld! Eines der prägnanteren VFL-Beispiele, die ich gesehen habe.
Evan R
Dies ist , was ich gesucht habe (Danke @ John) , damit ich eine Swift - Version dieser gepostet habe hier
James
1
"Sie müssen auswählen, welche Unteransicht die Höhe und / oder Breite des Containers bestimmen soll ... am häufigsten das UI-Element, das Sie in der unteren rechten Ecke Ihrer gesamten UI platziert haben". Ich verwende PureLayout, und das war der Schlüssel für mich. Bei einer übergeordneten Ansicht mit einer untergeordneten Ansicht befand sich die untergeordnete Ansicht in der unteren rechten Ecke, wodurch die übergeordnete Ansicht plötzlich eine Größe erhielt. Vielen Dank!
Steven Elliott
1
Die Auswahl einer Ansicht zum Fahren der Breite ist genau das, was ich nicht tun kann. Manchmal ist eine Unteransicht breiter, manchmal ist eine andere Unteransicht breiter. Irgendwelche Ideen für diesen Fall?
Henning
3

Sie können dies tun, indem Sie eine Einschränkung erstellen und über den Interface Builder verbinden

Siehe Erklärung: Auto_Layout_Constraints_in_Interface_Builder

raywenderlich Anfang-Auto-Layout

AutolayoutPG Artikel beschränken die Grundlagen

@interface ViewController : UIViewController {
    IBOutlet NSLayoutConstraint *leadingSpaceConstraint;
    IBOutlet NSLayoutConstraint *topSpaceConstraint;
}
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *leadingSpaceConstraint;

Verbinden Sie diesen Constraint-Ausgang mit Ihren Unteransichten. Constraint oder verbinden Sie auch Super Views Constraint und stellen Sie ihn entsprechend Ihren Anforderungen ein

 self.leadingSpaceConstraint.constant = 10.0;//whatever you want to assign

Ich hoffe das klärt es.

Chandan
quelle
So ist es möglich, in XCode erstellte Einschränkungen aus dem Quellcode zu konfigurieren. Die Frage ist jedoch, wie sie so konfiguriert werden können, dass die Größe der Übersicht geändert wird.
DAK
JA @DAK. Bitte achten Sie auf das Übersichtslayout. Setzen Sie die Einschränkung auf die Übersicht und machen Sie auch die Höhenbeschränkung. Wenn sich Ihre Unteransicht vergrößert, erhöht sich automatisch die Höhe der Aufsichtsbeschränkung.
Chandan
Das hat auch bei mir funktioniert. Es hilft, dass ich Zellen mit fester Größe habe (Höhe 60px). Wenn die Ansicht geladen wird, setze ich meine IBOutlet-Höhenbeschränkung auf 60 * x, wobei x die Anzahl der Zellen ist. Letztendlich möchten Sie dies wahrscheinlich in einer Bildlaufansicht, damit Sie das Ganze sehen können.
Sandy Chapman
-1

Dies kann für ein normales subviewin einem größeren gemacht werden UIView, aber es funktioniert nicht automatisch für headerViews. Die Höhe von a headerViewwird durch die Rückgabe von bestimmt. tableView:heightForHeaderInSection:Sie müssen also heightdie Höhe heightdes UILabelPlus-Platzes für die UIButtonund die von paddingIhnen benötigten berechnen . Sie müssen so etwas tun:

-(CGFloat)tableView:(UITableView *)tableView 
          heightForHeaderInSection:(NSInteger)section {
    NSString *s = self.headeString[indexPath.section];
    CGSize size = [s sizeWithFont:[UIFont systemFontOfSize:17] 
                constrainedToSize:CGSizeMake(281, CGFLOAT_MAX)
                    lineBreakMode:NSLineBreakByWordWrapping];
    return size.height + 60;
}

Hier headerStringist die Zeichenfolge, die Sie füllen möchten UILabel, und die Nummer 281 ist die widthder UILabel(wie in eingerichtet Interface Builder).

rdelmar
quelle
Das funktioniert leider nicht. "Größe an Inhalt anpassen" in der Übersicht entfernt die Einschränkung, die die Unterseite der Schaltfläche wie vorhergesagt mit der Übersicht verbindet. Zur Laufzeit wird die Größe des Etiketts geändert und die Taste nach unten gedrückt, aber die Größe der Übersicht wird nicht automatisch geändert.
DAK
@DAK, sorry, das Problem ist, dass Ihre Ansicht der Header für eine Tabellenansicht ist. Ich habe deine Situation falsch verstanden. Ich dachte, dass sich die Ansicht, die die Schaltfläche und die Beschriftung enthält, in einer anderen Ansicht befindet, aber es hört sich so an, als würden Sie sie als Kopfzeile verwenden (anstatt eine Ansicht innerhalb der Kopfzeile zu sein). Meine ursprüngliche Antwort wird also nicht funktionieren. Ich habe meine Antwort auf das geändert, was meiner Meinung nach funktionieren sollte.
Rdelmar
Dies ähnelt meiner aktuellen Implementierung, aber ich hatte gehofft, dass es einen besseren Weg gibt. Ich würde es vorziehen, diesen Code nicht jedes Mal zu aktualisieren, wenn ich meinen Header ändere.
DAK
@ Dak, Entschuldigung, ich glaube nicht, dass es einen besseren Weg gibt, so funktionieren Tabellenansichten.
Rdelmar
Bitte sehen Sie meine Antwort. Der "bessere Weg" ist die Verwendung von UIView systemLayoutSizeFittingSize:. Das heißt, ein vollständiges Autolayout für eine Ansicht oder Zelle durchzuführen, um die erforderliche Höhe in einer Tabellenansicht zu erhalten, ist ziemlich teuer. Ich würde es für komplexe Zellen tun, aber für einfachere Fälle vielleicht auf eine Lösung wie Ihre zurückgreifen.
TomSwift