Mengurai UICollectionViewCell

Setelah menonton Keynote WWDC 2019 dan mengenal SwiftUI , yang dirancang untuk mendeskripsikan secara deskriptif UI dalam kode, saya ingin berspekulasi tentang cara mengisi piring dan koleksi secara deklaratif. Misalnya, seperti ini:


enum Builder { static func widgets(objects: Objects) -> [Widget] { let header = [ Spacing(height: 25).widget, Header(string: " ").widget, Spacing(height: 10, separator: .bottom).widget ] let body = objects .flatMap({ (object: Object) -> [Widgets] in return [ Title(object: object).widget, Spacing(height: 1, separator: .bottom).widget ] }) return header + body } } let objects: [Object] = ... Builder .widgets(objects: objects) .bind(to: collectionView) 

Dalam koleksi, ini diterjemahkan sebagai berikut:

gambar


Pendahuluan


Seperti yang Anda ketahui dari sumber resmi : sebagian besar waktu mereka, pengembang iOS biasa menghabiskan waktu bekerja dengan tablet. Jika kami menganggap bahwa pengembang pada proyek ini sangat kurang dan tablet tidak sederhana, maka sama sekali tidak ada waktu tersisa untuk sisa aplikasi. Dan sesuatu perlu dilakukan dengan ini ... Solusi yang mungkin adalah dekomposisi sel.


Dekomposisi sel berarti mengganti satu sel dengan beberapa sel yang lebih kecil. Dengan penggantian ini, secara visual tidak ada yang berubah. Sebagai contoh, pertimbangkan posting dari umpan berita VK untuk iOS. Satu pos dapat disajikan baik sebagai sel tunggal, atau sebagai kelompok sel - primitif .


Penguraian sel tidak akan selalu berhasil. Akan sulit untuk memecah sel menjadi primitif yang memiliki bayangan atau pembulatan di semua sisi. Dalam hal ini, sel asli akan menjadi primitif.


Pro dan kontra


Menggunakan dekomposisi sel, tabel / koleksi mulai terdiri dari primitif yang akan sering digunakan kembali: primitif dengan teks, primitif dengan gambar, primitif dengan latar belakang, dll. Perhitungan ketinggian primitif tunggal jauh lebih sederhana dan lebih efektif daripada sel kompleks dengan sejumlah besar negara. Jika diinginkan, ketinggian dinamis primitif dapat dihitung atau bahkan digambar di latar belakang (misalnya, teks melalui CTFramesetter ).


Di sisi lain, bekerja dengan data menjadi lebih rumit. Data akan diperlukan untuk setiap primitif, dan menurut IndexPath primitif akan sulit untuk menentukan sel mana yang asli. Kami harus memperkenalkan lapisan abstraksi baru atau menyelesaikan masalah ini.


Anda dapat berbicara lama tentang kemungkinan pro dan kontra dari usaha ini, tetapi lebih baik untuk mencoba menggambarkan pendekatan dekomposisi sel.


Memilih Alat


Karena kemampuan UITableView terbatas, dan, seperti yang telah disebutkan, kami memiliki pelat yang agak rumit, solusi yang sesuai adalah menggunakan UICollectionView . Ini tentang UICollectionView yang akan dibahas dalam publikasi ini.


Menggunakan UICollectionView dihadapkan dengan situasi di mana basis UICollectionViewFlowLayout tidak dapat membentuk pengaturan yang diinginkan dari elemen koleksi (kami tidak memperhitungkan UICollectionViewCompositionalLayout baru). Pada saat-saat seperti itu, keputusan biasanya dibuat untuk menemukan beberapa sumber terbuka UICollectionViewLayout . Tetapi bahkan di antara solusi yang sudah jadi, mungkin tidak ada yang cocok, seperti, misalnya, dalam kasus halaman utama dinamis dari toko online besar atau jejaring sosial. Kami berasumsi yang terburuk, jadi kami akan membuat UICollectionViewLayout universal kami sendiri.


Selain kesulitan dalam memilih tata letak, Anda perlu memutuskan bagaimana koleksi akan menerima data. Selain pendekatan yang biasa, di mana objek (paling sering UIViewController ) mematuhi protokol UICollectionViewDataSource dan menyediakan data untuk koleksi, penggunaan kerangka kerja yang didorong data semakin populer. Perwakilan cerah dari pendekatan ini adalah CollectionKit , IGListKit , RxDataSources, dan lainnya. Penggunaan kerangka kerja semacam itu menyederhanakan pekerjaan dengan koleksi dan menyediakan kemampuan untuk menganimasikan perubahan data, karena Algoritma difing sudah ada dalam kerangka kerja. Untuk tujuan publikasi, kerangka kerja RxDataSources akan dipilih.


