AutoLayout dengan UIViews tersembunyi?

164

Saya merasa ini adalah paradigma yang cukup umum untuk ditunjukkan / sembunyikan UIViews, paling sering UILabels, tergantung pada logika bisnis. Pertanyaan saya adalah, apa cara terbaik menggunakan AutoLayout untuk menanggapi tampilan tersembunyi seolah-olah bingkai mereka 0x0. Berikut adalah contoh daftar dinamis 1-3 fitur.

Daftar fitur dinamis

Saat ini saya memiliki ruang atas 10px dari tombol ke label terakhir, yang jelas tidak akan geser ke atas ketika label disembunyikan. Sampai sekarang saya membuat outlet untuk batasan ini dan memodifikasi konstanta tergantung pada berapa banyak label yang saya tampilkan. Ini jelas agak membingungkan karena saya menggunakan nilai konstan negatif untuk menekan tombol di atas frame tersembunyi. Ini juga buruk karena tidak dibatasi oleh elemen tata letak yang sebenarnya, hanya perhitungan statis yang licik berdasarkan ketinggian / padding elemen lainnya yang diketahui, dan jelas berjuang melawan untuk apa AutoLayout dibuat.

Saya jelas bisa saja membuat batasan baru tergantung pada label dinamis saya, tapi itu banyak manajemen mikro dan banyak kata-kata untuk mencoba hanya menghancurkan beberapa spasi putih. Apakah ada pendekatan yang lebih baik? Mengubah ukuran bingkai 0,0 dan membiarkan AutoLayout melakukan tugasnya tanpa manipulasi kendala? Menghapus tampilan sepenuhnya?

Jujur saja, hanya mengubah konstanta dari konteks tampilan tersembunyi memerlukan satu baris kode dengan perhitungan sederhana. Menciptakan kendala baru dengan constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:tampaknya begitu berat.

Sunting Feb 2018 : Lihat jawaban Ben dengan UIStackViews

Ryan Romanchuk
sumber
Terima kasih Ryan untuk pertanyaan ini. Saya akan gila apa yang harus dilakukan untuk kasus-kasus seperti yang Anda minta. Setiap kali saya memeriksa tutorial untuk autolayout, kebanyakan dari mereka mengatakan merujuk ke situs tutorial raywenderlich yang saya temukan agak sulit untuk membuatnya.
Nassif

Jawaban:

82

UIStackViewmungkin cara untuk iOS 9+. Tidak hanya menangani tampilan tersembunyi, itu juga akan menghapus spasi dan margin tambahan jika diatur dengan benar.

Ben Packard
sumber
8
Hati-hati saat menggunakan UIStackView di UITableViewCell. Bagi saya, begitu pandangan menjadi sangat rumit, pengguliran mulai menjadi berombak di mana ia akan gagap setiap kali sel digulirkan ke / keluar. Di bawah penutup, StackView menambah / menghilangkan kendala dan ini tidak harus cukup efisien untuk pengguliran yang lancar.
Kento
7
Akan menyenangkan untuk memberikan contoh kecil tentang bagaimana tampilan stack menangani ini.
lostintranslation
@Kento apakah ini masih menjadi masalah di iOS 10?
Crashalot
3
Lima tahun kemudian ... haruskah saya memperbarui dan menetapkan ini sebagai jawaban yang diterima? Sudah tersedia untuk tiga siklus rilis sekarang.
Ryan Romanchuk
1
@RyanRomanchuk Anda harus, ini pasti jawaban terbaik sekarang. Ben terima kasih!
Duncan Luk
234

Preferensi pribadi saya untuk menampilkan / menyembunyikan tampilan adalah membuat IBOutlet dengan lebar atau batas ketinggian yang sesuai.

Saya kemudian memperbarui constantnilai untuk 0disembunyikan, atau nilai apa pun yang harus ditampilkan.

