Objek "sumber data" komposit dan elemen pendekatan fungsional

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 { // Default value if not implemented is "1". return dataSources.reduce(0) { $0 + ($1.numberOfSections?(in: tableView) ?? 1) } } 

(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 { // Default if not implemented is "true". return adduce(indexPath) { $0.tableView?(tableView, canEditRowAt: $1) ?? true } } 

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

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


All Articles