Pengalaman menggunakan "koordinator" dalam proyek "iOS" nyata

Dunia pemrograman modern kaya akan tren, dan ini benar ganda untuk dunia pemrograman aplikasi "iOS" . Saya harap saya tidak banyak salah dalam menyatakan bahwa salah satu pola arsitektur paling "modis" beberapa tahun terakhir adalah "koordinator". Jadi tim kami beberapa waktu lalu menyadari keinginan yang tak tertahankan untuk mencoba teknik ini pada diri mereka sendiri. Selain itu, kasus yang sangat bagus muncul - perubahan yang signifikan dalam logika dan perencanaan ulang total navigasi dalam aplikasi.

Masalahnya


Sering terjadi bahwa pengendali mulai mengambil terlalu banyak: "memberi perintah" langsung ke UINavigationController , "berkomunikasi" dengan pengendali "saudara" mereka (bahkan menginisialisasi dan meneruskannya ke tumpukan navigasi) - secara umum, ada banyak yang harus dilakukan tentang dari yang seharusnya tidak mereka curigai.

Salah satu cara yang mungkin untuk menghindari hal ini adalah "koordinator". Selain itu, ternyata, cukup nyaman untuk bekerja dan sangat fleksibel: templat ini mampu mengelola acara navigasi kedua modul kecil (mewakili, mungkin, hanya satu layar tunggal), dan seluruh aplikasi (meluncurkan "aliran" sendiri, secara relatif, langsung dari UIApplicationDelegate ).

Ceritanya


Martin Fowler, dalam bukunya Patterns of Enterprise Application Architecture, menyebut pola ini Application Controller . Dan popularizer pertamanya di lingkungan "iOS" adalah Sorush Khanlu : semuanya dimulai dengan laporannya tentang "NSSpain" pada tahun 2015. Kemudian artikel ulasan muncul di situs webnya , yang memiliki beberapa sekuel (misalnya, ini ).

Dan kemudian banyak ulasan diikuti (permintaan "koordinator ios" memberikan puluhan hasil dengan kualitas dan tingkat detail yang berbeda), termasuk bahkan panduan tentang Ray Wenderlich dan sebuah artikel dari Paul Hudson tentang "Peretasan Dengan Swift" sebagai bagian dari serangkaian materi tentang cara menyingkirkan masalah. Pengontrol "Massive".

Ke depan, topik diskusi yang paling menonjol adalah masalah tombol kembali di UINavigationController , klik yang diproses bukan oleh kode kita, tetapi kita hanya bisa mendapatkan panggilan balik .

Sebenarnya, mengapa ini menjadi masalah? Koordinator, seperti objek apa pun, agar dapat hidup dalam memori, perlu beberapa objek lain untuk "memiliki" mereka. Sebagai aturan, ketika membangun sistem navigasi menggunakan koordinator, beberapa koordinator menghasilkan yang lain dan menjaga hubungan yang kuat dengan mereka. Ketika "meninggalkan zona tanggung jawab" dari koordinator asal, kontrol kembali ke koordinator asal, dan memori yang ditempati oleh originator harus dibebaskan.

Sorush memiliki visinya sendiri untuk menyelesaikan masalah ini , dan juga mencatat beberapa pendekatan yang layak . Tapi kita akan kembali ke ini.

Pendekatan pertama


Sebelum mulai menunjukkan kode asli, saya ingin mengklarifikasi bahwa meskipun prinsip-prinsipnya sepenuhnya konsisten dengan prinsip-prinsip yang kami temukan dalam proyek, kutipan dari kode dan contoh penggunaannya disederhanakan dan dikurangi di mana pun tidak mengganggu persepsi mereka.

Ketika kami pertama kali mulai bereksperimen dengan koordinator dalam tim, kami tidak punya banyak waktu dan kebebasan untuk bertindak: perlu mempertimbangkan prinsip-prinsip yang ada dan perangkat navigasi. Opsi implementasi pertama untuk koordinator didasarkan pada "router" yang umum, yang dimiliki dan dioperasikan oleh UINavigationController . Dia tahu bagaimana melakukan semua instance UIViewController yang diperlukan sehubungan dengan navigasi - push / pop, present / dismiss plus manipulasi dengan root controller . Contoh antarmuka router seperti itu:

 import UIKit protocol Router { func present(_ module: UIViewController, animated: Bool) func dismissModule(animated: Bool, completion: (() -> Void)?) func push(_ module: UIViewController, animated: Bool, completion: (() -> Void)?) func popModule(animated: Bool) func setAsRoot(_ module: UIViewController) func popToRootModule(animated: Bool) } 

