Masalah pola koordinator dan apa yang harus dilakukan RouteComposer dengannya

Saya melanjutkan serangkaian artikel tentang perpustakaan RouteComposer yang kami gunakan, dan hari ini saya ingin berbicara tentang pola Koordinator. Saya diminta untuk menulis artikel ini dengan diskusi tentang salah satu artikel tentang pola tersebut. Koordinator di sini di Habrรฉ.


Pola Koordinator, yang diperkenalkan belum lama ini, semakin populer di kalangan pengembang iOS, dan, secara umum, jelas mengapa. Karena alat-alat di luar kotak yang disediakan UIKit bukan kekacauan universal.


gambar


Saya telah mengajukan pertanyaan tentang fragmentasi cara saya menyusun tampilan pengontrol pada stack, dan untuk menghindari pengulangan, Anda dapat membacanya di sini .


Mari kita jujur. Pada suatu titik, Epole menyadari bahwa dengan meletakkan pengontrol di pusat pengembangan aplikasi, ia tidak menawarkan cara yang masuk akal untuk membuat atau mentransfer data di antara mereka, dan, setelah mempercayakan solusi untuk masalah ini kepada pengembang, itu di-autocompleted dari Xcode, dan mungkin ke pengembang UISearchConnroller, di beberapa titik memperkenalkan storyboard dan segues kepada kami. Kemudian, Epolus menyadari bahwa dia menulis aplikasi yang hanya terdiri dari 2 layar saja, dan pada iterasi berikutnya dia menyarankan kemungkinan memecah storyboard menjadi beberapa komponen, karena Xcode mulai crash ketika storyboard mencapai ukuran tertentu. Segues telah berubah seiring dengan konsep ini, dalam beberapa iterasi yang tidak terlalu kompatibel satu sama lain. Dukungan mereka dijahit dengan ketat ke dalam kelas UIViewController besar, dan, pada akhirnya, kami mendapatkan apa yang kami dapatkan. Ini dia:


 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "showDetail" { if let indexPath = tableView.indexPathForSelectedRow { let object = objects[indexPath.row] as! NSDate let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController controller.detailItem = object controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem controller.navigationItem.leftItemsSupplementBackButton = true } } } 

Jumlah siaran gaya dalam blok kode ini luar biasa, seperti halnya konstanta string di storyboard itu sendiri, untuk melacak Xcode mana yang tidak menawarkan sarana sama sekali. Dan keinginan sekecil apa pun untuk mengubah sesuatu dalam proses navigasi akan memungkinkan Anda untuk mengkompilasi proyek tanpa usaha apa pun dan itu akan crash dengan bang in runtime tanpa peringatan sedikit pun dari Xcode. Berikut adalah WYSIWYG pada akhirnya ternyata. Apa yang Anda lihat adalah apa yang Anda dapatkan.


Anda dapat berdebat lama tentang pesona panah abu-abu ini di storyboard yang seharusnya menunjukkan kepada seseorang koneksi di antara layar, tetapi, seperti yang telah ditunjukkan oleh praktik saya, saya sengaja mewawancarai beberapa pengembang yang akrab dari perusahaan yang berbeda, segera setelah proyek tumbuh melampaui 5-6 layar, orang-orang mencoba menemukan solusi yang lebih dapat diandalkan dan akhirnya mulai menjaga struktur tumpukan pengontrol tampilan di kepalaku. Dan jika dukungan untuk iPad dan model navigasi lainnya atau dukungan untuk dorongan ditambahkan, maka semuanya menyedihkan di sana.


Sejak itu, beberapa upaya telah dilakukan untuk memecahkan masalah ini, beberapa di antaranya menghasilkan kerangka kerja yang terpisah, beberapa di pola arsitektur yang terpisah, sejak membuat pengontrol tampilan di dalam pengontrol tampilan membuat potongan kode yang besar dan kikuk ini bahkan lebih.


