Bagaimana cara mengubah ukuran superview agar sesuai dengan semua subview dengan autolayout?

142

Pemahaman saya tentang autolayout adalah bahwa diperlukan ukuran superview dan berdasarkan batasan dan ukuran intrinsik yang menghitung posisi subview.

Apakah ada cara untuk membalik proses ini? Saya ingin mengubah ukuran superview berdasarkan batasan dan ukuran intrinsik. Apa cara paling sederhana untuk mencapai ini?

Saya telah melihat dirancang dalam Xcode yang saya gunakan sebagai header untuk UITableView. Tampilan ini termasuk label dan tombol. Ukuran label berbeda tergantung pada data. Tergantung pada batasan, label berhasil menekan tombol ke bawah atau jika ada kendala antara tombol dan bagian bawah superview label dikompresi.

Saya telah menemukan beberapa pertanyaan serupa tetapi mereka tidak memiliki jawaban yang baik dan mudah.

DAK
sumber
28
Anda harus memilih salah satu jawaban di bawah ini, karena pasti Tom Swifts menjawab pertanyaan Anda. Semua poster menghabiskan banyak waktu untuk membantu Anda, sekarang Anda harus melakukan bagian Anda dan memilih jawaban yang paling Anda sukai.
David H

Jawaban:

149

API yang benar untuk digunakan adalah UIView systemLayoutSizeFittingSize:, melewati salah satu UILayoutFittingCompressedSizeatau UILayoutFittingExpandedSize.

Untuk UIViewpenggunaan autolayout yang normal, ini hanya akan berfungsi selama kendala Anda benar. Jika Anda ingin menggunakannya pada UITableViewCell(untuk menentukan tinggi baris misalnya) maka Anda harus menyebutnya terhadap sel Anda contentViewdan ambil tingginya.

Pertimbangan lebih lanjut ada jika Anda memiliki satu atau lebih UILabel dalam pandangan Anda yang multiline. Untuk ini, sangat penting bahwa preferredMaxLayoutWidthproperti diatur dengan benar sehingga label memberikan yang benar intrinsicContentSize, yang akan digunakan dalam systemLayoutSizeFittingSize'sperhitungan.

EDIT: dengan permintaan, menambahkan contoh perhitungan tinggi untuk sel tampilan tabel

Menggunakan autolayout untuk penghitungan tinggi sel tabel tidak super efisien tetapi tentu saja nyaman, terutama jika Anda memiliki sel yang memiliki tata letak yang kompleks.

Seperti yang saya katakan di atas, jika Anda menggunakan multiline, UILabelsangat penting untuk menyinkronkan preferredMaxLayoutWidthke lebar label. Saya menggunakan UILabelsubkelas khusus untuk melakukan ini:

@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

Berikut ini adalah subclass UITableViewController yang dibuat yang memperlihatkan 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

Sel khusus sederhana:

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

@implementation TSTableViewCell
{
    IBOutlet TSLabel* _label;
}

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

@end

Dan, inilah gambar kendala yang didefinisikan di Storyboard. Perhatikan bahwa tidak ada batasan tinggi / lebar pada label - itu disimpulkan dari label intrinsicContentSize:

masukkan deskripsi gambar di sini

TomSwift
sumber
1
Bisakah Anda memberikan contoh seperti apa penerapan heightForRowAtIndexPath: seperti menggunakan metode ini dengan sel yang berisi label multi-baris? Saya sudah mengacaukannya sedikit, dan belum membuatnya bekerja. Bagaimana Anda mendapatkan sel (terutama jika sel diatur dalam storyboard)? Kendala apa yang Anda butuhkan untuk membuatnya berfungsi?
rdelmar
@rdelmar - tentu saja. Saya telah menambahkan contoh untuk jawaban saya.
TomSwift
7
Ini tidak berfungsi untuk saya sampai saya menambahkan batasan vertikal akhir dari bagian bawah sel ke bagian bawah subview terendah dalam sel. Tampaknya kendala vertikal harus mencakup jarak vertikal atas dan bawah antara sel dan kontennya agar perhitungan tinggi sel berhasil.
Eric Baker
1
Saya hanya perlu satu trik lagi untuk membuatnya bekerja. Dan tip @EricBaker akhirnya berhasil. Terima kasih telah berbagi pria itu. Saya mendapatkan ukuran non-nol sekarang terhadap sizingCellatau itu contentView.
MkVal
1
Bagi mereka yang kesulitan tidak mendapatkan ketinggian yang tepat, pastikan sizingCelllebar Anda cocok dengan tableViewlebar Anda.
MkVal
30

Komentar Eric Baker memberi saya ide inti bahwa agar suatu tampilan dapat ditentukan ukurannya oleh konten yang ditempatkan di dalamnya, maka konten yang ditempatkan di dalamnya harus memiliki hubungan eksplisit dengan tampilan yang mengandung untuk mendorong ketinggiannya. (atau lebar) secara dinamis . "Tambahkan subview" tidak membuat hubungan ini seperti yang Anda asumsikan. Anda harus memilih subview mana yang akan mengarahkan tinggi dan / atau lebar wadah ... paling umum apa pun elemen UI yang telah Anda tempatkan di sudut kanan bawah keseluruhan UI Anda. Berikut beberapa kode dan komentar sebaris untuk mengilustrasikan intinya.

Catatan, ini mungkin bernilai khusus bagi mereka yang bekerja dengan tampilan gulir karena biasanya dirancang di sekitar tampilan konten tunggal yang menentukan ukurannya (dan mengomunikasikan ini ke tampilan gulir) secara dinamis berdasarkan pada apa pun yang Anda masukkan ke dalamnya. Semoga beruntung, semoga ini membantu seseorang di luar sana.

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