Implementasi spesifik diinisialisasi dengan turunan UINavigationController dan tidak mengandung sesuatu yang rumit. Satu-satunya batasan: Anda tidak dapat melewatkan instance lain dari UINavigationController sebagai argumen untuk metode antarmuka (untuk alasan yang jelas: UINavigationController tidak dapat berisi UINavigationController dalam tumpukannya - ini adalah batasan UIKit ).

Koordinator, seperti objek apa pun, membutuhkan pemilik - objek lain yang akan menyimpan tautan ke sana. Tautan ke root dapat disimpan oleh objek yang menghasilkannya, tetapi setiap koordinator juga dapat menghasilkan koordinator lain. Oleh karena itu, antarmuka dasar ditulis untuk menyediakan mekanisme manajemen untuk koordinator yang dihasilkan:

 class Coordinator { private var childCoordinators = [Coordinator]() func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } } 

Salah satu keuntungan tersirat dari koordinator adalah enkapsulasi pengetahuan tentang subkelas spesifik dari UIViewController . Untuk memastikan interaksi router dan koordinator, kami memperkenalkan antarmuka berikut:

 protocol Presentable { func presented() -> UIViewController } 

Maka setiap koordinator khusus harus mewarisi dari Coordinator dan mengimplementasikan antarmuka Presentable , dan antarmuka router harus mengambil bentuk berikut:

 protocol Router { func present(_ module: Presentable, animated: Bool) func dismissModule(animated: Bool, completion: (() -> Void)?) func push(_ module: Presentable, animated: Bool, completion: (() -> Void)?) func popModule(animated: Bool) func setAsRoot(_ module: Presentable) func popToRootModule(animated: Bool) } 

(Pendekatan dengan Presentable juga memungkinkan Anda menggunakan koordinator di dalam modul yang ditulis untuk berinteraksi langsung dengan instance UIViewController , tanpa menjadikannya (modul) pemrosesan radikal.)

Contoh singkat dari semua tindakan ini:

 final class FirstCoordinator: Coordinator, Presentable { func presented() -> UIViewController { return UIViewController() } } final class SecondCoordinator: Coordinator, Presentable { func presented() -> UIViewController { return UIViewController() } } let nc = UINavigationController() let router = RouterImpl(navigationController: nc) // Router implementation. router.setAsRoot(FirstCoordinator()) router.push(SecondCoordinator(), animated: true, completion: nil) router.popToRootModule(animated: true) 

Perkiraan selanjutnya


Dan suatu hari tiba saatnya untuk perubahan total navigasi dan kebebasan berekspresi mutlak! Saat ketika tidak ada yang menghalangi kami untuk mencoba menerapkan navigasi pada koordinator menggunakan metode start() didambakan - versi yang memikat awalnya dengan kesederhanaan dan keringkasannya.

Fitur Coordinator disebutkan di atas jelas tidak akan berlebihan. Tetapi metode yang sama perlu ditambahkan ke antarmuka umum:

 protocol Coordinator { func add(dependency coordinator: Coordinator) func remove(dependency coordinator: Coordinator) func start() } class BaseCoordinator: Coordinator { private var childCoordinators = [Coordinator]() func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } func start() { } } 

"Swift" tidak menawarkan kemampuan untuk mendeklarasikan kelas abstrak (karena lebih berorientasi pada pendekatan berorientasi protokol daripada yang lebih klasik, berorientasi objek ), oleh karena itu metode start() dapat dibiarkan dengan implementasi atau dorongan kosong ada sesuatu seperti fatalError(_:file:line:) (memaksa untuk menimpa metode ini dengan ahli waris). Secara pribadi, saya lebih suka opsi pertama.

Tetapi Swift memiliki peluang besar untuk menambahkan metode implementasi standar ke metode protokol, jadi pemikiran pertama, tentu saja, bukan untuk mendeklarasikan kelas dasar, tetapi untuk melakukan sesuatu seperti ini:

 extension Coordinator { func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } } 

Tetapi ekstensi protokol tidak dapat mendeklarasikan bidang yang disimpan, dan implementasi kedua metode ini jelas harus didasarkan pada properti tipe tersimpan pribadi.