Mari kita kembali ke pola Koordinator. Untuk alasan yang jelas, Anda tidak akan menemukan deskripsinya di Wikipedia karena itu bukan pola pemrograman / desain standar. Alih-alih, ini adalah semacam abstraksi, yang menyarankan untuk menyembunyikan di bawah kap semua kode "jelek" ini untuk membuat dan memasukkan lilitan pengendali baru pada tumpukan, menyimpan referensi ke wadah pengontrol dan mendorong data di antara pengontrol. Artikel yang paling cocok menggambarkan proses ini saya sebut artikel di raywenderlich.com . Itu mulai menjadi populer setelah konferensi NSSpain 2015, ketika masyarakat umum diberitahu tentang hal itu. Secara lebih rinci apa yang diceritakan dapat ditemukan di sini dan di sini .


Saya akan menjelaskan secara singkat apa itu sebelum melanjutkan.


Pola Koordinator dalam semua interpretasi kira-kira cocok dengan gambar ini:



Artinya, koordinator adalah protokol


 protocol Coordinator { func start() } 

Dan semua kode jelek seharusnya disembunyikan di fungsi start . Koordinator, di samping itu, dapat memiliki tautan ke koordinator anak, yaitu, mereka memiliki beberapa kemampuan komposisi, dan, misalnya, Anda dapat mengganti satu implementasi dengan yang lain. Artinya, kedengarannya cukup elegan.


Namun, kegilaan dimulai segera:


  1. Beberapa implementasi mengusulkan untuk mengubah Koordinator dari pola pembangkit tertentu menjadi sesuatu yang lebih masuk akal, mengawasi tumpukan pengontrol dan menjadikannya delegasi wadah , misalnya, UINavigationController , untuk memproses mengklik tombol Kembali atau geser kembali dan hapus koordinator anak. Untuk alasan alami, hanya satu objek yang dapat menjadi delegasi, yang membatasi kontrol wadah itu sendiri dan mengarah pada fakta bahwa logika ini terletak pada koordinator, atau menciptakan kebutuhan untuk mendelegasikan logika ini lebih jauh kepada seseorang yang berada jauh di bawah daftar.
  2. Seringkali logika untuk membuat pengontrol berikutnya tergantung pada logika bisnis . Misalnya, untuk menuju ke layar berikutnya, pengguna harus masuk ke sistem. Jelas, ini adalah proses asinkron, yang termasuk menghasilkan beberapa layar perantara dengan formulir login, proses login itu sendiri dapat berakhir dengan sukses atau tidak. Untuk menghindari mengubah Koordinator menjadi Koordinator Massive (mirip dengan Massive View Controller), kita perlu dekomposisi. Artinya, Anda perlu membuat Koordinator Koordinator.
  3. Masalah lain yang dihadapi oleh koordinator adalah bahwa mereka pada dasarnya adalah pembungkus untuk pengontrol tampilan wadah seperti UINavigationController , UITabBarController dan sebagainya. Dan seseorang harus menyediakan tautan ke pengontrol ini . Jika dengan koordinator anak semuanya menjadi kurang jelas, maka dengan koordinator awal rantai, tidak semuanya begitu sederhana. Plus, ketika mengubah navigasi, misalnya untuk tes A / B, refactoring dan adaptasi dari koordinator tersebut menghasilkan sakit kepala yang terpisah. Apalagi jika jenis wadahnya berubah.
  4. Semua ini menjadi lebih rumit ketika aplikasi mulai mendukung acara eksternal yang menghasilkan pengontrol tampilan. Seperti pemberitahuan push atau tautan universal (pengguna mengklik tautan dalam surat itu dan melanjutkan di layar aplikasi yang sesuai). Di sini timbul ketidakpastian lain yang pola Koordinatornya tidak memiliki jawaban yang pasti. Anda harus tahu persis layar mana yang sedang digunakan pengguna untuk menunjukkan kepadanya layar berikutnya yang diminta oleh peristiwa eksternal.
    Contoh paling sederhana adalah aplikasi obrolan yang terdiri dari 3 layar - daftar obrolan, obrolan itu sendiri yang didorong ke dalam navigasi pengontrol daftar obrolan dan layar pengaturan ditampilkan secara moderen. Pengguna dapat berada di salah satu layar ini ketika ia menerima pemberitahuan push dan mengetuknya. Dan di sini ketidakpastian dimulai, jika dia ada dalam daftar obrolan, Anda perlu memulai obrolan dengan pengguna khusus ini, jika dia sudah ada dalam obrolan, maka Anda perlu beralih, dan jika dia sudah dalam obrolan dengan pengguna ini, maka jangan lakukan apa-apa dan perbarui, jika pengguna aktif layar pengaturan - itu, tampaknya Anda harus menutup dan mengikuti langkah-langkah sebelumnya. Atau mungkin tidak menutup dan hanya menunjukkan obrolan secara moderat atas pengaturan? Dan jika pengaturannya di tab lain, dan bukan modal? Ini if/else mulai tersebar di koordinator atau pergi ke Mega-Coordinator lain dalam bentuk sepotong spageti. Selain itu, ini merupakan iterasi aktif pada tumpukan tampilan pengontrol dan upaya untuk menentukan di mana pengguna saat ini, atau upaya untuk membangun beberapa jenis aplikasi yang memantau status mereka, tetapi ini bukan tugas yang mudah, hanya berdasarkan pada sifat tumpukan pengendali tampilan itu sendiri.
  5. Dan ceri pada kue itu adalah gangguan UIKit . Contoh sepele: UITabBarController dengan UINavigationController di tab kedua dengan beberapa UIViewController lainnya. Pengguna di tab pertama menyebabkan peristiwa tertentu yang mengharuskan pengalihan tab dan UINavigationController pengontrol tampilan lain ke UINavigationController . Semua ini perlu dilakukan hanya dalam urutan seperti itu. Jika pengguna belum pernah membuka tab kedua sebelum ini dan UINavigationController tidak dipanggil di viewDidLoad metode push tidak akan berfungsi hanya menyisakan pesan tidak jelas di konsol. Artinya, koordinator tidak bisa hanya menjadi pendengar peristiwa dalam contoh ini, mereka harus bekerja dalam urutan tertentu. Jadi mereka harus memiliki pengetahuan satu sama lain. Dan ini sudah bertentangan dengan pernyataan pertama dari pola Koordinator, bahwa koordinator tidak tahu apa-apa tentang koordinator pembangkit dan hanya terhubung dengan yang anak-anak. Dan juga membatasi pertukaran mereka.

