Menentukan satu Dimensi Sel di UICollectionView menggunakan Tata Letak Otomatis

113

Di iOS 8, UICollectionViewFlowLayoutdukungan secara otomatis mengubah ukuran sel berdasarkan ukuran kontennya sendiri. Ini mengubah ukuran sel dalam lebar dan tinggi sesuai dengan isinya.

Apakah mungkin untuk menentukan nilai tetap untuk lebar (atau tinggi) dari semua sel dan mengizinkan dimensi lain untuk mengubah ukurannya?

Untuk contoh sederhana pertimbangkan label multi-baris dalam sel dengan batasan yang memposisikannya ke sisi sel. Label multi-baris dapat diubah ukurannya dengan cara yang berbeda untuk mengakomodasi teks. Sel harus memenuhi lebar tampilan koleksi dan menyesuaikan tingginya. Sebaliknya, sel-sel tersebut berukuran sembarangan dan bahkan menyebabkan crash ketika ukuran sel lebih besar dari dimensi tampilan koleksi yang tidak dapat digulir.


iOS 8 memperkenalkan metode systemLayoutSizeFittingSize: withHorizontalFittingPriority: verticalFittingPriority:Untuk setiap sel dalam tampilan koleksi, tata letak memanggil metode ini pada sel, dengan meneruskan ukuran yang diperkirakan. Apa yang masuk akal bagi saya adalah mengganti metode ini pada sel, meneruskan ukuran yang diberikan dan menyetel batasan horizontal menjadi diperlukan dan prioritas rendah ke batasan vertikal. Dengan cara ini ukuran horizontal ditetapkan ke nilai yang ditetapkan dalam tata letak dan ukuran vertikal bisa fleksibel.

Sesuatu seperti ini:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    UICollectionViewLayoutAttributes *attributes = [super preferredLayoutAttributesFittingAttributes:layoutAttributes];
    attributes.size = [self systemLayoutSizeFittingSize:layoutAttributes.size withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    return attributes;
}

Namun, ukuran yang diberikan kembali oleh metode ini benar-benar aneh. Dokumentasi tentang metode ini sangat tidak jelas bagi saya dan menyebutkan menggunakan konstantaUILayoutFittingCompressedSize UILayoutFittingExpandedSize yang hanya mewakili ukuran nol dan ukuran yang cukup besar.

Adalah size parameter metode ini benar-benar hanya cara untuk melewatkan dua konstanta? Apakah tidak ada cara untuk mencapai perilaku yang saya harapkan untuk mendapatkan ketinggian yang sesuai untuk ukuran tertentu?


Solusi Alternatif

1) Menambahkan batasan yang akan menentukan lebar tertentu untuk sel mencapai tata letak yang benar. Ini adalah solusi yang buruk karena batasan tersebut harus disetel ke ukuran tampilan kumpulan sel yang tidak memiliki referensi aman. Nilai untuk batasan itu bisa diteruskan ketika sel dikonfigurasi, tetapi itu juga tampak sangat berlawanan dengan intuisi. Ini juga janggal karena menambahkan batasan langsung ke sel atau tampilan kontennya menyebabkan banyak masalah.

2) Gunakan tampilan tabel. Tampilan tabel berfungsi seperti ini di luar kotak karena sel memiliki lebar tetap, tetapi ini tidak mengakomodasi situasi lain seperti tata letak iPad dengan sel lebar tetap di beberapa kolom.

Anthony Mattox
sumber

Jawaban:

135

Sepertinya yang Anda minta adalah cara menggunakan UICollectionView untuk menghasilkan layout seperti UITableView. Jika itu yang Anda inginkan, cara yang tepat untuk melakukannya adalah dengan subkelas UICollectionViewLayout kustom (mungkin sesuatu seperti SBTableLayout ).

