Apa hubungan antara setNeedsLayout, layoutIfNeeded, dan layoutSubviews UIView?

105

Adakah yang bisa memberikan penjelasan definitif tentang hubungan antara UIView's setNeedsLayout, layoutIfNeededdan layoutSubviewsmetode? Dan contoh implementasi dimana ketiganya akan digunakan. Terima kasih.

Yang membuat saya bingung adalah jika saya mengirim setNeedsLayoutpesan ke tampilan kustom saya, hal berikutnya yang dipanggil setelah metode ini adalah layoutSubviews, melompati langsung layoutIfNeeded. Dari dokumen, saya berharap alirannya menjadi setNeedsLayout> sebab yang layoutIfNeededakan dipanggil> sebab layoutSubviewsharus dipanggil.

Tarfa
sumber

Jawaban:

104

Saya masih mencoba untuk mencari tahu sendiri, jadi ambillah ini dengan sedikit skeptis dan maafkan saya jika ada kesalahan.

setNeedsLayoutmudah: ia hanya menyetel flag di suatu tempat di UIView yang menandainya sebagai memerlukan tata letak. Itu akan memaksa layoutSubviewsuntuk dipanggil pada tampilan sebelum penggambaran ulang berikutnya terjadi. Perhatikan bahwa dalam banyak kasus Anda tidak perlu memanggil ini secara eksplisit, karena autoresizesSubviewssifatnya. Jika itu disetel (yang secara default) maka setiap perubahan pada bingkai tampilan akan menyebabkan tampilan untuk menyusun subviewnya.

layoutSubviewsadalah metode di mana Anda melakukan semua hal menarik. Ini setara dengan drawRectuntuk tata letak, jika Anda mau. Contoh sepele mungkin:

-(void)layoutSubviews {
    // Child's frame is always equal to our bounds inset by 8px
    self.subview1.frame = CGRectInset(self.bounds, 8.0, 8.0);
    // It seems likely that this is incorrect:
    // [self.subview1 layoutSubviews];
    // ... and this is correct:
    [self.subview1 setNeedsLayout];
    // but I don't claim to know definitively.
}

AFAIK layoutIfNeededumumnya tidak dimaksudkan untuk diganti di subkelas Anda. Ini adalah metode yang seharusnya Anda panggil saat Anda ingin tampilan ditata sekarang . Implementasi Apple mungkin terlihat seperti ini:

-(void)layoutIfNeeded {
    if (self._needsLayout) {
        UIView *sv = self.superview;
        if (sv._needsLayout) {
            [sv layoutIfNeeded];
        } else {
            [self layoutSubviews];
        }
    }
}

Anda akan meminta layoutIfNeededtampilan untuk memaksanya (dan tampilan supernya jika diperlukan) untuk segera ditata.

n8gray.dll
sumber
2
Terima kasih - senang akhirnya seseorang menjawab ini. Sementara itu, saya juga harus mempelajari setNeedsDisplay - yang menyebabkan drawRect dipanggil - dan contentMode. Hanya contentMode = UIViewContentModeRedraw yang akan menyebabkan setNeedsDisplay dipanggil saat batas tampilan berubah. Pilihan contentMode lainnya hanya menyebabkan tampilan diskalakan, bergeser dengan jumlah tertentu, atau disejajarkan ke tepi. UITableViewCell kustom saya tidak ingin menggambar ulang pada perubahan orientasi, itu hanya akan menskalakan, sampai saya menyetel contentMode ke UIViewContentModeRedraw.
Tarfa
Saya pikir mungkin [self.subview1 layoutSubviews] di kode Anda harus diganti dengan [self.subview1 setNeedsLayout]. Karena layoutSubviews dimaksudkan untuk diganti tetapi tidak dimaksudkan untuk dipanggil dari atau ke tampilan lain. Entah kerangka kerja menentukan kapan harus memanggilnya (dengan mengelompokkan beberapa permintaan tata letak menjadi satu panggilan untuk efisiensi) atau Anda secara tidak langsung melakukannya dengan memanggil layoutIfNeeded di suatu tempat dalam hierarki tampilan.
Tarfa
Koreksi atas komentar saya di atas: "... Karena layoutSubviews dimaksudkan untuk ditimpa atau dipanggil dari diri sendiri tetapi tidak dimaksudkan untuk dipanggil dari atau ke tampilan lain ..."
Tarfa
Saya benar-benar tidak yakin tentang [subview1 layoutSubviews]. Bisa jadi drawRect, Anda tidak seharusnya memanggilnya secara langsung. Namun saya tidak yakin apakah memanggil setNeedsLayoutselama fase tata letak akan menyebabkan tampilan ditata selama fase tata letak yang sama atau jika ditunda hingga fase berikutnya. Alangkah baiknya melihat jawaban dari seseorang yang benar - benar mengerti bagaimana semua ini bekerja ...
n8gray
34

Saya ingin menambahkan jawaban n8gray bahwa dalam beberapa kasus Anda perlu menelepon setNeedsLayoutdiikuti oleh layoutIfNeeded.

Misalnya, Anda menulis tampilan kustom yang memperluas UIView, di mana pemosisian subview rumit dan tidak dapat dilakukan dengan AutoresizingMask atau iOS6 AutoLayout. Penentuan posisi kustom dapat dilakukan dengan mengganti layoutSubviews.

Sebagai contoh, katakanlah Anda memiliki tampilan kustom yang memiliki contentViewproperti dan edgeInsetsproperti yang memungkinkan untuk menyetel margin di sekitar contentView. layoutSubviewsakan terlihat seperti ini:

- (void) layoutSubviews {
    self.contentView.frame = CGRectMake(
        self.bounds.origin.x + self.edgeInsets.left,
        self.bounds.origin.y + self.edgeInsets.top,
        self.bounds.size.width - self.edgeInsets.left - self.edgeInsets.right,
        self.bounds.size.height - self.edgeInsets.top - self.edgeInsets.bottom); 
}

Jika Anda ingin dapat menganimasikan perubahan bingkai setiap kali Anda mengubah edgeInsetsproperti, Anda perlu mengganti edgeInsetspenyetel sebagai berikut dan memanggil setNeedsLayoutdiikuti oleh layoutIfNeeded:

- (void) setEdgeInsets:(UIEdgeInsets)edgeInsets {
    _edgeInsets = edgeInsets;
    [self setNeedsLayout]; //Indicates that the view needs to be laid out 
                           //at next update or at next call of layoutIfNeeded, 
                           //whichever comes first 
    [self layoutIfNeeded]; //Calls layoutSubviews if flag is set
}

Dengan demikian, jika Anda melakukan hal berikut, jika Anda mengubah properti edgeInsets di dalam blok animasi, perubahan bingkai dari contentView akan dianimasikan.

[UIView animateWithDuration:2 animations:^{
    customView.edgeInsets = UIEdgeInsetsMake(45, 17, 18, 34);
}];

Jika Anda tidak menambahkan panggilan ke layoutIfNeeded dalam metode setEdgeInsets, animasi tidak akan berfungsi karena layoutSubviews akan dipanggil pada siklus pembaruan berikutnya, yang sama dengan memanggilnya di luar blok animasi.

Jika Anda hanya memanggil layoutIfNeeded dalam metode setEdgeInsets, tidak akan ada yang terjadi karena tanda setNeedsLayout tidak disetel.

quentinadam
sumber
4
Atau Anda hanya dapat memanggil [self layoutIfNeeded]dalam blok animasi setelah mengatur sisipan tepi.
Scott Fister