Daftar ini dapat dilanjutkan, tetapi secara umum jelas bahwa pola Koordinator adalah solusi yang agak terbatas dan skalabel. Jika Anda melihatnya tanpa kacamata merah muda, maka itu adalah cara penguraian bagian dari logika, yang biasanya ditulis di dalam UIViewController besar-besaran, ke dalam kelas lain. Semua upaya untuk membuatnya lebih dari sekadar beberapa pabrik generatif dan memperkenalkan logika lain di sana tidak berakhir dengan baik.


Perlu dicatat bahwa ada perpustakaan berdasarkan pola ini, yang, dengan satu atau lain cara, memungkinkan untuk mengurangi sebagian kerugian di atas. Saya akan menyebutkan XCoordinator dan RxFlow .


Apa yang telah kita lakukan


Setelah bermain dalam proyek yang kami dapatkan dari tim lain untuk dukungan dan pengembangan, dengan koordinator dan Router "buyut" mereka yang disederhanakan dalam pendekatan arsitektur VIPER , kami kembali ke pendekatan yang bekerja dengan baik dalam proyek besar sebelumnya di perusahaan kami. Pendekatan ini tidak memiliki nama. Itu terletak di permukaan. Ketika kami memiliki waktu luang, itu dikompilasi ke perpustakaan RouteComposer terpisah yang sepenuhnya menggantikan koordinator dan terbukti lebih fleksibel.


Apa pendekatan ini? Dalam hal itu, untuk mengandalkan stack (tree) saya memutar controller seperti apa adanya. Agar tidak membuat entitas yang tidak perlu yang perlu dipantau. Jangan menyimpan atau melacak kondisi.