Widget dan propertinya


Kami memperkenalkan struktur data perantara dan menyebutnya widget . Kami menjelaskan properti utama yang harus dimiliki widget:


  1. Widget harus mematuhi protokol yang diperlukan untuk menggunakan kerangka kerja berbasis data. Protokol semacam itu biasanya berisi nilai yang terkait (mis. IdentifiableType di RxDataSources )
  2. Seharusnya dimungkinkan untuk merakit widget untuk primitif berbeda menjadi sebuah array. Untuk mencapai ini, widget tidak boleh memiliki nilai terkait. Untuk tujuan ini, Anda dapat menggunakan mekanisme tipe erasure atau sesuatu seperti itu.
  3. Widget harus dapat menghitung ukuran primitif. Kemudian, ketika membentuk UICollectionViewLayout , tetap hanya untuk memposisikan primitif dengan benar sesuai aturan yang telah ditentukan.
  4. Widget harus menjadi pabrik untuk UICollectionViewCell . Oleh karena itu, dari implementasi UICollectionViewDataSource semua logika untuk membuat sel akan dihapus dan yang tersisa adalah:
     let cell = widget.widgetCell(collectionView: collectionView, indexPath: indexPath) return cell 

Implementasi widget


Agar dapat menggunakan widget dengan kerangka kerja RxDataSources , widget harus mematuhi protokol Equatable dan IdentifiableType . Karena widget mewakili primitif, itu akan cukup untuk keperluan penerbitan jika widget mengidentifikasi dirinya untuk mematuhi protokol IdentifiableType . Dalam praktiknya, ini akan memengaruhi fakta bahwa ketika widget berubah, primitif tidak akan dimuat ulang, tetapi dihapus dan dimasukkan. Untuk melakukan ini, perkenalkan protokol WidgetIdentifiable baru:


 protocol WidgetIdentifiable: IdentifiableType { } extension WidgetIdentifiable { var identity: Self { return self } } 

Untuk menyesuaikan dengan WidgetIdentifiable , widget harus sesuai dengan protokol Hashable . Widget Hashable akan mengambil data untuk kepatuhan dengan protokol dari objek yang akan menggambarkan primitif tertentu. Anda dapat menggunakan AnyHashable untuk "menghapus" widget jenis objek.


 struct Widget: WidgetIdentifiable { let underlying: AnyHashable init(_ underlying: AnyHashable) { self.underlying = underlying } } extension Widget: Hashable { func hash(into hasher: inout Hasher) { self.underlying.hash(into: &hasher) } static func ==(lhs: Widget, rhs: Widget) -> Bool { return lhs.underlying == rhs.underlying } } 

Pada tahap ini, dua properti pertama widget dijalankan. Ini tidak sulit untuk diperiksa dengan mengumpulkan beberapa widget dengan berbagai jenis objek dalam sebuah array.


 let widgets = [Widget("Hello world"), Widget(100500)] 

Untuk mengimplementasikan properti yang tersisa, kami memperkenalkan protokol baru WidgetPresentable


 protocol WidgetPresentable { func widgetCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell func widgetSize(containerWidth: CGFloat) -> CGSize } 

Fungsi widgetSize(containerWidth:) akan digunakan dalam UICollectionViewLayout saat membuat atribut sel, dan widgetCell(collectionView:indexPath:) - untuk mendapatkan sel.


Jika widget WidgetPresentable protokol WidgetPresentable , widget akan memenuhi semua properti yang ditunjukkan pada awal publikasi. Namun, objek yang terdapat dalam widget AnyHashable harus diganti dengan komposisi WidgetPresentable dan WidgetHashable , di mana WidgetHashable tidak akan memiliki nilai terkait (seperti dalam kasus Hashable ) dan jenis objek di dalam widget akan tetap "dihapus":


 protocol WidgetHashable { func widgetEqual(_ any: Any) -> Bool func widgetHash(into hasher: inout Hasher) } 

Di versi final, widget akan terlihat seperti ini:


 struct Widget: WidgetIdentifiable { let underlying: WidgetHashable & WidgetPresentable init(_ underlying: WidgetHashable & WidgetPresentable) { self.underlying = underlying } } extension Widget: Hashable { func hash(into hasher: inout Hasher) { self.underlying.widgetHash(into: &hasher) } static func ==(lhs: Widget, rhs: Widget) -> Bool { return lhs.underlying.widgetEqual(rhs.underlying) } } extension Widget: WidgetPresentable { func widgetCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { return underlying.widgetCell(collectionView: collectionView, indexPath: indexPath) } func widgetSize(containerWidth: CGFloat) -> CGSize { return underlying.widgetSize(containerWidth: containerWidth) } } 

Objek primitif


