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) {
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)
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) {
"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) {
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) {
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)
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 .