Mari kita lihat entitas UIKit lebih dekat dan coba cari tahu apa yang kita miliki di garis bawah dan apa yang bisa kita kerjakan:


  1. Tumpukan pengontrol adalah pohon. Ada pengontrol tampilan root yang memiliki pengontrol tampilan anak. Pengontrol tampilan yang disajikan secara digital adalah kasus khusus pengontrol tampilan anak, karena mereka juga memiliki pengikatan pada pengontrol tampilan yang dihasilkan. Semuanya tersedia di luar kotak.
  2. Saya perlu membuat entitas pengontrol. Mereka semua memiliki konstruktor yang berbeda, mereka dapat dibuat menggunakan file Xib atau Storyboards. Mereka memiliki parameter input yang berbeda. Tetapi mereka bersatu dalam hal bahwa mereka perlu diciptakan. Jadi, di sini kita dapat menggunakan pola Pabrik , yang tahu cara membuat pengontrol tampilan yang diinginkan. Setiap pabrik mudah ditutup dengan unit test yang komprehensif dan tidak tergantung pada yang lain.
  3. Kami membagi pengontrol tampilan menjadi 2 kelas: 1. Hanya melihat pengontrol, 2. Pengontrol tampilan kontainer (Container View Controller) . Pengontrol tampilan wadah berbeda dari yang biasa di mana mereka dapat mengandung pengontrol tampilan anak - juga wadah atau yang sederhana. Pengontrol tampilan seperti itu tersedia di luar kotak: UINavigationController , UITabBarController dan sebagainya, tetapi juga dapat dibuat oleh pengguna. Jika kita mengabaikannya, kita dapat menemukan properti berikut di semua wadah: 1. Mereka memiliki daftar semua pengontrol yang dikandungnya. 2. Satu atau lebih pengontrol saat ini terlihat. 3. Mereka mungkin diminta untuk membuat salah satu dari pengontrol ini terlihat. Ini semua yang dapat dilakukan oleh pengontrol UIKit . Mereka hanya memiliki metode berbeda untuk ini. Tetapi hanya ada 3 tugas.
  4. Untuk menyematkan view controller buatan pabrik, metode tampilan induk dari controller adalah UINavigationController.pushViewController(...) , UITabBarController.selectedViewController = ... , UIViewController.present(...) dan sebagainya. Anda mungkin memperhatikan bahwa 2 pengontrol tampilan selalu diperlukan, satu sudah ada di stack, dan satu lagi yang perlu disematkan di stack. Bungkus ini dalam pembungkus dan sebut itu Aksi (Aksi) . Setiap tindakan mudah ditutup dengan unit test yang komprehensif dan masing-masing independen dari yang lain.
  5. Dari penjelasan di atas, ternyata dengan menggunakan entitas yang telah disiapkan, Anda dapat membangun rantai konfigurasi Factory -> Action -> Factory -> Action -> Factory dan, setelah menyelesaikannya, Anda dapat membangun pohon tampilan pengontrol dari kompleksitas apa pun. Anda hanya perlu menentukan titik masuk. Titik input ini biasanya berupa rootViewController yang dimiliki oleh UIWindow atau pengontrol tampilan saat ini, yang merupakan cabang paling ekstrim dari pohon. Artinya, konfigurasi seperti itu ditulis dengan benar sebagai: Memulai ViewController -> Action -> Factory -> ... -> Factory .
  6. Selain konfigurasi, Anda akan memerlukan beberapa entitas yang tahu cara memulai dan membangun konfigurasi yang disediakan. Kami akan menyebutnya Router . Tidak memiliki status, tidak memiliki tautan apa pun. Ini memiliki satu metode yang konfigurasi dilewatkan dan secara berurutan melakukan langkah-langkah konfigurasi.
  7. Tambahkan tanggung jawab ke router dengan menambahkan kelas Interceptors ke rantai konfigurasi. Interceptors dimungkinkan dari 3 jenis: 1. Diluncurkan sebelum memulai navigasi. Kami menghapus tugas otentikasi pengguna di sistem dan tugas asinkron lainnya di dalamnya. 2. Jalankan pada saat pembuatan view controller untuk mengatur nilai. 3. Dilakukan setelah navigasi dan melakukan berbagai tugas analitis. Setiap entitas mudah dicakup oleh unit test dan tidak tahu bagaimana ia akan digunakan dalam konfigurasi. Dia hanya memiliki satu tanggung jawab dan dia memenuhinya. Yaitu, konfigurasi untuk navigasi yang kompleks mungkin terlihat seperti [Tugas Pra-Navigasi ...] -> Memulai ViewController -> Action -> (Factory + [ContextTask ...]) -> ... -> (Factory + [ContextTask ...]) -> [Post NavigationTask ...] . Artinya, semua tugas akan dilakukan oleh router secara berurutan, berkinerja pada gilirannya kecil, entitas atom mudah dibaca.
  8. Tugas terakhir yang tidak dapat diselesaikan dengan konfigurasi tetap - ini adalah keadaan aplikasi saat ini. Bagaimana jika kita perlu membangun bukan seluruh rantai konfigurasi, tetapi hanya sebagian saja, karena sebagian pengguna melewatinya? Pertanyaan ini selalu dapat dijawab dengan jelas oleh pohon pengontrol tampilan. Karena jika bagian dari rantai sudah dibangun, itu sudah ada di pohon. Ini berarti bahwa jika setiap pabrik dalam rantai dapat menjawab pertanyaan apakah itu dibangun atau tidak, maka router akan dapat memahami bagian rantai yang mana yang perlu diselesaikan. Tentu saja, ini bukan tugas pabrik, jadi entitas atom lain diperkenalkan - Finder, dan konfigurasi apa pun tampak seperti ini: [Tugas Pra-navigasi ...] -> Memulai ViewController -> Action -> (Finder / Pabrik + [ContextTask ...]) -> ... -> (Finder / Pabrik + [ContextTask ...]) -> [Post NavigationTask ...] . Jika router mulai membacanya dari akhir, maka salah satu Pencari akan memberitahunya bahwa itu sudah dibangun, dan router dari titik ini akan mulai membangun rantai kembali. Jika tidak satu pun dari mereka menemukan dirinya di pohon, maka Anda perlu membangun seluruh rantai dari pengontrol awal.
    gambar
  9. Konfigurasi harus diketik dengan kuat. Oleh karena itu, setiap entitas bekerja dengan hanya satu jenis tampilan pengontrol, satu jenis data dan konfigurasi sepenuhnya bergantung pada kemampuan swift untuk bekerja dengan jenis terkait . Kami ingin mengandalkan kompiler, bukan pada runtime. Pengembang dapat dengan sengaja melemahkan pengetikan, tetapi tidak sebaliknya.

