Bagaimana cara menghindari UITableViewController yang besar dan canggung di iOS?

36

Saya memiliki masalah ketika menerapkan pola-MVC di iOS. Saya telah mencari di internet tetapi sepertinya tidak menemukan solusi yang bagus untuk masalah ini.

Banyak UITableViewControllerimplementasi tampaknya agak besar. Kebanyakan contoh yang saya lihat memungkinkan UITableViewControllerimplementasi <UITableViewDelegate>dan <UITableViewDataSource>. Implementasi ini adalah alasan utama mengapa UITableViewControllersemakin besar. Salah satu solusinya adalah membuat kelas terpisah yang mengimplementasikan <UITableViewDelegate>dan <UITableViewDataSource>. Tentu saja kelas-kelas ini harus memiliki referensi ke UITableViewController. Apakah ada kekurangan menggunakan solusi ini? Secara umum saya pikir Anda harus mendelegasikan fungsionalitas ke kelas "Pembantu" lainnya atau serupa, menggunakan pola delegasi. Apakah ada cara yang mapan untuk memecahkan masalah ini?

Saya tidak ingin model mengandung terlalu banyak fungsi, atau tampilan. Saya percaya bahwa logika harus benar-benar berada di kelas controller, karena ini adalah salah satu landasan pola MVC. Namun pertanyaan besarnya adalah:

Bagaimana seharusnya Anda membagi pengontrol implementasi-MVC menjadi bagian-bagian yang lebih kecil yang dapat dikelola? (Berlaku untuk MVC di iOS dalam kasus ini)

Mungkin ada pola umum untuk menyelesaikan ini, meskipun saya secara khusus mencari solusi untuk iOS. Tolong beri contoh pola yang baik untuk memecahkan masalah ini. Berikan argumen mengapa solusi Anda luar biasa.

Johan Karlsson
sumber
1
"Juga argumen mengapa solusi ini luar biasa." :)
occulus
1
Itu sedikit di luar intinya, tetapi UITableViewControllermekanika tampaknya cukup aneh bagi saya, jadi saya bisa mengaitkannya dengan masalah. Aku Aku digunakan benar-benar senang MonoTouch, karena MonoTouch.Dialogsecara khusus membuat yang lebih mudah untuk bekerja dengan tabel di iOS. Sementara itu, saya ingin tahu apa yang mungkin disarankan orang lain yang lebih berpengetahuan di sini ...
Patryk Ćwiek

Jawaban:

43

Saya menghindari penggunaan UITableViewController, karena menempatkan banyak tanggung jawab ke dalam satu objek. Oleh karena itu saya memisahkan UIViewControllersubclass dari sumber data dan mendelegasikan. Tanggung jawab pengontrol tampilan adalah menyiapkan tampilan tabel, membuat sumber data dengan data, dan menyatukan semuanya. Mengubah cara tampilan tabel diwakili dapat dilakukan tanpa mengubah pengontrol tampilan, dan memang pengontrol tampilan yang sama dapat digunakan untuk beberapa sumber data yang semuanya mengikuti pola ini. Demikian pula, mengubah alur kerja aplikasi berarti perubahan pada pengontrol tampilan tanpa khawatir tentang apa yang terjadi pada tabel.

Saya sudah mencoba memisahkan protokol UITableViewDataSourcedan UITableViewDelegatemenjadi objek yang berbeda, tetapi yang biasanya berakhir dengan pembelahan palsu karena hampir setiap metode pada delegasi perlu menggali ke dalam sumber data (misalnya pada seleksi, delegasi perlu tahu objek apa yang diwakili oleh baris yang dipilih). Jadi saya berakhir dengan satu objek yang merupakan sumber data dan delegasi. Objek ini selalu menyediakan metode di -(id)tableView: (UITableView *)tableView representedObjectAtIndexPath: (NSIndexPath *)indexPathmana sumber data dan aspek pendelegasian perlu mengetahui apa yang sedang mereka kerjakan.

Itu pemisahan "tingkat 0" saya. Level 1 bertunangan jika saya harus mewakili objek dari berbagai jenis dalam tampilan tabel yang sama. Sebagai contoh, bayangkan Anda harus menulis aplikasi Kontak — untuk satu kontak, Anda mungkin memiliki baris yang mewakili nomor telepon, baris lain yang mewakili alamat, yang lain mewakili alamat email, dan sebagainya. Saya ingin menghindari pendekatan ini:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  if ([object isKindOfClass: [PhoneNumber class]]) {
    //configure phone number cell
  }
  else if …
}