Dasar dari koordinator tertentu akan terlihat seperti ini:

 final class SomeCoordinator: BaseCoordinator { override func start() { // ... } } 

Setiap dependensi yang diperlukan agar koordinator berfungsi dapat ditambahkan ke inisialisasi. Sebagai contoh umum, instance dari UINavigationController .

Jika ini adalah koordinator root yang tanggung jawabnya adalah memetakan UIViewController root, koordinator dapat, misalnya, menerima instance baru dari UINavigationController dengan tumpukan kosong.

Saat memproses acara (lebih lanjut tentang itu nanti), koordinator dapat meneruskan UINavigationController ini lebih lanjut ke koordinator lain yang dihasilkannya. Dan mereka juga dapat melakukan dengan kondisi navigasi saat ini apa yang mereka butuhkan: "push", "present", dan setidaknya mengganti seluruh tumpukan navigasi.

Kemungkinan peningkatan antarmuka


Ternyata nanti, tidak setiap koordinator akan menghasilkan koordinator lain, jadi tidak semuanya tergantung pada kelas dasar seperti itu. Oleh karena itu, salah satu kolega dari tim terkait menyarankan untuk menyingkirkan warisan dan memperkenalkan antarmuka manajer dependensi sebagai ketergantungan eksternal:

 protocol CoordinatorDependencies { func add(dependency coordinator: Coordinator) func remove(dependency coordinator: Coordinator) } final class DefaultCoordinatorDependencies: CoordinatorDependencies { private let dependencies = [Coordinator]() func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } } final class SomeCoordinator: Coordinator { private let dependencies: CoordinatorDependencies init(dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { dependencies = dependenciesManager } func start() { // ... } } 

Menangani acara yang dibuat pengguna


Nah, koordinator menciptakan dan entah bagaimana memulai pemetaan baru. Kemungkinan besar, pengguna melihat layar dan melihat serangkaian elemen visual yang dengannya ia dapat berinteraksi: tombol, bidang teks, dll. Beberapa dari mereka memprovokasi acara navigasi, dan mereka harus dikendalikan oleh koordinator yang membuat pengontrol ini. Untuk mengatasi masalah ini, kami menggunakan delegasi tradisional.

Misalkan ada subkelas dari UIViewController :

 final class SomeViewController: UIViewController { } 

Dan koordinator yang menambahkannya ke stack:

 final class SomeCoordinator: Coordinator { private let dependencies: CoordinatorDependencies private weak var navigationController: UINavigationController? init(navigationController: UINavigationController, dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { self.navigationController = navigationController dependencies = dependenciesManager } func start() { let vc = SomeViewController() navigationController?.pushViewController(vc, animated: true) } } 

Kami mendelegasikan pemrosesan kejadian pengontrol yang sesuai ke koordinator yang sama. Di sini, pada kenyataannya, skema klasik digunakan:

 protocol SomeViewControllerRoute: class { func onSomeEvent() } final class SomeViewController: UIViewController { private weak var route: SomeViewControllerRoute? init(route: SomeViewControllerRoute) { self.route = route super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @IBAction private func buttonAction() { route?.onSomeEvent() } } final class SomeCoordinator: Coordinator { private let dependencies: CoordinatorDependencies private weak var navigationController: UINavigationController? init(navigationController: UINavigationController, dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { self.navigationController = navigationController dependencies = dependenciesManager } func start() { let vc = SomeViewController(route: self) navigationController?.pushViewController(vc, animated: true) } } extension SomeCoordinator: SomeViewControllerRoute { func onSomeEvent() { // ... } } 

Menangani tombol kembali


Ulasan bagus lain dari template arsitektur yang dibahas diterbitkan oleh Paul Hudson di situs webnya "Hacking dengan Swift," bahkan ada yang mengatakan panduan. Ini juga berisi penjelasan yang sederhana, langsung, dari salah satu solusi yang mungkin untuk masalah tombol kembali yang disebutkan di atas: koordinator (jika perlu) menyatakan dirinya sebagai delegasi dari instance UINavigationController diteruskan kepadanya dan memantau acara yang menarik bagi kami.

Pendekatan ini memiliki kelemahan kecil: hanya NSObject dapat menjadi delegasi UINavigationController .

Jadi, ada koordinator yang memunculkan koordinator lain. Yang lain ini, dengan memanggil start() menambahkan semacam UIViewController ke tumpukan UINavigationController . Dengan mengeklik tombol kembali pada UINavigationBar yang perlu Anda lakukan adalah membiarkan koordinator asal tahu bahwa koordinator yang dihasilkan telah menyelesaikan pekerjaannya ("flow"). Untuk melakukan ini, kami memperkenalkan alat delegasi lain: delegasi dialokasikan untuk setiap koordinator yang dihasilkan, antarmuka yang diimplementasikan oleh koordinator pembangkit:

 protocol CoordinatorFlowListener: class { func onFlowFinished(coordinator: Coordinator) } final class MainCoordinator: NSObject, Coordinator { private let dependencies: CoordinatorDependencies private let navigationController: UINavigationController init(navigationController: UINavigationController, dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { self.navigationController = navigationController dependencies = dependenciesManager super.init() } func start() { let someCoordinator = SomeCoordinator(navigationController: navigationController, flowListener: self) dependencies.add(someCoordinator) someCoordinator.start() } } extension MainCoordinator: CoordinatorFlowListener { func onFlowFinished(coordinator: Coordinator) { dependencies.remove(coordinator) // ... } } final class SomeCoordinator: NSObject, Coordinator { private weak var flowListener: CoordinatorFlowListener? private weak var navigationController: UINavigationController? init(navigationController: UINavigationController, flowListener: CoordinatorFlowListener) { self.navigationController = navigationController self.flowListener = flowListener } func start() { // ... } } extension SomeCoordinator: UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { guard let fromVC = navigationController.transitionCoordinator?.viewController(forKey: .from) else { return } if navigationController.viewControllers.contains(fromVC) { return } if fromVC is SomeViewController { flowListener?.onFlowFinished(coordinator: self) } } } 

Dalam contoh di atas, MainCoordinator tidak melakukan apa-apa: itu hanya meluncurkan aliran koordinator lain - dalam kehidupan nyata, tentu saja, itu tidak berguna. Dalam aplikasi kami, MainCoordinator menerima data dari luar, yang menurutnya menentukan status aplikasi yang diotorisasi, tidak diotorisasi, dll. - dan layar mana yang perlu ditampilkan. Bergantung pada ini, itu meluncurkan aliran koordinator yang sesuai. Jika koordinator asal telah menyelesaikan pekerjaannya, koordinator utama menerima sinyal tentang hal ini melalui CoordinatorFlowListener dan, katakanlah, meluncurkan aliran koordinator lain.

Kesimpulan


Solusi yang biasa, tentu saja, memiliki sejumlah kelemahan (seperti solusi untuk masalah apa pun).

Ya, Anda harus menggunakan banyak delegasi, tetapi sederhana dan memiliki arah tunggal: dari yang dihasilkan ke yang dihasilkan (dari controller ke koordinator, dari koordinator yang dihasilkan ke yang dihasilkan).

Ya, untuk melarikan diri dari kebocoran memori, Anda harus menambahkan metode delegasi UINavigationController dengan implementasi yang hampir sama untuk setiap koordinator. (Pendekatan pertama tidak memiliki kelemahan ini, tetapi lebih murah hati membagikan pengetahuan internalnya tentang penunjukan koordinator tertentu.)

Tetapi kelemahan terbesar dari pendekatan ini adalah bahwa, dalam kehidupan nyata, koordinator, sayangnya, akan tahu sedikit lebih banyak tentang dunia di sekitar mereka daripada yang kita inginkan. Lebih tepatnya, mereka harus menambahkan elemen logika yang tergantung pada kondisi eksternal, yang tidak diketahui langsung oleh koordinator. Pada dasarnya, inilah yang sebenarnya terjadi ketika metode start() onFlowFinished(coordinator:) atau panggilan balik onFlowFinished(coordinator:) . Dan apa pun bisa terjadi di tempat-tempat ini, dan itu akan selalu menjadi perilaku "hardcoded": menambahkan controller ke stack, mengganti stack, kembali ke root controller - apa pun. Dan semua ini tidak tergantung pada kompetensi kontroler saat ini, tetapi pada kondisi eksternal.

Namun demikian, kodenya β€œcantik” dan ringkas, sangat bagus untuk bekerja dengannya, dan navigasi melalui kodenya jauh lebih mudah. Tampaknya bagi kita bahwa dengan kekurangan-kekurangan yang disebutkan itu, dengan menyadarinya, sangat mungkin untuk ada.
Terima kasih sudah membaca ke tempat ini! Saya harap mereka belajar sesuatu yang berguna untuk diri mereka sendiri. Dan jika Anda tiba-tiba ingin "lebih dari saya", maka di sini ada tautan ke Twitter saya .

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


All Articles