Mari kita coba merakit primitif paling sederhana, yang akan menjadi lekukan dari ketinggian yang diberikan.


 struct Spacing: Hashable { let height: CGFloat } class SpacingView: UIView { lazy var constraint = self.heightAnchor.constraint(equalToConstant: 1) init() { super.init(frame: .zero) self.constraint.isActive = true } } extension Spacing: WidgetHashable { func widgetEqual(_ any: Any) -> Bool { if let spacing = any as? Spacing { return self == spacing } return false } func widgetHash(into hasher: inout Hasher) { self.hash(into: &hasher) } } extension Spacing: WidgetPresentable { func widgetCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { let cell: WidgetCell<SpacingView> = collectionView.cellDequeueSafely(indexPath: indexPath) if cell.view == nil { cell.view = SpacingView() } cell.view?.constraint.constant = height return cell } func widgetSize(containerWidth: CGFloat) -> CGSize { return CGSize(width: containerWidth, height: height) } } 

WidgetCell<T> hanyalah subkelas dari UICollectionViewCell yang menerima UIView dan menambahkannya sebagai subview. cellDequeueSafely(indexPath:) adalah fungsi yang mendaftarkan sel dalam koleksi sebelum digunakan kembali jika sel dalam koleksi sebelumnya belum terdaftar. Spacing akan digunakan seperti yang dijelaskan di awal publikasi.


Setelah menerima array widget, tetap hanya untuk mengikat observerWidgets :


 typealias DataSource = RxCollectionViewSectionedAnimatedDataSource<WidgetSection> class Controller: UIViewController { private lazy var dataSource: DataSource = self.makeDataSource() var observerWidgets: (Observable<Widgets>) -> Disposable { return collectionView.rx.items(dataSource: dataSource) } func makeDataSource() -> DataSource { return DataSource(configureCell: { (_, collectionView: UICollectionView, indexPath: IndexPath, widget: Widget) in let cell = widget.widgetCell(collectionView: collectionView, indexPath: indexPath) return cell }) } } 

Hasil


Sebagai kesimpulan, saya ingin menunjukkan karya nyata dari koleksi, yang dibangun sepenuhnya di atas widget.


gambar

Seperti yang Anda lihat, dekomposisi UICollectionViewCell layak dan dalam situasi yang sesuai dapat menyederhanakan kehidupan pengembang.


Komentar


Kode yang diberikan dalam publikasi sangat disederhanakan dan tidak boleh dikompilasi. Tujuannya adalah untuk menggambarkan pendekatan, bukan untuk menyediakan solusi turnkey.


Protokol WidgetPresentable dapat diperluas dengan fungsi lain yang mengoptimalkan tata letak, misalnya, widgetSizeEstimated(containerWidth:) atau widgetSizePredefined(containerWidth:) , yang masing-masing mengembalikan estimasi dan ukuran tetap. Perlu dicatat bahwa fungsi widgetSize(containerWidth:) harus mengembalikan ukuran primitif bahkan untuk perhitungan yang menuntut, misalnya, untuk systemLayoutSizeFitting(_:) . Perhitungan seperti itu dapat di-cache melalui Dictionary , NSCache , dll.


Seperti yang Anda ketahui, semua jenis sel yang digunakan oleh UICollectionView harus didaftarkan sebelumnya dalam koleksi. Namun, untuk menggunakan kembali widget di antara berbagai layar / koleksi dan tidak mendaftarkan semua pengenal / jenis sel terlebih dahulu, Anda perlu memperoleh mekanisme yang akan mendaftarkan sel segera sebelum digunakan pertama kali dalam setiap koleksi. Dalam publikasi, fungsi cellDequeueSafely(indexPath:) digunakan untuk ini.


Tidak ada header atau footer dalam koleksi. Di tempat mereka akan menjadi primitif. Kehadiran pelengkap dalam koleksi tidak akan memberikan bonus khusus dalam pendekatan saat ini. Biasanya mereka digunakan ketika array data secara ketat sesuai dengan jumlah sel dan diperlukan untuk menampilkan tampilan tambahan sebelum, setelah atau di antara sel. Dalam kasus kami, data untuk tampilan tambahan juga dapat ditambahkan ke larik widget dan digambarkan sebagai primitif.


Dalam koleksi yang sama, widget dengan objek yang sama dapat ditemukan. Misalnya, Spacing sama di awal dan di akhir koleksi. Kehadiran objek non-unik tersebut akan mengarah pada kenyataan bahwa animasi dalam koleksi menghilang. Untuk membuat objek tersebut unik, Anda dapat menggunakan tag AnyHashable khusus, #file dan #file tempat objek itu dibuat, dll.

Source: https://habr.com/ru/post/id455421/


All Articles