Ini adalah bagian pertama dari serangkaian artikel di pustaka
ReactiveDataDisplayManager (RDDM) . Dalam artikel ini, saya akan menjelaskan masalah umum yang harus saya tangani saat bekerja dengan tabel "biasa", serta memberikan deskripsi RDDM.

Masalah 1. UITableViewDataSource
Sebagai permulaan, lupakan alokasi tanggung jawab, penggunaan kembali, dan kata-kata keren lainnya. Mari kita lihat pekerjaan biasa dengan tabel:
class ViewController: UIViewController { ... } extension ViewController: UITableViewDelegate { ... } extension ViewController: UITableViewDataSource { ... }
Kami akan menganalisis opsi yang paling umum. Apa yang perlu kita terapkan? Benar, 3 metode
UITableViewDataSource
biasanya diterapkan:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int func numberOfSections(in tableView: UITableView) -> Int func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
Untuk saat ini, kami tidak akan memperhatikan metode bantu (
numberOfSection
, dll.) Dan mempertimbangkan yang paling menarik -
func tableView(tableView: UITableView, indexPath: IndexPath)
Misalkan kita ingin mengisi tabel dengan sel dengan deskripsi produk, maka metode kita akan terlihat seperti ini:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) { let anyCell = tableView.dequeueReusableCell(withIdentifier: ProductCell.self, for: indexPath) guard let cell = anyCell as? ProductCell else { return UITableViewCell() } cell.configure(for: self.products[indexPath.row]) return cell }
Luar biasa, tidak sulit. Sekarang, anggaplah kita memiliki beberapa jenis sel, misalnya tiga:
Sebagai contoh sederhana, kita mendapatkan metode cell to
getCell
:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) { switch indexPath.row { case 0: guard let cell: PromoCell = self.getCell() else { return UITableViewCell() } cell.configure(self.promo) return cell case 1: guard let cell: AdCell = self.getCell() else { return UITableViewCell() } cell.configure(self.ad) return cell default: guard let cell: AdCell = self.getCell() else { return UITableViewCell() } cell.configure(self.products[indexPath.row - 2]) return cell } }
Entah bagaimana banyak kodenya. Bayangkan kita ingin memperbaiki layar pengaturan. Apa yang akan ada di sana?
- Tutup sel dengan avatar;
- Seperangkat sel dengan transisi "mendalam";
- Sel dengan sakelar (misalnya, aktifkan / nonaktifkan input dengan kode pin);
- Sel dengan informasi (misalnya, sel di mana akan ada telepon, email, apa pun);
- Penawaran pribadi.
Apalagi urutannya sudah diatur. Metode yang bagus akan berubah ...
Dan sekarang situasi lain - ada formulir input. Pada formulir input, sekelompok sel identik, masing-masing bertanggung jawab untuk bidang tertentu dalam model data. Misalnya, sel untuk masuk ke ponsel bertanggung jawab atas telepon dan sebagainya.
Semuanya sederhana, tetapi ada satu "TETAPI". Dalam hal ini, Anda masih harus melukis kasus yang berbeda, karena Anda perlu memperbarui bidang yang diperlukan.
Anda dapat terus berfantasi dan membayangkan Backend Driven Design, di mana kami menerima 6 jenis bidang input yang berbeda, dan tergantung pada keadaan bidang (visibilitas, jenis input, validasi, nilai default, dan sebagainya) sel berubah begitu banyak sehingga mereka tidak dapat mengarah ke satu antarmuka. Dalam hal ini, metode ini akan terlihat sangat tidak menyenangkan. Bahkan jika Anda menguraikan konfigurasi menjadi metode yang berbeda.
Ngomong-ngomong, setelah itu, bayangkan seperti apa kode Anda jika Anda ingin menambah / menghapus sel saat Anda bekerja. Ini tidak akan terlihat sangat bagus karena kita akan dipaksa untuk secara independen memantau konsistensi data yang disimpan dalam
ViewController
dan jumlah sel.
Masalahnya:
- Jika ada sel dengan tipe berbeda, maka kodenya menjadi seperti mie;
- Ada banyak masalah dengan penanganan acara dari sel;
- Kode jelek jika Anda perlu mengubah status tabel.
Masalah 2. MindSet
Waktu untuk kata-kata keren belum datang.
Mari kita lihat bagaimana aplikasi bekerja, atau lebih tepatnya, bagaimana data muncul di layar. Kami selalu menyajikan proses ini secara berurutan. Ya kurang lebih:
- Dapatkan data dari jaringan;
- Untuk memproses;
- Tampilkan data ini di layar.
Tapi benarkah begitu? Tidak! Faktanya, kami melakukan ini:
- Dapatkan data dari jaringan;
- Untuk memproses;
- Simpan di dalam model ViewController;
- Sesuatu menyebabkan penyegaran layar;
- Model yang disimpan dikonversi ke sel;
- Data ditampilkan di layar.
Selain kuantitas, masih ada perbedaan. Pertama, kami tidak lagi menampilkan data, itu adalah keluaran. Kedua, ada kesenjangan logis dalam proses pemrosesan data, model disimpan dan proses berakhir di sana. Kemudian sesuatu terjadi dan proses lain dimulai. Jadi, kami jelas tidak menambahkan elemen ke layar, tetapi hanya menyimpannya (yang, omong-omong, juga penuh) sesuai permintaan.
Dan ingat tentang
UITableViewDelegate
, itu juga termasuk metode untuk menentukan ketinggian sel. Biasanya Dimensi
automaticDimension
cukup, tetapi kadang-kadang ini tidak cukup dan Anda perlu mengatur ketinggian sendiri (misalnya, dalam hal animasi atau header)
Kemudian kita biasanya berbagi pengaturan sel, bagian dengan konfigurasi ketinggian ada di metode lain.
Masalahnya:
- Koneksi eksplisit antara pemrosesan data dan tampilannya pada UI terputus;
- Konfigurasi sel dipecah menjadi beberapa bagian.
Ide
Masalah yang terdaftar pada layar yang rumit menyebabkan sakit kepala dan keinginan yang tajam untuk minum teh.
Pertama, saya tidak ingin terus menerapkan metode delegasi. Solusi yang jelas adalah membuat objek yang akan mengimplementasikannya. Selanjutnya kita akan melakukan sesuatu seperti:
let displayManager = DisplayManager(self.tableView)
Bagus Sekarang Anda membutuhkan objek untuk dapat bekerja dengan sel apa pun, sedangkan konfigurasi sel-sel ini perlu dipindahkan ke tempat lain.
Jika kita meletakkan konfigurasi di objek terpisah, maka kita merangkum (saatnya untuk kata-kata pintar) konfigurasi di satu tempat. Di tempat yang sama ini, kita bisa menghilangkan logika untuk memformat data (misalnya, mengubah format tanggal, penggabungan string, dll.). Melalui objek yang sama, kita dapat berlangganan acara di sel.
Dalam hal ini, kita akan memiliki objek yang memiliki dua antarmuka berbeda:
- Antarmuka pembuatan instance
UITableView
adalah untuk DisplayManager kami. - Inisialisasi, berlangganan, dan konfigurasi antarmuka - untuk Presenter atau ViewController.
Kami menyebut objek ini generator. Kemudian generator kami untuk tabel adalah sel, dan untuk segalanya - cara untuk menyajikan data pada UI dan memproses acara.
Dan karena konfigurasi sekarang dienkapsulasi oleh generator, dan generator itu sendiri adalah sebuah sel, kita dapat memecahkan banyak masalah. Termasuk yang tercantum di atas.
Implementasi
public protocol TableCellGenerator: class { var identifier: UITableViewCell.Type { get } var cellHeight: CGFloat { get } var estimatedCellHeight: CGFloat? { get } func generate(tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell func registerCell(in tableView: UITableView) } public protocol ViewBuilder { associatedtype ViewType: UIView func build(view: ViewType) }
Dengan implementasi seperti itu, kita dapat membuat implementasi default:
public extension TableCellGenerator where Self: ViewBuilder { func generate(tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: self.identifier.nameOfClass, for: indexPath) as? Self.ViewType else { return UITableViewCell() } self.build(view: cell) return cell as? UITableViewCell ?? UITableViewCell() } func registerCell(in tableView: UITableView) { tableView.registerNib(self.identifier) } }<source lang="swift">
Saya akan memberikan contoh generator kecil:
final class FamilyCellGenerator { private var cell: FamilyCell? private var family: Family? var didTapPerson: ((Person) -> Void)? func show(family: Family) { self.family = family cell?.fill(with: family) } func showLoading() { self.family = nil cell?.showLoading() } } extension FamilyCellGenerator: TableCellGenerator { var identifier: UITableViewCell.Type { return FamilyCell.self } } extension FamilyCellGenerator: ViewBuilder { func build(view: FamilyCell) { self.cell = view view.selectionStyle = .none view.didTapPerson = { [weak self] person in self?.didTapPerson?(person) } if let family = self.family { view.fill(with: family) } else { view.showLoading() } } }
Di sini kami menyembunyikan konfigurasi dan langganan. Perhatikan bahwa sekarang kita memiliki tempat di mana kita dapat merangkum keadaan (karena tidak mungkin untuk merangkum keadaan dalam sel karena digunakan kembali oleh tabel). Dan mereka juga mendapat kesempatan untuk mengubah data di sel "on the fly."
Perhatikan
self.cell = view
. Kami ingat sel dan sekarang kami dapat memperbarui data tanpa memuat ulang sel ini. Ini adalah fitur yang bermanfaat.
Tapi saya terganggu. Karena kita dapat memiliki sel apa pun yang diwakili oleh generator, kita dapat membuat antarmuka DisplayManager kita sedikit lebih indah.
public protocol DataDisplayManager: class { associatedtype CollectionType associatedtype CellGeneratorType associatedtype HeaderGeneratorType init(collection: CollectionType) func forceRefill() func addSectionHeaderGenerator(_ generator: HeaderGeneratorType) func addCellGenerator(_ generator: CellGeneratorType) func addCellGenerators(_ generators: [CellGeneratorType], after: CellGeneratorType) func addCellGenerator(_ generator: CellGeneratorType, after: CellGeneratorType) func addCellGenerators(_ generators: [CellGeneratorType]) func update(generators: [CellGeneratorType]) func clearHeaderGenerators() func clearCellGenerators() }
Ini sebenarnya tidak semuanya. Kami dapat memasukkan generator di tempat yang tepat atau menghapusnya.
By the way, memasukkan sel setelah sel tertentu bisa sangat berguna. Terutama jika kami secara bertahap memuat data (misalnya, pengguna memasukkan TIN, kami mengunggah informasi TIN dan menampilkannya dengan menambahkan beberapa sel baru setelah bidang TIN).
Ringkasan
Bagaimana sel bekerja sekarang akan terlihat:
class ViewController: UIViewController { func update(data: [Products]) { let gens = data.map { ProductCellGenerator($0) } self.ddm.addGenerators(gens) } }
Atau di sini:
class ViewController: UIViewController { func update(fields: [Field]) { let gens = fields.map { field switch field.type { case .phone: let gen = PhoneCellGenerator(item) gen.didUpdate = { self.updatePhone($0) } return gen case .date: let gen = DateInputCellGenerator(item) gen.didTap = { self.showPicker() } return gen case .dropdown: let gen = DropdownCellGenerator(item) gen.didTap = { self.showDropdown(item) } return gen } } let splitter = SplitterGenerator() self.ddm.addGenerator(splitter) self.ddm.addGenerators(gens) self.ddm.addGenerator(splitter) } }
Kami dapat mengontrol urutan penambahan elemen dan, pada saat yang sama, koneksi antara pemrosesan data dan menambahkannya ke UI tidak hilang. Jadi, dalam kasus sederhana, kami memiliki kode sederhana. Dalam kasus-kasus sulit, kode tidak berubah menjadi pasta dan pada saat yang sama terlihat lumayan. Antarmuka deklaratif untuk bekerja dengan tabel telah muncul dan sekarang kami merangkum konfigurasi sel, yang dengan sendirinya memungkinkan kami untuk menggunakan kembali sel bersama dengan konfigurasi antara layar yang berbeda.
Kelebihan menggunakan RDDM:
- Enkapsulasi konfigurasi sel;
- Mengurangi duplikasi kode dengan merangkum kerja dari koleksi ke adaptor;
- Pilih objek adaptor yang merangkum logika spesifik bekerja dengan koleksi;
- Kode menjadi lebih jelas dan lebih mudah dibaca;
- Jumlah kode yang perlu ditulis untuk menambahkan tabel dikurangi;
- Proses memproses peristiwa dari sel disederhanakan.
Sumber di
sini .
Terima kasih atas perhatian anda!