Cara mengubah ukuran superview agar sesuai dengan semua subview dengan autolayout.png

John Erck
sumber
3
Ini adalah trik hebat dan tidak terkenal. Untuk mengulangi: jika pandangan bagian dalam memiliki tinggi intrinsik dan disematkan ke atas dan bawah, tampilan luar tidak perlu menentukan tinggi dan sebenarnya akan memeluk isinya. Anda mungkin perlu menyesuaikan kompresi konten dan memeluk pandangan bagian dalam untuk mendapatkan hasil yang diinginkan.
phatmann
Ini uang! Salah satu contoh VFL ringkas yang lebih baik yang pernah saya lihat.
Evan R
Inilah yang saya cari (Terima kasih @John) jadi saya telah memposting versi Swift ini di sini
James
1
"Anda harus memilih subview mana yang akan mengarahkan tinggi dan / atau lebar wadah ... paling umum apa pun elemen UI yang telah Anda tempatkan di sudut kanan bawah keseluruhan UI Anda" .. Saya menggunakan PureLayout, dan inilah kuncinya bagi saya. Untuk satu tampilan orang tua, dengan satu tampilan anak, itu duduk tampilan anak untuk pin ke kanan bawah yang tiba-tiba memberikan ukuran tampilan orang tua. Terima kasih!
Steven Elliott
1
Memilih satu tampilan untuk menggerakkan lebar adalah hal yang tidak bisa saya lakukan. Terkadang satu subview lebih lebar, terkadang subview lain lebih luas. Ada ide untuk kasus itu?
Henning
3

Anda dapat melakukan ini dengan membuat batasan dan menghubungkannya melalui pembuat antarmuka

Lihat penjelasan: Auto_Layout_Constraints_in_Interface_Builder

raywenderlich begin-auto-layout

AutolayoutPG Artikel membatasi Fundamentals

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

hubungkan outlet Constraint ini dengan sub-view Anda Constraint atau hubungkan juga super views Constraint dan atur sesuai dengan kebutuhan Anda seperti ini

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

Saya harap ini menjelaskannya.

chandan
sumber
Jadi dimungkinkan untuk mengkonfigurasi batasan yang dibuat dalam XCode dari kode sumber. Tetapi pertanyaannya adalah bagaimana mengkonfigurasi mereka untuk mengubah ukuran superview.
DAK
YA @ DAK. tolong pastikan tentang tata letak superview. Pasang Constraint pada superview dan buat Constraint tinggi juga sehingga setiap kali subview Anda menambahnya secara otomatis meningkatkan tinggi superview Constraint.
chandan
Ini juga bekerja untuk saya. Ini membantu bahwa saya memiliki sel berukuran tetap (tinggi 60px). Jadi ketika tampilan dimuat, saya menetapkan batasan tinggi IBOutlet'd saya menjadi 60 * x di mana x adalah jumlah sel. Pada akhirnya, Anda mungkin menginginkan ini dalam tampilan gulir sehingga Anda dapat melihat semuanya.
Sandy Chapman
-1

Ini dapat dilakukan untuk yang normal subviewdi dalam yang lebih besar UIView, tetapi tidak bekerja secara otomatis untuk headerViews. Ketinggian dari headerViewditentukan oleh apa yang dikembalikan oleh tableView:heightForHeaderInSection:sehingga Anda harus menghitung heightberdasarkan heightdari UILabelruang plus untuk UIButtondan setiap paddingAnda perlu. Anda perlu melakukan sesuatu seperti ini:

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

Berikut headerStringadalah string apa pun yang Anda inginkan untuk mengisi UILabel, dan nomor 281 adalah widthdari UILabel(seperti pengaturan di Interface Builder)

rdelmar
sumber
Sayangnya ini tidak berhasil. “Ukuran untuk menyesuaikan konten” pada superview menghilangkan kendala yang menghubungkan bagian bawah tombol ke superview seperti yang telah Anda prediksi. Saat runtime, label diubah ukurannya dan menekan tombol ke bawah tetapi superview tidak secara otomatis diubah ukurannya.
DAK
@ DAK, maaf, masalahnya adalah tampilan Anda adalah tajuk untuk tampilan tabel. Saya salah mengerti situasi Anda. Saya berpikir bahwa tampilan yang melampirkan tombol dan label ada di dalam tampilan lain, tetapi sepertinya Anda menggunakannya sebagai tajuk (daripada menjadi tampilan di dalam tajuk). Jadi, jawaban awal saya tidak akan berfungsi. Saya telah mengubah jawaban saya untuk apa yang menurut saya harus berhasil.
rdelmar
ini mirip dengan implementasi saya saat ini tetapi saya berharap ada cara yang lebih baik. Saya lebih suka menghindari memperbarui kode ini setiap kali saya mengubah header saya.
DAK
@ Dak, Maaf, saya tidak berpikir ada cara yang lebih baik, itu hanya cara tampilan tabel bekerja.
rdelmar
Tolong lihat jawaban saya. "Cara yang lebih baik" adalah dengan menggunakan UIView systemLayoutSizeFittingSize :. Yang mengatakan, melakukan autolayout penuh pada tampilan atau sel hanya untuk mendapatkan ketinggian yang diperlukan dalam tampilan tabel cukup mahal. Saya akan melakukannya untuk sel yang kompleks tetapi mungkin mundur ke solusi seperti milik Anda untuk kasus yang lebih sederhana.
TomSwift