Keuntungan besar dari teknik ini adalah bahwa kendala relatif akan dipertahankan. Misalnya, Anda memiliki tampilan A dan tampilan B dengan celah horizontal x . Ketika tampilan A lebar constantdiatur ke 0.fkemudian tampilan B akan bergerak ke kiri untuk mengisi ruang itu.

Tidak perlu menambah atau menghilangkan kendala, yang merupakan operasi kelas berat. Cukup memperbarui kendala constantakan melakukan trik.

Max MacLeod
sumber
1
persis. Selama - dalam contoh ini - Anda memposisikan tampilan B di sebelah kanan tampilan A menggunakan celah horizontal. Saya menggunakan teknik ini sepanjang waktu dan ini bekerja dengan sangat baik
Max MacLeod
9
@MaxMacLeod Hanya untuk memastikan: jika jarak antara dua tampilan adalah x, agar tampilan B dimulai dari posisi A, kita juga harus mengubah konstanta batasan celah menjadi 0, juga, kan?
kernix
1
@kernix ya jika perlu ada batasan gap horizontal antara dua tampilan. konstanta 0 tentu saja berarti tidak ada celah. Jadi menyembunyikan tampilan A dengan mengatur lebarnya ke 0 akan memindahkan tampilan B ke kiri untuk menyiram wadah. Btw terima kasih atas penilaiannya!
Max MacLeod
2
@ MaxMacLeod Terima kasih atas jawaban Anda. Saya mencoba melakukan ini dengan UIView yang berisi tampilan lain, tetapi subview tampilan itu tidak disembunyikan ketika saya menetapkan batasan tingginya menjadi 0. Apakah solusi ini seharusnya bekerja dengan view yang mengandung subview? Terima kasih!
shaunlim
6
Juga akan menghargai solusi yang bekerja untuk UIView yang mengandung pandangan lain ...
Mark Gibaud
96

Solusi menggunakan konstanta 0ketika disembunyikan dan konstanta lain jika Anda menampilkannya lagi fungsional, tetapi tidak memuaskan jika konten Anda memiliki ukuran yang fleksibel. Anda perlu mengukur konten fleksibel Anda dan menetapkan kembali konstan. Ini terasa salah, dan memiliki masalah jika konten berubah ukuran karena kejadian server atau UI.

Saya punya solusi yang lebih baik.

Idenya adalah untuk menetapkan aturan ketinggian 0 untuk memiliki prioritas tinggi ketika kita menyembunyikan elemen sehingga tidak memakan ruang autolayout.

Begini cara Anda melakukannya:

1. mengatur lebar (atau tinggi) dari 0 di pembangun antarmuka dengan prioritas rendah.

pengaturan lebar 0 aturan

Pembuat Antarmuka tidak akan berteriak tentang konflik karena prioritasnya rendah. Uji perilaku ketinggian dengan menetapkan prioritas ke 999 sementara (1000 dilarang untuk bermutasi secara program, jadi kami tidak akan menggunakannya). Pembuat antarmuka mungkin sekarang akan berteriak tentang kendala yang saling bertentangan. Anda dapat memperbaikinya dengan menetapkan prioritas pada objek terkait hingga 900 atau lebih.

2. Tambahkan outlet sehingga Anda dapat mengubah prioritas batasan lebar dalam kode:

koneksi outlet

3. Sesuaikan prioritas ketika Anda menyembunyikan elemen Anda:

cell.alertTimingView.hidden    = place.closingSoon != true
cell.alertTimingWidth.priority = place.closingSoon == true ? 250 : 999
SimplGy
sumber
@SimplGy, bisakah Anda memberi tahu saya tempat apa ini?
Niharika
Ini bukan bagian dari solusi umum, @Niharika, Ini hanya aturan bisnis dalam kasus saya (tunjukkan tampilan jika restoran tutup / tidak segera tutup).
SimplGy
11

Dalam hal ini, saya memetakan ketinggian label Penulis ke IBOutlet yang sesuai:

@property (retain, nonatomic) IBOutlet NSLayoutConstraint* authorLabelHeight;