Di sisi lain, jika Anda benar-benar bertanya apakah ada cara yang bersih untuk melakukan ini dengan UICollectionViewFlowLayout default, maka saya yakin tidak mungkin. Bahkan dengan sel self-sizing iOS8, itu tidak langsung. Masalah mendasar, seperti yang Anda katakan, adalah bahwa mesin tata letak aliran tidak menyediakan cara untuk memperbaiki satu dimensi dan membiarkan dimensi lain merespons. (Selain itu, meskipun Anda bisa, akan ada kerumitan tambahan seputar perlunya dua penerusan tata letak untuk mengukur label multi-baris. Ini mungkin tidak sesuai dengan cara sel yang mengukur sendiri ingin menghitung semua ukuran melalui satu panggilan ke systemLayoutSizeFittingSize.)

Namun, jika Anda masih ingin membuat tata letak seperti tampilan tabel dengan tata letak aliran, dengan sel yang menentukan ukurannya sendiri, dan merespons secara alami lebar tampilan koleksi, tentu saja hal itu dimungkinkan. Masih ada cara yang berantakan. Saya telah melakukannya dengan "sel ukuran", yaitu UICollectionViewCell yang tidak ditampilkan yang disimpan oleh pengontrol hanya untuk menghitung ukuran sel.

Ada dua bagian dalam pendekatan ini. Bagian pertama adalah untuk delegasi tampilan koleksi untuk menghitung ukuran sel yang benar, dengan mengambil lebar tampilan koleksi dan menggunakan sel ukuran untuk menghitung tinggi sel.

Di UICollectionViewDelegateFlowLayout Anda, Anda menerapkan metode seperti ini:

func collectionView(collectionView: UICollectionView,
  layout collectionViewLayout: UICollectionViewLayout,
  sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
  // NOTE: here is where we say we want cells to use the width of the collection view
   let requiredWidth = collectionView.bounds.size.width

   // NOTE: here is where we ask our sizing cell to compute what height it needs
  let targetSize = CGSize(width: requiredWidth, height: 0)
  /// NOTE: populate the sizing cell's contents so it can compute accurately
  self.sizingCell.label.text = items[indexPath.row]
  let adequateSize = self.sizingCell.preferredLayoutSizeFittingSize(targetSize)
  return adequateSize
}

Ini akan menyebabkan tampilan koleksi menyetel lebar sel berdasarkan tampilan koleksi terlampir, tetapi kemudian meminta sel ukuran untuk menghitung tinggi.

Bagian kedua adalah mendapatkan sel ukuran untuk menggunakan batasan AL-nya sendiri untuk menghitung ketinggian. Ini bisa lebih sulit dari yang seharusnya, karena cara UILabel multi-baris secara efektif membutuhkan proses tata letak dua tahap. Pekerjaan dilakukan dengan metode preferredLayoutSizeFittingSize, yang seperti ini:

 /*
 Computes the size the cell will need to be to fit within targetSize.

 targetSize should be used to pass in a width.

 the returned size will have the same width, and the height which is
 calculated by Auto Layout so that the contents of the cell (i.e., text in the label)
 can fit within that width.

 */
 func preferredLayoutSizeFittingSize(targetSize:CGSize) -> CGSize {

   // save original frame and preferredMaxLayoutWidth
   let originalFrame = self.frame
   let originalPreferredMaxLayoutWidth = self.label.preferredMaxLayoutWidth

   // assert: targetSize.width has the required width of the cell

   // step1: set the cell.frame to use that width
   var frame = self.frame
   frame.size = targetSize
   self.frame = frame

   // step2: layout the cell
   self.setNeedsLayout()
   self.layoutIfNeeded()
   self.label.preferredMaxLayoutWidth = self.label.bounds.size.width

   // assert: the label's bounds and preferredMaxLayoutWidth are set to the width required by the cell's width

   // step3: compute how tall the cell needs to be

   // this causes the cell to compute the height it needs, which it does by asking the 
   // label what height it needs to wrap within its current bounds (which we just set).
   let computedSize = self.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)

   // assert: computedSize has the needed height for the cell

   // Apple: "Only consider the height for cells, because the contentView isn't anchored correctly sometimes."
   let newSize = CGSize(width:targetSize.width,height:computedSize.height)

   // restore old frame and preferredMaxLayoutWidth
   self.frame = originalFrame
   self.label.preferredMaxLayoutWidth = originalPreferredMaxLayoutWidth

   return newSize
 }

