Jak zmienić rozmiar nadzoru, aby dopasować wszystkie podglądy do automatycznego układu?


142

Moje rozumienie autoukładu jest takie, że bierze rozmiar superwizji i na podstawie ograniczeń i wewnętrznych rozmiarów oblicza pozycje podwidoków.

Czy istnieje sposób na odwrócenie tego procesu? Chcę zmienić rozmiar superview na podstawie ograniczeń i wewnętrznych rozmiarów. Jaki jest najprostszy sposób osiągnięcia tego?

Mam widok zaprojektowany w Xcode, którego używam jako nagłówka UITableView. Ten widok zawiera etykietę i przycisk. Rozmiar etykiety różni się w zależności od danych. W zależności od ograniczeń etykieta pomyślnie naciska przycisk w dół lub jeśli istnieje ograniczenie między przyciskiem a dolną częścią nadzoru, etykieta jest kompresowana.

Znalazłem kilka podobnych pytań, ale nie mają one dobrych i łatwych odpowiedzi.


28
Wybierz jedną z poniższych odpowiedzi, ponieważ na pewno Tom Swifts odpowiedział na Twoje pytanie. Wszystkie plakaty poświęciły ogromną ilość czasu, aby ci pomóc, teraz powinieneś zrobić swoją część i wybrać odpowiedź, którą lubisz najbardziej.
David H

Odpowiedzi:


149

Prawidłowe API do stosowania jest UIView systemLayoutSizeFittingSize:, przekazując albo UILayoutFittingCompressedSizealbo UILayoutFittingExpandedSize.

W przypadku normalnego UIViewkorzystania z autoukładu powinno to działać tak długo, jak długo ograniczenia są poprawne. Jeśli chcesz go użyć na UITableViewCell(na przykład do określenia wysokości wiersza), powinieneś wywołać go w swojej komórcecontentView i złapać wysokość.

Dalsze rozważania istnieją, jeśli masz jeden lub więcej UILabel, które są wielowierszowe. W tym przypadku konieczne jest prawidłowe ustawienie preferredMaxLayoutWidthwłaściwości, tak aby etykieta zawierała poprawną intrinsicContentSize, która zostanie użyta w systemLayoutSizeFittingSize'sobliczeniach.

EDYCJA: na żądanie, dodanie przykładu obliczenia wysokości dla komórki widoku tabeli

Używanie autoukładu do obliczania wysokości komórek tabeli nie jest super wydajne, ale z pewnością jest wygodne, zwłaszcza jeśli masz komórkę o złożonym układzie.

Jak powiedziałem powyżej, jeśli używasz multilinii UILabel, konieczne jest zsynchronizowanie z preferredMaxLayoutWidthszerokością etykiety. UILabelAby to zrobić, używam niestandardowej podklasy:

@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

Oto sztuczna podklasa UITableViewController demonstrująca heightForRowAtIndexPath:

#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

Prosta komórka niestandardowa:

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

@implementation TSTableViewCell
{
    IBOutlet TSLabel* _label;
}

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

@end

A oto obraz ograniczeń zdefiniowanych w Storyboard. Zwróć uwagę, że na etykiecie nie ma ograniczeń wysokości / szerokości - są one wywnioskowane z etykiety intrinsicContentSize:

wprowadź opis obrazu tutaj


1
Czy możesz podać przykład, jak implementacja heightForRowAtIndexPath: wyglądałaby przy użyciu tej metody z komórką zawierającą etykietę wieloliniową? Sporo się z tym pogubiłem i nie udało mi się go uruchomić. Jak zdobyć komórkę (zwłaszcza jeśli komórka jest ustawiona w scenorysie)? Jakie ograniczenia potrzebujesz, aby to działało?
rdelmar

@rdelmar - jasne. Dodałem przykład do mojej odpowiedzi.
TomSwift,

7
Nie działało to w moim przypadku, dopóki nie dodałem ostatecznego wiązania pionowego od dołu komórki do dołu najniższego widoku podrzędnego w komórce. Wydaje się, że ograniczenia pionowe muszą obejmować górny i dolny pionowy odstęp między komórką a jej zawartością, aby obliczenie wysokości komórki się powiodło.
Eric Baker

1
Potrzebowałem tylko jednej sztuczki, żeby to zadziałało. I wreszcie wskazówka @EricBakera przyniosła skutek. Dzięki za podzielenie się tym człowiekiem. Mam teraz rozmiar niezerowy w porównaniu z sizingCelllub jego contentView.
MkVal

1
Tym, którzy mają problemy z uzyskaniem odpowiedniej wysokości, upewnij się sizingCell, że szerokość odpowiada tableViewszerokości.
MkVal

30

Komentarz Erica Baker'a wskazał mi podstawową ideę, że aby widok był określany przez zawartość umieszczoną w nim, zawartość umieszczona w nim musi mieć wyraźny związek z widokiem zawierającym, aby napędzać jego wysokość (lub szerokość) dynamicznie . „Dodaj widok podrzędny” nie tworzy tej relacji, jak można by przypuszczać. Musisz wybrać, który widok podrzędny będzie sterował wysokością i / lub szerokością kontenera ... najczęściej jest to element interfejsu użytkownika, który umieściłeś w prawym dolnym rogu ogólnego interfejsu użytkownika. Oto kod i komentarze w wierszu, które ilustrują ten punkt.