dan ketika saya mengatur ketinggian kendala menjadi 0,0f, kami mempertahankan "padding", karena ketinggian tombol Play memungkinkan untuk itu.

cell.authorLabelHeight.constant = 0; //Hide 
cell.authorLabelHeight.constant = 44; //Show

masukkan deskripsi gambar di sini

Mitul Marsoniya
sumber
9

Ada banyak solusi di sini tetapi pendekatan saya yang biasa berbeda lagi :)

Siapkan dua set kendala yang mirip dengan jawaban Jorge Arimany dan TMin:

masukkan deskripsi gambar di sini

Ketiga batasan yang ditandai memiliki nilai yang sama untuk Constant. Batasan yang ditandai A1 dan A2 memiliki Prioritasnya diatur ke 500, sedangkan kendala yang ditandai B menjadikan Prioritasnya diatur ke 250 (atau UILayoutProperty.defaultLowdalam kode).

Hubungkan kendala B ke IBOutlet. Kemudian, ketika Anda menyembunyikan elemen, Anda hanya perlu mengatur prioritas kendala ke tinggi (750):

constraintB.priority = .defaultHigh

Tetapi ketika elemen terlihat, atur prioritas kembali ke rendah (250):

constraintB.priority = .defaultLow

Keuntungan (diakui minor) untuk pendekatan ini dari hanya mengubah isActiveuntuk kendala B adalah bahwa Anda masih memiliki kendala kerja jika elemen transien dihapus dari tampilan dengan cara lain.

SeanR
sumber
Ini memiliki keuntungan karena tidak memiliki nilai duplikat di Storyboard dan kode seperti tinggi / lebar elemen. Sangat elegan.
Robin Daugherty
Terima kasih!! Itu membantu saya
Parth Barot
Pendekatan Luar Biasa, Terima kasih
Basil
5

Subkelas tampilan dan timpa func intrinsicContentSize() -> CGSize. Kembali saja CGSizeZerojika tampilan disembunyikan.

Daniel
sumber
2
Ini bagus. Beberapa elemen UI membutuhkan pemicu invalidateIntrinsicContentSize. Anda dapat melakukannya secara override setHidden.
DrMickeyLauer
5

Saya baru tahu bahwa untuk mendapatkan UILabel agar tidak memakan tempat, Anda harus menyembunyikannya DAN mengatur teksnya ke string kosong. (iOS 9)

Mengetahui fakta / bug ini dapat membantu beberapa orang menyederhanakan layout mereka, bahkan mungkin dari pertanyaan awal, jadi saya pikir saya akan mempostingnya.

Matt Koala
sumber
1
Saya tidak berpikir bahwa Anda perlu menyembunyikannya. Mengatur teksnya ke string kosong sudah cukup.
Rob VS
4

Saya terkejut bahwa tidak ada pendekatan yang lebih elegan yang disediakan oleh UIKit untuk perilaku yang diinginkan ini. Sepertinya hal yang sangat umum untuk dapat dilakukan.

Karena menghubungkan kendala ke IBOutletsdan menyetel konstanta mereka menjadi 0yucky (dan menyebabkan NSLayoutConstraintperingatan ketika tampilan Anda memiliki subview), saya memutuskan untuk membuat ekstensi yang memberikan pendekatan sederhana, stateful untuk menyembunyikan / menampilkan suatu UIViewyang memiliki kendala Tata Letak Otomatis

Itu hanya menyembunyikan pandangan dan menghilangkan kendala eksterior. Ketika Anda menunjukkan tampilan lagi, itu menambah kendala kembali. Satu-satunya peringatan adalah bahwa Anda harus menentukan batasan failover fleksibel untuk tampilan sekitarnya.

Sunting Jawaban ini ditargetkan untuk iOS 8.4 dan di bawahnya. Di iOS 9, gunakan saja UIStackViewpendekatannya.