(Kode ini diadaptasi dari kode contoh Apple dari kode contoh sesi WWDC2014 pada "Tampilan Koleksi Lanjutan".)

Beberapa hal yang perlu diperhatikan. Ini menggunakan layoutIfNeeded () untuk memaksa tata letak seluruh sel, untuk menghitung dan menyetel lebar label. Tapi itu belum cukup. Saya yakin Anda juga perlu mengatur preferredMaxLayoutWidthagar label akan menggunakan lebar itu dengan Tata Letak Otomatis. Dan hanya dengan begitu Anda dapat menggunakansystemLayoutSizeFittingSize agar sel menghitung tingginya sambil mempertimbangkan label.

Apakah saya menyukai pendekatan ini? Tidak!! Rasanya terlalu rumit, dan tata letak dua kali. Namun selama kinerja tidak menjadi masalah, saya lebih suka melakukan tata letak dua kali pada waktu proses daripada harus mendefinisikannya dua kali dalam kode, yang tampaknya menjadi satu-satunya alternatif lain.

Harapan saya adalah bahwa pada akhirnya sel yang mengukur sendiri akan bekerja secara berbeda dan ini semua akan menjadi jauh lebih sederhana.

Contoh proyek yang menunjukkannya di tempat kerja.

Tapi mengapa tidak menggunakan sel yang mengukur sendiri?

Secara teori, fasilitas baru iOS8 untuk "sel yang mengukur sendiri" seharusnya membuat hal ini tidak perlu. Jika Anda telah menentukan sel dengan Tata Letak Otomatis (AL), tampilan koleksi harus cukup pintar untuk membiarkannya mengukur sendiri dan menata dirinya sendiri dengan benar. Dalam praktiknya, saya belum melihat contoh apa pun yang membuat ini berfungsi dengan label multi-garis. Saya pikir ini sebagian karena mekanisme sel yang mengukur sendiri masih bermasalah.

Tapi saya berani bertaruh itu sebagian besar karena kerumitan Tata Letak Otomatis dan label yang biasa, yaitu UILabel pada dasarnya memerlukan proses tata letak dua langkah. Tidak jelas bagi saya bagaimana Anda dapat melakukan kedua langkah dengan sel yang mengukur sendiri.

Dan seperti yang saya katakan, ini benar-benar pekerjaan untuk tata letak yang berbeda. Ini adalah bagian dari esensi tata letak aliran yang memposisikan benda-benda yang memiliki ukuran, daripada memperbaiki lebar dan membiarkan mereka memilih tingginya.

Dan bagaimana dengan preferLayoutAttributesFittingAttributes:?

The preferredLayoutAttributesFittingAttributes:Metode adalah ikan merah, saya pikir. Itu hanya untuk digunakan dengan mekanisme sel ukuran sendiri yang baru. Jadi ini bukan jawabannya selama mekanismenya tidak bisa diandalkan.

Dan ada apa dengan systemlayoutSizeFittingSize :?

Anda benar, dokumen membingungkan.

Dokumen di systemLayoutSizeFittingSize:dan systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority:keduanya menyarankan agar Anda hanya meneruskan UILayoutFittingCompressedSizedan UILayoutFittingExpandedSizesebagai filetargetSize . Namun, tanda tangan metode itu sendiri, komentar header, dan perilaku fungsi menunjukkan bahwa mereka merespons nilai targetSizeparameter yang tepat.

