Suatu kali, saya (yah, bahkan tidak saya) menghadapi tugas menambahkan satu sel dari tipe yang sama sekali berbeda ke
UICollectionView
dengan jenis sel tertentu, dan untuk melakukan ini hanya dalam kasus khusus, yang diproses "di atas" dan tidak langsung bergantung pada
UICollectionView
. Tugas ini memunculkan, jika ingatanku, beberapa blok jelek
if
- else
dalam metode
UICollectionViewDataSource
dan
UICollectionViewDelegate
, yang dengan aman menetap dalam kode "produksi" dan, mungkin, tidak akan pergi ke mana pun dari sana.
Dalam kerangka tugas yang disebutkan di atas, tidak ada gunanya memikirkan solusi yang lebih elegan, atau membuang energi "bijaksana" untuk ini. Namun demikian, saya ingat cerita ini: Saya berpikir untuk mencoba mengimplementasikan objek "sumber data" tertentu yang dapat terdiri dari sejumlah objek "sumber data" yang lain menjadi satu keseluruhan. Solusinya, jelas, harus digeneralisasi, cocok untuk sejumlah komponen (termasuk nol dan satu) dan tidak tergantung pada jenis tertentu. Ternyata ini tidak hanya nyata, tetapi juga tidak terlalu sulit (walaupun membuat kode juga "cantik" sedikit lebih sulit).
Saya akan menunjukkan apa yang saya lakukan dengan contoh
UITableView
. Jika Anda mau, menulis kode yang serupa untuk
UICollectionView
seharusnya tidak sulit.
“Suatu ide selalu lebih penting daripada perwujudannya”
Pepatah ini milik penulis buku komik hebat Alan Moore ( "Keepers , " "V singkatan dari Vendetta , " "League of Outstanding Gentlemen" ), tapi ini bukan soal pemrograman, kan?Gagasan utama dari pendekatan saya adalah untuk menyimpan array objek
UITableViewDataSource
, mengembalikan jumlah total bagian mereka dan dapat menentukan kapan mengakses bagian objek "sumber data" asli yang akan mengarahkan kembali panggilan ini.
Protokol
UITableViewDataSource
sudah memiliki metode yang diperlukan untuk mendapatkan jumlah bagian, baris, dll., Tetapi, sayangnya, dalam hal ini saya merasa sangat tidak nyaman untuk digunakan karena kebutuhan untuk memberikan referensi ke instance
UITableView
sebagai salah satu argumen. Oleh karena itu, saya memutuskan untuk memperluas protokol
UITableViewDataSource
standar dengan beberapa anggota sederhana tambahan:
protocol ComposableTableViewDataSource: UITableViewDataSource { var numberOfSections: Int { get } func numberOfRows(for section: Int) -> Int }
Dan "sumber data" komposit ternyata menjadi kelas sederhana yang mengimplementasikan persyaratan
UITableViewDataSource
dan diinisialisasi dengan hanya satu argumen - satu set contoh spesifik dari
ComposableTableViewDataSource
:
final class ComposedTableViewDataSource: NSObject, UITableViewDataSource { private let dataSources: [ComposableTableViewDataSource] init(dataSources: ComposableTableViewDataSource...) { self.dataSources = dataSources super.init() } private override init() { fatalError("\(#file) \(#line): Initializer with parameters must be used.") } }
Sekarang tinggal menulis implementasi semua metode protokol
UITableViewDataSource
sehingga mereka merujuk ke metode komponen yang sesuai.
“Itu keputusan yang tepat. Keputusan saya
Kata-kata ini milik Boris Nikolayevich Yeltsin , presiden pertama Federasi Rusia , dan tidak benar-benar merujuk pada teks di bawah ini, saya hanya menyukainya.Keputusan yang tepat bagi saya tampaknya menggunakan
fungsi bahasa
Swift , dan itu ternyata benar-benar nyaman.
Pertama, kami menerapkan metode yang mengembalikan jumlah bagian - ini tidak sulit. Seperti disebutkan di atas, kita hanya perlu jumlah total semua bagian komponen:
func numberOfSections(in tableView: UITableView) -> Int {
(Saya tidak akan menjelaskan sintaks dan makna fungsi standar. Jika diperlukan, Internet penuh dengan
artikel pengantar yang bagus tentang topik ini . Dan saya juga dapat merekomendasikan buku yang
cukup bagus .)
Melihat sekilas pada semua metode
UITableViewDataSource
, Anda akan melihat bahwa sebagai argumen, mereka hanya menerima tautan ke tabel dan nilai nomor bagian atau baris
IndexPath
sesuai. Kami akan menulis beberapa helper yang akan berguna bagi kami dalam mengimplementasikan semua metode protokol lainnya.
Pertama, semua tugas dapat direduksi menjadi fungsi
"generik" , yang mengambil sebagai argumen referensi ke
ComposableTableViewDataSource
spesifik dan nilai nomor bagian atau
IndexPath
. Untuk kenyamanan dan singkatnya, kami menetapkan
nama samaran untuk jenis fungsi ini. Plus, untuk menambah keterbacaan, saya sarankan mendeklarasikan alias untuk nomor bagian:
private typealias SectionNumber = Int private typealias AdducedSectionTask<T> = (_ composableDataSource: ComposableTableViewDataSource, _ sectionNumber: SectionNumber) -> T private typealias AdducedIndexPathTask<T> = (_ composableDataSource: ComposableTableViewDataSource, _ indexPath: IndexPath) -> T
(Saya akan menjelaskan nama-nama yang dipilih tepat di bawah.)
Kedua, kami menerapkan fungsi sederhana yang menentukan
ComposableTableViewDataSource
spesifik dan nomor bagian yang sesuai dengan nomor bagian
ComposedTableViewDataSource
:
private func decompose(section: SectionNumber) -> (dataSource: ComposableTableViewDataSource, decomposedSection: SectionNumber) { var section = section var dataSourceIndex = 0 for (index, dataSource) in dataSources.enumerated() { let diff = section - dataSource.numberOfSections dataSourceIndex = index if diff < 0 { break } else { section = diff } } return (dataSources[dataSourceIndex], section) }
Mungkin, jika Anda berpikir sedikit lebih lama dari milik saya, implementasinya akan menjadi lebih elegan dan tidak langsung. Sebagai contoh, kolega saya segera menyarankan agar saya menerapkan
pencarian biner dalam fungsi ini (sebelumnya, misalnya, selama inisialisasi, dengan menyusun indeks jumlah bagian -
array sederhana
bilangan bulat ). Atau bahkan menghabiskan sedikit waktu untuk menyusun dan menyimpan tabel korespondensi nomor bagian, tetapi daripada menggunakan metode dengan
kompleksitas waktu O (n) atau O (log n), Anda bisa mendapatkan hasilnya dengan biaya O (1). Tetapi saya memutuskan untuk mengambil nasihat dari
Donald Knuth yang hebat untuk tidak terlibat dalam optimasi prematur tanpa kebutuhan nyata dan pengukuran yang tepat. Ya, dan bukan tentang artikel ini.
Dan akhirnya, fungsi yang menerima
AdducedSectionTask
dan
AdducedIndexPathTask
ditunjukkan di atas dan "mengarahkan" mereka ke instance
ComposedTableViewDataSource
spesifik:
private func adduce<T>(_ section: SectionNumber, _ task: AdducedSectionTask<T>) -> T { let (dataSource, decomposedSection) = decompose(section: section) return task(dataSource, decomposedSection) } private func adduce<T>(_ indexPath: IndexPath, _ task: AdducedIndexPathTask<T>) -> T { let (dataSource, decomposedSection) = decompose(section: indexPath.section) return task(dataSource, IndexPath(row: indexPath.row, section: decomposedSection)) }
Dan sekarang Anda dapat menjelaskan nama yang saya pilih untuk semua fungsi ini. Sederhana: mereka mencerminkan gaya penamaan fungsional. Yaitu berarti sedikit, tetapi terdengar mengesankan.
Dua fungsi terakhir terlihat hampir seperti kembar, tetapi setelah sedikit berpikir, saya menyerah berusaha menyingkirkan duplikasi kode, karena membawa lebih banyak ketidaknyamanan daripada keuntungan: Saya harus menampilkan atau mentransfer fungsi konversi ke nomor bagian dan kembali ke jenis aslinya. Selain itu, kemungkinan menggunakan kembali pendekatan umum ini cenderung nol.
Semua persiapan dan bantuan ini memberikan keuntungan luar biasa dalam implementasi, pada kenyataannya, dari metode protokol. Metode konfigurasi tabel:
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return adduce(section) { $0.tableView?(tableView, titleForHeaderInSection: $1) } } func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { return adduce(section) { $0.tableView?(tableView, titleForFooterInSection: $1) } } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return adduce(section) { $0.tableView(tableView, numberOfRowsInSection: $1) } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { return adduce(indexPath) { $0.tableView(tableView, cellForRowAt: $1) } }
Sisipkan dan hapus baris:
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { return adduce(indexPath) { $0.tableView?(tableView, commit: editingStyle, forRowAt: $1) } } func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
Dengan cara yang sama, dukungan untuk header indeks bagian dapat diimplementasikan. Dalam hal ini, alih-alih nomor bagian, Anda harus beroperasi dengan indeks tajuk. Selain itu, kemungkinan besar, ini akan berguna untuk menambahkan bidang tambahan ke protokol
ComposableTableViewDataSource
. Saya meninggalkan bagian ini di luar materi.
"Yang mustahil hari ini akan memungkinkan besok"
Ini adalah kata-kata dari ilmuwan Rusia Konstantin Eduardovich Tsiolkovsky , pendiri kosmonautika teoretis.Pertama, solusi yang disajikan tidak mendukung menyeret dan menjatuhkan baris. Paket awal menyertakan dukungan untuk seret dan letakkan di dalam salah satu objek “sumber data” utama, tetapi, sayangnya, ini tidak dapat dicapai hanya dengan menggunakan
UITableViewDataSource
. Metode protokol ini menentukan apakah mungkin untuk "menarik dan melepas" garis tertentu dan menerima "panggilan balik" di akhir hambatan. Dan pemrosesan acara itu sendiri tersirat dalam metode
UITableViewDelegate
.
Kedua, dan yang lebih penting, perlu untuk memikirkan mekanisme untuk memperbarui data di layar. Saya pikir ini dapat diimplementasikan dengan mendeklarasikan protokol delegasi
ComposableTableViewDataSource
, metode yang akan diimplementasikan oleh
ComposedTableViewDataSource
dan menerima sinyal bahwa "sumber data" asli telah menerima pembaruan. Dua pertanyaan tetap terbuka: bagaimana menentukan secara andal
ComposableTableViewDataSource
telah berubah di dalam
ComposedTableViewDataSource
dan bagaimana tugas itu terpisah dan bukan tugas yang paling sepele, tetapi memiliki sejumlah solusi (misalnya,
seperti itu ). Dan, tentu saja, Anda akan memerlukan
ComposedTableViewDataSource
delegasi
ComposedTableViewDataSource
, metode yang akan dipanggil ketika memperbarui "sumber data" komposit dan diimplementasikan oleh jenis klien (misalnya, model
controller atau
view ).
Saya berharap dapat menyelidiki masalah ini lebih baik dari waktu ke waktu dan membahasnya di bagian kedua artikel ini. Sementara itu, saya geli, Anda penasaran untuk membaca tentang percobaan ini!
PS
Beberapa hari yang lalu, saya harus masuk ke kode yang disebutkan dalam pendahuluan untuk memodifikasinya: Saya perlu menukar sel dari kedua jenis itu. Singkatnya, saya harus menyiksa diri sendiri dan "mencari nafkah" dari
Index out of bounds
terus-menerus muncul di tempat yang berbeda. Saat menggunakan pendekatan yang dijelaskan, hanya perlu menukar dua "sumber data" objek dalam array yang dilewatkan sebagai argumen penginisialisasi.
Referensi:-
Playgroud dengan kode dan contoh lengkap-
twitter saya