Albert Bori
sumber
1
Ini juga pendekatan yang saya sukai. Ini adalah peningkatan signifikan pada jawaban lain di sini karena tidak hanya memencet tampilan ke ukuran nol. Pendekatan squash akan mengetuk masalah untuk apa pun kecuali pembayaran yang cukup sepele. Contoh dari ini adalah kesenjangan besar di mana item yang tersembunyi di jawaban lain ini . Dan Anda bisa berakhir dengan pelanggaran batasan seperti yang disebutkan.
Benjohn
@DanielSchlaug Terima kasih telah menunjukkannya. Saya telah memperbarui lisensi untuk MIT. Namun, saya memang memerlukan mental lima tinggi setiap kali seseorang mengimplementasikan solusi ini.
Albert Bori
4

Praktik terbaik adalah, setelah semuanya memiliki batasan tata letak yang benar, tambahkan ketinggian atau dengan kendala, tergantung bagaimana Anda ingin tampilan di sekitarnya bergerak dan menghubungkan kendala ke IBOutletproperti.

Pastikan bahwa properti Anda strong

dalam kode kamu hanya perlu mengatur konstanta ke 0 dan mengaktifkannya , untuk menyembunyikan konten, atau menonaktifkannya untuk menampilkan konten. Ini lebih baik daripada mengacaukan nilai konstan tabungan-memulihkannya. Jangan lupa menelepon layoutIfNeededsesudahnya.

Jika konten yang akan disembunyikan dikelompokkan, praktik terbaik adalah menempatkan semua ke tampilan dan menambahkan kendala ke tampilan itu

@property (strong, nonatomic) IBOutlet UIView *myContainer;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *myContainerHeight; //should be strong!!

-(void) showContainer
{
    self.myContainerHeight.active = NO;
    self.myContainer.hidden = NO;
    [self.view layoutIfNeeded];
}
-(void) hideContainer
{
    self.myContainerHeight.active = YES;
    self.myContainerHeight.constant = 0.0f;
    self.myContainer.hidden = YES;
    [self.view layoutIfNeeded];
}

Setelah Anda mengatur Anda, Anda dapat mengujinya di IntefaceBuilder dengan menetapkan batasan Anda ke 0 dan kemudian kembali ke nilai aslinya. Jangan lupa untuk memeriksa prioritas kendala lainnya sehingga ketika disembunyikan tidak ada konflik sama sekali. Cara lain untuk mengujinya adalah dengan meletakkannya ke 0 dan mengatur prioritas ke 0, tetapi, jangan lupa untuk mengembalikannya ke prioritas tertinggi lagi.

Jorge Arimany
sumber
1
Itu karena batasan dan tampilan tidak selalu dapat dipertahankan oleh hierarki tampilan, jadi jika Anda menonaktifkan batasan dan menyembunyikan tampilan, Anda masih mempertahankannya untuk menampilkannya kembali terlepas dari apa yang diputuskan untuk dipertahankan atau tidak oleh hierarki tampilan Anda.
Jorge Arimany
2

Metode pilihan saya sangat mirip dengan yang disarankan oleh Jorge Arimany.

Saya lebih suka membuat banyak kendala. Pertama buat batasan Anda ketika label kedua terlihat. Buat outlet untuk batasan antara tombol dan label ke-2 (jika Anda menggunakan objc pastikan kuatnya). Batasan ini menentukan ketinggian antara tombol dan label kedua saat terlihat.

Kemudian buat batasan lain yang menentukan ketinggian antara tombol dan label atas ketika tombol kedua disembunyikan. Buat outlet ke batasan kedua dan pastikan outlet ini memiliki pointer yang kuat. Kemudian hapus centang padainstalled centang pada kotak centang di pembangun antarmuka, dan pastikan prioritas kendala pertama lebih rendah dari prioritas kendala kedua ini.

Akhirnya ketika Anda menyembunyikan label kedua, beralih .isActive properti dari kendala dan panggilan inisetNeedsDisplay()