Faktanya, jika Anda menyetel UICollectionViewFlowLayoutDelegate.estimatedItemSize, untuk mengaktifkan mekanisme sel self-sizing baru, nilai tersebut tampaknya diteruskan sebagai targetSize. Dan UILabel.systemLayoutSizeFittingSizetampaknya mengembalikan nilai yang sama persis seperti UILabel.sizeThatFits. Hal ini mencurigakan, mengingat dalil ke systemLayoutSizeFittingSizeseharusnya menjadi sasaran kasar dan dalil untuksizeThatFits: seharusnya menjadi ukuran yang membatasi maksimum.

Sumber Daya Lainnya

Meskipun menyedihkan untuk berpikir bahwa persyaratan rutin seperti itu harus membutuhkan "sumber daya penelitian", saya rasa memang demikian. Contoh dan diskusi yang bagus adalah:

alga
sumber
Mengapa Anda menyetel bingkai kembali ke bingkai lama?
vrwim
Saya melakukannya agar fungsi hanya dapat digunakan untuk menentukan ukuran tata letak tampilan yang disukai, tanpa memengaruhi status tata letak tampilan tempat Anda memanggilnya. Secara praktis, ini tidak relevan jika Anda memegang tampilan ini hanya untuk tujuan melakukan perhitungan tata letak. Juga, apa yang saya lakukan di sini tidak cukup. Memulihkan bingkai saja tidak benar-benar memulihkannya ke keadaan sebelumnya, karena melakukan tata letak mungkin telah mengubah tata letak sub-tampilan.
alga
1
Ini benar-benar berantakan untuk sesuatu yang telah dilakukan orang-orang sejak iOS6. Apple selalu keluar dengan beberapa cara tetapi tidak pernah benar.
GregP
1
Anda dapat secara manual memberi contoh sel pengatur ukuran di viewDidLoad dan menahannya untuk melihat properti khusus. Itu hanya satu sel jadi itu murah. Karena Anda hanya menggunakannya untuk menentukan ukuran, dan Anda mengelola semua interaksi dengannya, fitur ini tidak perlu berpartisipasi dalam mekanisme penggunaan kembali sel tampilan koleksi, jadi Anda tidak perlu mendapatkannya dari tampilan koleksi itu sendiri.
alga
1
Bagaimana mungkin sel yang mengukur diri sendiri masih bisa rusak ?!
Ruben Scratton
50

Ada cara yang lebih bersih untuk melakukan ini daripada beberapa jawaban lain di sini, dan itu bekerja dengan baik. Ini harus berkinerja baik (tampilan koleksi dimuat dengan cepat, tidak ada tata letak otomatis yang tidak perlu, dll.), Dan tidak memiliki 'angka ajaib' seperti lebar tampilan koleksi yang tetap. Mengubah ukuran tampilan koleksi, misalnya pada rotasi, dan kemudian membatalkan tata letak juga akan bekerja dengan baik.

1. Buat subclass tata letak aliran berikut

class HorizontallyFlushCollectionViewFlowLayout: UICollectionViewFlowLayout {

    // Don't forget to use this class in your storyboard (or code, .xib etc)

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes
        guard let collectionView = collectionView else { return attributes }
        attributes?.bounds.size.width = collectionView.bounds.width - sectionInset.left - sectionInset.right
        return attributes
    }

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let allAttributes = super.layoutAttributesForElementsInRect(rect)
        return allAttributes?.flatMap { attributes in
            switch attributes.representedElementCategory {
            case .Cell: return layoutAttributesForItemAtIndexPath(attributes.indexPath)
            default: return attributes
            }
        }
    }
}

2. Daftarkan tampilan koleksi Anda untuk penentuan ukuran otomatis

// The provided size should be a plausible estimate of the actual
// size. You can set your item size in your storyboard
// to a good estimate and use the code below. Otherwise,
// you can provide it manually too, e.g. CGSize(width: 100, height: 100)

flowLayout.estimatedItemSize = flowLayout.itemSize