Contoh konfigurasi seperti itu:


 let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory()) .add(LoginInterceptor<UUID>()) // Have to specify the context type till https://bugs.swift.org/browse/SR-8719 is fixed .add(ProductViewControllerContextTask()) .add(ProductViewControllerPostTask(analyticsManager: AnalyticsManager.sharedInstance)) .using(UINavigationController.push()) .from(NavigationControllerStep()) .using(GeneralActions.presentModally()) .from(GeneralStep.current()) .assemble() 

Item yang dijelaskan di atas mencakup seluruh perpustakaan dan menjelaskan pendekatannya. Yang tersisa bagi kami adalah menyediakan konfigurasi berantai yang akan dijalankan router saat pengguna mengklik tombol atau peristiwa eksternal terjadi. Jika ini adalah berbagai jenis perangkat, misalnya iPhone atau iPad, maka kami akan menyediakan konfigurasi transisi yang berbeda menggunakan polimorfisme. Jika kita memiliki pengujian A / B, hal yang sama. Kita tidak perlu memikirkan keadaan aplikasi pada saat memulai navigasi, kita perlu memastikan bahwa konfigurasi ditulis dengan benar pada awalnya, dan kami yakin bahwa router akan membangunnya.


Pendekatan yang dideskripsikan lebih rumit daripada abstraksi atau pola tertentu, tetapi kami belum menghadapi masalah di mana itu tidak akan cukup. Tentu saja, RouteComposer memerlukan beberapa studi dan pemahaman tentang cara kerjanya. Namun, ini jauh lebih mudah daripada mempelajari dasar-dasar AutoLayout atau RunLoop. Tidak ada matematika yang lebih tinggi.


Pustaka, serta implementasi router yang disediakan untuknya, tidak menggunakan trik objektif dengan runtime dan sepenuhnya mengikuti semua konsep Cocoa Touch, hanya membantu memecah proses komposisi menjadi langkah-langkah dan mengeksekusi mereka dalam urutan yang diberikan. Perpustakaan diuji dengan versi iOS 9 hingga 12.


Rincian lebih lanjut dapat ditemukan di artikel sebelumnya:
Komposisi UIViewControllers dan navigasi di antara mereka (dan tidak hanya) / majalah geek
Contoh konfigurasi UIViewControllers menggunakan RouteComposer / geek magazine


Terima kasih atas perhatian anda Saya akan dengan senang hati menjawab pertanyaan di komentar.

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


All Articles