Dua solusi telah muncul sejauh ini. Pertama adalah membangun pemilih secara dinamis:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  NSString *cellSelectorName = [NSString stringWithFormat: @"tableView:cellFor%@AtIndexPath:", [object class]];
  SEL cellSelector = NSSelectorFromString(cellSelectorName);
  return [self performSelector: cellSelector withObject: tableView withObject: object];
}

- (UITableViewCell *)tableView: (UITableView *)tableView cellForPhoneNumberAtIndexPath: (NSIndexPath *)indexPath {
  // configure phone number cell
}

Dalam pendekatan ini, Anda tidak perlu mengedit if()pohon epik untuk mendukung tipe baru - cukup tambahkan metode yang mendukung kelas baru. Ini adalah pendekatan yang bagus jika tampilan tabel ini adalah satu-satunya yang perlu mewakili objek-objek ini, atau perlu menyajikannya dengan cara khusus. Jika objek yang sama akan diwakili dalam tabel yang berbeda dengan sumber data yang berbeda, pendekatan ini rusak karena metode pembuatan sel perlu dibagi di seluruh sumber data — Anda bisa menentukan superclass umum yang menyediakan metode ini, atau Anda bisa melakukan ini:

@interface PhoneNumber (TableViewRepresentation)

- (UITableViewCell *)tableView: (UITableView *)tableView representationAsCellForRowAtIndexPath: (NSIndexPath *)indexPath;

@end

@interface Address (TableViewRepresentation)

//more of the same…

@end

Kemudian di kelas sumber data Anda:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  return [object tableView: tableView representationAsCellForRowAtIndexPath: indexPath];
}

Ini berarti bahwa setiap sumber data yang perlu menampilkan nomor telepon, alamat, dll. Bisa saja bertanya objek apa pun yang diwakili untuk sel tampilan tabel. Sumber data itu sendiri tidak perlu lagi tahu apa-apa tentang objek yang ditampilkan.

"Tapi, tunggu," aku mendengar kata sela hipotetis, "bukankah itu merusak MVC? Apakah kamu tidak memasukkan detail tampilan ke dalam kelas model?"

Tidak, itu tidak merusak MVC. Anda dapat menganggap kategori dalam hal ini sebagai penerapan Dekorator ; begitu PhoneNumberjuga kelas model tetapi PhoneNumber(TableViewRepresentation)merupakan kategori tampilan. Sumber data (objek pengontrol) memediasi antara model dan tampilan, sehingga arsitektur MVC masih berlaku.

Anda dapat melihat penggunaan kategori ini sebagai hiasan dalam kerangka kerja Apple juga. NSAttributedStringadalah kelas model, memegang beberapa teks dan atribut. AppKit menyediakan NSAttributedString(AppKitAdditions)dan UIKit menyediakan NSAttributedString(NSStringDrawing), kategori dekorator yang menambahkan perilaku menggambar ke kelas model ini.


sumber
Apa nama yang baik untuk kelas yang berfungsi sebagai sumber data dan delegasi tampilan tabel?
Johan Karlsson
1
@JohanKarlsson Saya sering menyebutnya sumber data. Mungkin agak ceroboh, tapi saya cukup sering menggabungkan keduanya untuk mengetahui bahwa "sumber data" saya merupakan adaptasi ke definisi Apple yang lebih terbatas.
1
Artikel ini: objc.io/issue-1/table-views.html mengusulkan cara untuk menangani beberapa tipe sel di mana Anda mengerjakan kelas sel dalam cellForPhotoAtIndexPathmetode sumber data, kemudian memanggil metode pabrik yang sesuai. Yang tentu saja hanya mungkin jika kelas tertentu diprediksi menempati baris tertentu. Sistem Anda pada kategori-model yang menghasilkan tampilan jauh lebih elegan dalam praktiknya, saya pikir, meskipun mungkin itu adalah pendekatan yang tidak lazim untuk MVC! :)
Benji XVI
1
Saya telah mencoba untuk mendemokan pola ini di github.com/yonglam/TableViewPattern . Semoga bermanfaat bagi seseorang.
Andrew
1
Saya akan memilih tidak pasti untuk pendekatan pemilih dinamis. Ini sangat berbahaya karena masalah hanya muncul saat dijalankan. Tidak ada cara otomatis untuk memastikan bahwa pemilih yang diberikan ada dan diketik dengan benar dan pendekatan semacam ini pada akhirnya akan berantakan dan merupakan mimpi buruk untuk dipertahankan. Namun pendekatan lain sangat pintar.
mkko
3