3. Gunakan lebar yang telah ditentukan + tinggi khusus di subkelas sel Anda

override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    layoutAttributes.bounds.size.height = systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
    return layoutAttributes
}
Jordan Smith
sumber
8
Ini adalah jawaban terbesar yang saya temukan tentang topik mengukur diri. Terima kasih Jordan!
haik.ampardjian
1
Tidak bisa membuat ini bekerja di iOS (9.3). Pada 10 itu bekerja dengan baik. Aplikasi macet pada _updateVisibleCells (rekursi tak terbatas?). openradar.me/25303115
cristiano2lopes
@ cristiano2lopes ini bekerja dengan baik untuk saya di iOS 9.3. Pastikan Anda memberikan perkiraan yang masuk akal, dan pastikan batasan sel Anda diatur untuk menyelesaikan ke ukuran yang benar.
Jordan Smith
Ini bekerja luar biasa. Saya menggunakan Xcode 8 dan diuji di iOS 9.3.3 (perangkat fisik) dan tidak macet! Bekerja dengan baik. Pastikan saja perkiraan Anda bagus!
hahmed
1
@JordanSmith Ini tidak berfungsi di iOS 10. Apakah Anda memiliki contoh demo. Saya telah mencoba banyak hal untuk melakukan ketinggian sel tampilan koleksi berdasarkan isinya tetapi tidak ada. Saya menggunakan tata letak otomatis di iOS 10 dan menggunakan swift 3.
Ekta Padaliya
6

Cara sederhana untuk melakukannya di iOS 9 dalam beberapa baris kode - contoh cara horizontal (memperbaiki ketinggiannya ke tinggi Tampilan Koleksi):

Masukkan Tata Letak Alur Tampilan Koleksi Anda dengan estimatedItemSizeuntuk mengaktifkan sel swa-ukuran:

self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.estimatedItemSize = CGSizeMake(1, 1);

Menerapkan Delegasi Tata Letak Tampilan Koleksi (di Pengontrol Tampilan Anda sebagian besar waktu) collectionView:layout:sizeForItemAtIndexPath:,. Sasarannya di sini adalah untuk menyetel tinggi (atau lebar) tetap ke dimensi Tampilan Koleksi. Nilai 10 bisa apa saja, tetapi Anda harus menyetelnya ke nilai yang tidak melanggar batasan:

- (CGSize)collectionView:(UICollectionView *)collectionView
                  layout:(UICollectionViewLayout *)collectionViewLayout
  sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return CGSizeMake(10, CGRectGetHeight(collectionView.bounds));
}

Timpa preferredLayoutAttributesFittingAttributes:metode sel kustom Anda , bagian ini sebenarnya menghitung lebar sel dinamis Anda berdasarkan batasan Tata Letak Otomatis dan tinggi yang baru saja Anda tetapkan:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
    UICollectionViewLayoutAttributes *attributes = [layoutAttributes copy];
    float desiredWidth = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].width;
    CGRect frame = attributes.frame;
    frame.size.width = desiredWidth;
    attributes.frame = frame;
    return attributes;
}
Tanguy G.
sumber
Tahukah Anda jika di iOS9 metode yang lebih sederhana ini berfungsi dengan benar ketika sel menyertakan multi-garis UILabel, yang pemutusan garisnya memengaruhi tinggi intrinsik sel? Ini adalah kasus umum yang biasanya membutuhkan semua backflip yang saya jelaskan. Saya bertanya-tanya apakah iOS telah memperbaikinya sejak jawaban saya.
alga
2
@algal Sayangnya, jawabannya tidak.
jamesk
4

Coba perbaiki lebar Anda di atribut tata letak pilihan:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    UICollectionViewLayoutAttributes *attributes = [[super preferredLayoutAttributesFittingAttributes:layoutAttributes] copy];
    CGSize newSize = [self systemLayoutSizeFittingSize:CGSizeMake(FIXED_WIDTH,layoutAttributes.size) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    CGRect newFrame = attr.frame;
    newFrame.size.height = size.height;
    attr.frame = newFrame;
    return attr;
}