Dan itu saja, tidak ada angka Ajaib, tidak ada matematika, jika Anda memiliki beberapa kendala untuk menghidupkan dan mematikan Anda bahkan dapat menggunakan koleksi outlet untuk menjaga mereka diatur oleh negara. (AKA menyimpan semua kendala tersembunyi di satu OutletCollection dan yang tidak tersembunyi di yang lain dan hanya mengulangi setiap koleksi untuk mengubah status .isActive mereka).

Saya tahu Ryan Romanchuk mengatakan dia tidak ingin menggunakan banyak kendala, tapi saya merasa ini bukan manajemen mikro-y, dan lebih sederhana yang secara dinamis menciptakan pandangan dan kendala secara terprogram (yang menurut saya ingin dia hindari jika saya sedang membaca pertanyaan dengan benar).

Saya telah membuat contoh sederhana, semoga bermanfaat ...

import UIKit

class ViewController: UIViewController {

    @IBOutlet var ToBeHiddenLabel: UILabel!


    @IBOutlet var hiddenConstraint: NSLayoutConstraint!
    @IBOutlet var notHiddenConstraint: NSLayoutConstraint!


    @IBAction func HideMiddleButton(_ sender: Any) {

        ToBeHiddenLabel.isHidden = !ToBeHiddenLabel.isHidden
        notHiddenConstraint.isActive = !notHiddenConstraint.isActive
        hiddenConstraint.isActive = !hiddenConstraint.isActive

        self.view.setNeedsDisplay()
    }
}

masukkan deskripsi gambar di sini

TMin
sumber
0

Saya akan memberikan solusi saya juga, untuk menawarkan variasi.) Saya pikir membuat outlet untuk lebar / tinggi masing-masing item plus untuk jarak hanya konyol dan meledakkan kode, kemungkinan kesalahan dan jumlah komplikasi.

Metode saya menghapus semua tampilan (dalam kasus saya, contoh UIImageView), memilih mana yang perlu ditambahkan kembali, dan dalam satu lingkaran, itu menambahkan kembali masing-masing dan menciptakan kendala baru. Ini sebenarnya sangat sederhana, harap tindak lanjuti. Ini kode cepat dan kotor saya untuk melakukannya:

// remove all views
[self.twitterImageView removeFromSuperview];
[self.localQuestionImageView removeFromSuperview];

// self.recipients always has to be present
NSMutableArray *items;
items = [@[self.recipients] mutableCopy];

// optionally add the twitter image
if (self.question.sharedOnTwitter.boolValue) {
    [items addObject:self.twitterImageView];
}

// optionally add the location image
if (self.question.isLocal) {
    [items addObject:self.localQuestionImageView];
}

UIView *previousItem;
UIView *currentItem;

previousItem = items[0];
[self.contentView addSubview:previousItem];

// now loop through, add the items and the constraints
for (int i = 1; i < items.count; i++) {
    previousItem = items[i - 1];
    currentItem = items[i];

    [self.contentView addSubview:currentItem];

    [currentItem mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.centerY.equalTo(previousItem.mas_centerY);
        make.right.equalTo(previousItem.mas_left).offset(-5);
    }];
}


// here I just connect the left-most UILabel to the last UIView in the list, whichever that was
previousItem = items.lastObject;

[self.userName mas_remakeConstraints:^(MASConstraintMaker *make) {
    make.right.equalTo(previousItem.mas_left);
    make.leading.equalTo(self.text.mas_leading);
    make.centerY.equalTo(self.attachmentIndicator.mas_centerY);;
}];

Saya mendapatkan tata letak dan spasi yang bersih dan konsisten. Kode saya menggunakan Masonry, saya sangat merekomendasikannya: https://github.com/SnapKit/Masonry

Zoltán
sumber
0

Coba BoxView , itu membuat tata letak dinamis ringkas dan mudah dibaca.
Dalam kasus Anda itu adalah:

    boxView.optItems = [
        firstLabel.boxed.useIf(isFirstLabelShown),
        secondLabel.boxed.useIf(isSecondLabelShown),
        button.boxed
    ]
Vladimir
sumber