Orang-orang cenderung banyak mengemasnya ke dalam UIViewController / UITableViewController.

Delegasi ke kelas lain (bukan view controller) biasanya bekerja dengan baik. Delegasi tidak perlu memerlukan referensi kembali ke pengontrol tampilan, karena semua metode delegasi mendapatkan referensi ke UITableView, tetapi mereka akan membutuhkan akses entah bagaimana untuk data yang mereka delegasikan.

Beberapa ide untuk reorganisasi untuk mengurangi panjang:

  • jika Anda membuat sel tampilan tabel dalam kode, pertimbangkan untuk memuatnya dari file nib atau dari storyboard. Storyboard memungkinkan prototipe dan sel tabel statis - periksa fitur-fitur itu jika Anda tidak terbiasa

  • jika metode delegasi Anda mengandung banyak pernyataan 'jika' (atau beralih pernyataan) itu adalah tanda klasik bahwa Anda dapat melakukan beberapa refactoring

Itu selalu terasa agak lucu bagi saya bahwa UITableViewDataSourcebertanggung jawab untuk menangani sedikit data yang benar dan mengonfigurasi tampilan untuk menunjukkannya. Satu titik refactoring yang bagus mungkin untuk mengubah Anda cellForRowAtIndexPathuntuk menangani data yang perlu ditampilkan dalam sel, kemudian mendelegasikan pembuatan tampilan sel ke delegasi lain (misalnya membuat CellViewDelegateatau serupa) yang akan diteruskan dalam item data yang sesuai.

occulus
sumber
Ini jawaban yang bagus. Namun beberapa pertanyaan muncul di kepala saya. Mengapa Anda menemukan banyak pernyataan if (atau switch-statement) sebagai desain yang buruk? Apakah yang Anda maksud sebenarnya adalah banyak pernyataan if dan switch yang bersarang? Bagaimana Anda mempertimbangkan ulang faktor untuk menghindari if- atau beralih-pernyataan?
Johan Karlsson
@JohanKarlsson salah satu teknik adalah melalui polimorfisme. Jika Anda perlu melakukan satu hal dengan satu jenis objek dan sesuatu yang lain dengan jenis yang berbeda, buatlah benda-benda itu kelas yang berbeda dan biarkan mereka memilih pekerjaan untuk Anda.
@ GrahamLee Ya, saya tahu polimorfisme ;-) Namun saya tidak yakin bagaimana menerapkannya dalam konteks ini. Tolong jelaskan ini.
Johan Karlsson
@JohanKarlsson selesai;)
2

Berikut kira-kira apa yang sedang saya lakukan saat menghadapi masalah serupa:

  • Memindahkan operasi terkait data ke kelas XXXDataSource (yang mewarisi dari BaseDataSource: NSObject). BaseDataSource menyediakan beberapa metode yang mudah digunakan seperti - (NSUInteger)rowsInSection:(NSUInteger)sectionNum;, subclass menimpa metode pemuatan data (seperti aplikasi yang biasanya memiliki semacam metode pemuatan cache secara offline seperti - (void)loadDataWithUpdateBlock:(LoadProgressBlock)dataLoadBlock completion:(LoadCompletionBlock)completionBlock;sehingga kami dapat memperbarui UI dengan data cache yang diterima di LoadProgressBlock sementara kami memperbarui info dari jaringan, dan dalam penyelesaian blok kami menyegarkan UI dengan data baru dan menghapus indikator progess, jika ada). Kelas-kelas itu TIDAK sesuai dengan UITableViewDataSourceprotokol.

  • Dalam BaseTableViewController (yang sesuai dengan UITableViewDataSourcedan UITableViewDelegateprotokol) saya punya referensi ke BaseDataSource, yang saya buat selama init pengontrol. Di UITableViewDataSourcebagian controller saya cukup mengembalikan nilai dari dataSource (seperti - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [self.tableViewDataSource sectionsCount]; }).