Tentu Anda juga ingin memastikan bahwa Anda mengatur tata letak Anda dengan benar untuk:

UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *) self.collectionView.collectionViewLayout;
flowLayout.estimatedItemSize = CGSizeMake(FIXED_WIDTH, estimatedHeight)];

Inilah sesuatu yang saya pakai di Github yang menggunakan sel lebar konstan dan mendukung tipe dinamis sehingga ketinggian sel diperbarui saat ukuran font sistem berubah.

Daniel Galasko
sumber
Dalam cuplikan ini Anda memanggil systemLayoutSizeFittingSize :. Sudahkah Anda berhasil membuat ini berfungsi tanpa juga menyetel preferMaxLayoutWidth di suatu tempat? Dalam pengalaman saya, jika Anda tidak menyetel preferMaxLayoutWidth, Anda perlu melakukan tata letak manual tambahan, misalnya, dengan mengganti sizeThatFits: dengan kalkulasi yang mereproduksi logika batasan tata letak otomatis yang ditentukan di tempat lain.
alga
@algal preferMaxLayoutWidth adalah properti di UILabel? Tidak sepenuhnya yakin bagaimana itu relevan?
Daniel Galasko
Ups! Poin bagus, tidak! Saya tidak pernah menangani ini dengan UITextView, jadi tidak menyadari bahwa API mereka berbeda di sana. Sepertinya UITextView.systemLayoutSizeFittingSize menghormati bingkainya karena tidak memiliki intrinsicContentSize. Tapi UILabel.systemLayoutSizeFittingSize mengabaikan frame, karena memiliki intrinsicContentSize, jadi preferMaxLayoutWidth diperlukan untuk mendapatkan intrinsicContentSize untuk mengekspos tinggi yang dibungkus ke AL. Sepertinya UITextView akan membutuhkan dua lintasan atau tata letak manual, tetapi saya rasa saya tidak memahami semua ini sepenuhnya.
alga
@algal Saya setuju, saya juga mengalami beberapa masalah saat menggunakan UILabel. Pengaturan lebar yang disukai memecahkan masalah tetapi alangkah baiknya memiliki pendekatan yang lebih dinamis karena lebar ini perlu diskalakan dengan tampilan supernya. Sebagian besar proyek saya tidak menggunakan properti pilihan, saya biasanya menggunakannya sebagai perbaikan cepat karena selalu ada masalah yang lebih dalam
Daniel Galasko
0

YA itu dapat dilakukan dengan menggunakan tata letak otomatis secara terprogram dan dengan menetapkan batasan di storyboard atau xib. Anda perlu menambahkan batasan untuk ukuran lebar agar tetap konstan dan menyetel tinggi lebih dari atau sama dengan.
http://www.thinkandbuild.it/learn-to-love-auto-layout-programmatically/

http://www.cocoanetics.com/2013/08/variable-sized-items-in-uicollectionview/
Semoga ini akan terjadi membantu dan menyelesaikan masalah Anda.

Dhaivat Vyas
sumber
3
Ini harus berfungsi untuk nilai lebar absolut, tetapi jika Anda ingin sel selalu menjadi lebar penuh tampilan koleksi, itu tidak akan berfungsi.
Air Terjun Michael
@MichaelWaterfall Saya berhasil membuatnya berfungsi dengan lebar penuh menggunakan AutoLayout. Saya telah menambahkan batasan lebar pada tampilan konten saya di dalam sel. Kemudian pada cellForItemAtIndexPathmemperbarui kendala: cell.constraintItemWidth.constant = UIScreen.main.bounds.width. Aplikasi saya hanya untuk potret. Anda mungkin ingin reloadDatasetelah perubahan orientasi untuk memperbarui batasan.
Flitskikker