Uwaga: może to mieć szczególną wartość dla osób pracujących z widokami przewijania, ponieważ często projektuje się wokół pojedynczego widoku zawartości, który określa jego rozmiar (i przekazuje go do widoku przewijania) dynamicznie na podstawie tego, co w nim umieścisz. Powodzenia, mam nadzieję, że to komuś pomoże.

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

Jak zmienić rozmiar superview, aby pasował do wszystkich podglądów z autolayout.png


3
To świetna sztuczka i mało znana. Powtórzę: jeśli wewnętrzne widoki mają wewnętrzną wysokość i są przypięte do góry i do dołu, widok zewnętrzny nie musi określać swojej wysokości i faktycznie będzie obejmował zawartość. Aby uzyskać pożądane rezultaty, może być konieczne dostosowanie kompresji i przytulania treści do wewnętrznych widoków.
phatmann

To są pieniądze! Jeden z lepszych, zwięzłych przykładów VFL, jakie widziałem.
Evan R

To właśnie szukałem (dzięki @John) więc już napisali wersji Swift tego tutaj
James

1
„Musisz wybrać, który widok podrzędny będzie sterował wysokością i / lub szerokością kontenera… najczęściej jest to element interfejsu użytkownika umieszczony w prawym dolnym rogu ogólnego interfejsu użytkownika”. Używam PureLayout, i to był dla mnie klucz. W przypadku jednego widoku rodzica, z jednym widokiem dziecka, umieszczano widok dziecka, aby przypiąć w prawym dolnym rogu, co nagle nadało widokowi rodzica rozmiar. Dzięki!
Steven Elliott

1
Wybranie jednego widoku do sterowania szerokością jest dokładnie tym, czego nie mogę zrobić. Czasami jeden pod-widok jest szerszy, a czasami inny pod-widok jest szerszy. Jakieś pomysły na tę sprawę?
Henning,

3

Możesz to zrobić, tworząc ograniczenie i łącząc je za pomocą konstruktora interfejsu

Zobacz wyjaśnienie: Auto_Layout_Constraints_in_Interface_Builder

raywenderlich początek-automatyczny-układ

Podstawy ograniczeń artykułów AutolayoutPG

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

połącz to gniazdo Ograniczenia z widokami podrzędnymi Ogranicz lub połącz super widoki Ograniczenie i ustaw je zgodnie z własnymi wymaganiami w ten sposób

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

Mam nadzieję, że to wyjaśnia to.


Możliwe jest więc skonfigurowanie ograniczeń utworzonych w XCode z kodu źródłowego. Ale pytanie brzmi, jak je skonfigurować, aby zmienić rozmiar superview.
DAK,

TAK @DAK. upewnij się, że masz układ superview. Umieść Constraint na superview i utwórz także Height Constraint, aby za każdym razem, gdy twój subview zwiększył, automatycznie zwiększał superview Constraint height.
chandan

To też zadziałało dla mnie. Pomaga, że ​​mam komórki o stałej wielkości (wysokość 60px). Więc kiedy widok się ładuje, ustawiłem ograniczenie wysokości IBOutlet na 60 * x, gdzie x to liczba komórek. Ostatecznie prawdopodobnie chcesz, aby to było w widoku przewijania, abyś mógł zobaczyć całość.
Sandy Chapman

-1

Można to zrobić dla normalnego subviewwnętrza większego UIView, ale nie działa to automatycznie headerViews. Wysokość headerViewjest określona przez co zwracana przez tableView:heightForHeaderInSection:więc trzeba obliczyć heightw oparciu o heightna UILabelprzestrzeni plus dla UIButtona wszelkie paddingpotrzebujesz. Musisz zrobić coś takiego:

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

Tutaj headerStringjest cokolwiek ciąg chcesz, aby wypełnić UILabel, a liczba 281 jest widthz UILabel(jak w konfiguracji Interface Builder)


Niestety to nie działa. „Rozmiar dopasowany do treści” w superviewie usuwa ograniczenie, które łączy dolną część przycisku z superviewem, zgodnie z przewidywaniami. W czasie wykonywania rozmiar etykiety jest zmieniany i przycisk jest wciskany, ale rozmiar nadzoru nie jest zmieniany automatycznie.
DAK

@DAK, przepraszam, problem polega na tym, że Twój widok jest nagłówkiem widoku tabeli. Źle zrozumiałem twoją sytuację. Myślałem, że widok obejmujący przycisk i etykietę znajduje się wewnątrz innego widoku, ale wygląda na to, że używasz go jako nagłówka (zamiast być widokiem wewnątrz nagłówka). Tak więc moja pierwotna odpowiedź nie zadziała. Zmieniłem odpowiedź na to, co moim zdaniem powinno działać.
rdelmar

jest to podobne do mojej obecnej implementacji, ale miałem nadzieję, że jest lepszy sposób. Wolałbym unikać aktualizowania tego kodu za każdym razem, gdy zmieniam nagłówek.
DAK

@Dak, przepraszam, nie sądzę, że istnieje lepszy sposób, tak po prostu działają widoki tabeli.
rdelmar

Zobacz moją odpowiedź. „Lepszym sposobem” jest użycie UIView systemLayoutSizeFittingSize :. To powiedziawszy, wykonanie pełnego automatycznego układu widoku lub komórki tylko po to, aby uzyskać wymaganą wysokość w widoku tabeli, jest dość kosztowne. Zrobiłbym to dla złożonych komórek, ale być może wrócę do rozwiązania takiego jak twoje w prostszych przypadkach.
TomSwift
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.