Berikut ini adalah cellForRow saya di kelas dasar (tidak perlu diganti dalam subkelas):

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *cellIdentifier = [NSString stringWithFormat:@"%@%@", NSStringFromClass([self class]), @"TableViewCell"];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell) {
        cell = [self createCellForIndexPath:indexPath withCellIdentifier:cellIdentifier];
    }
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}

configureCell harus diganti oleh subclass dan createCell mengembalikan UITableViewCell, jadi jika Anda ingin sel kustom, timpa juga.

  • Setelah hal-hal dasar dikonfigurasikan (sebenarnya, dalam proyek pertama yang menggunakan skema seperti itu, setelah itu bagian ini dapat digunakan kembali) yang tersisa untuk BaseTableViewControllersubkelas adalah:

    • Override configureCell (ini biasanya berubah menjadi meminta dataSource untuk objek untuk jalur indeks dan mengumpankannya ke configureWithXXX: metode atau mendapatkan representasi UITableViewCell objek seperti dalam jawaban user4051)

    • Override didSelectRowAtIndexPath: (jelas)

    • Tulislah subclass BaseDataSource yang menangani pekerjaan dengan bagian yang diperlukan dari Model (misalkan ada 2 kelas Accountdan Language, jadi subclass akan menjadi AccountDataSource dan LanguageDataSource).

Dan itu semua untuk bagian tampilan tabel. Saya dapat memposting beberapa kode ke GitHub jika diperlukan.

Sunting: beberapa rekomendasi dapat ditemukan di http://www.objc.io/issue-1/lighter-view-controllers.html (yang memiliki tautan ke pertanyaan ini) dan artikel pendamping tentang tableviewcontrollers.

Timur Kuchkarov
sumber
2

Pandangan saya tentang ini adalah bahwa model perlu memberikan array objek yang disebut ViewModel atau viewData yang dienkapsulasi dalam cellConfigurator. CellConfigurator memegang CellInfo yang diperlukan untuk menghapusnya dan mengkonfigurasi sel. itu memberi sel beberapa data sehingga sel dapat mengkonfigurasi sendiri. ini juga berfungsi dengan bagian jika Anda menambahkan beberapa objek SectionConfigurator yang memegang CellConfigurators. Saya mulai menggunakan ini beberapa waktu lalu pada awalnya hanya memberikan sel viewData dan memiliki kesepakatan ViewController dengan mengeluarkan sel. tapi saya membaca sebuah artikel yang menunjuk ke repo gitHub ini.

https://github.com/fastred/ConfigurableTableViewController

ini dapat mengubah cara Anda mendekati ini.

Pascale Beaulac
sumber
2

Saya baru-baru ini menulis sebuah artikel tentang bagaimana menerapkan delegasi dan sumber data untuk UITableView: http://gosuwachu.gitlab.io/2014/01/12/uitableview-controller/

Gagasan utamanya adalah untuk membagi tanggung jawab menjadi beberapa kelas yang terpisah, seperti pabrik sel, pabrik bagian, dan menyediakan beberapa antarmuka umum untuk model yang akan ditampilkan oleh UITableView. Diagram di bawah ini menjelaskan semuanya:

masukkan deskripsi gambar di sini

Piotr Wach
sumber
Tautan ini tidak lagi berfungsi.
koen
1

Mengikuti prinsip-prinsip SOLID akan menyelesaikan segala jenis masalah seperti ini.

Jika Anda ingin kelas Anda untuk memiliki JUST A TUNGGAL tanggung jawab, Anda harus mendefinisikan terpisah DataSourcedan Delegatekelas dan hanya menyuntikkan mereka ke tableViewpemilik (bisa UITableViewControlleratau UIViewControlleratau apa pun). Inilah cara Anda mengatasi pemisahan kekhawatiran .

Tetapi jika Anda hanya ingin memiliki kode yang bersih dan mudah dibaca dan ingin menyingkirkan file viewController yang besar itu dan Anda menggunakan Swif , Anda dapat menggunakan extensions untuk itu. Ekstensi dari kelas tunggal dapat ditulis dalam file yang berbeda dan semuanya memiliki akses satu sama lain. Tapi ini benar-benar memecahkan masalah SoC nit seperti yang saya sebutkan.

Mojtaba Hosseini
sumber