
Dalam artikel ini saya ingin berbagi pengalaman yang telah berhasil kami gunakan selama beberapa tahun di aplikasi iOS kami, 3 di antaranya saat ini ada di Appstore. Pendekatan ini telah bekerja dengan baik dan kami baru saja memisahkannya dari sisa kode dan mendesainnya menjadi pustaka RouteComposer yang terpisah, yang akan dibahas pada kenyataannya .
https://github.com/ekazaev/route-composer
Tetapi, sebagai permulaan, mari kita coba mencari tahu apa yang dimaksud dengan komposisi pengontrol tampilan di iOS.
Sebelum melanjutkan ke penjelasan itu sendiri, saya mengingatkan Anda bahwa di iOS itu paling sering dipahami sebagai view controller atau UIViewController
. Ini adalah kelas yang diwarisi dari UIViewController
standar, yang merupakan pengontrol pola MVC dasar yang direkomendasikan Apple untuk digunakan untuk mengembangkan aplikasi iOS.
Anda dapat menggunakan pola arsitektur alternatif seperti MVVM, VIP, VIPER, tetapi di dalamnya UIViewController
akan terlibat dengan satu atau lain cara, yang berarti bahwa perpustakaan ini dapat digunakan bersama mereka. Esensi dari UIViewController
digunakan untuk mengontrol UIView
, yang paling sering mewakili layar atau bagian penting dari layar, memproses peristiwa dari itu dan menampilkan beberapa data di dalamnya.

Semua UIViewController
dapat dibagi secara kondisional menjadi Pengendali Tampilan Normal , yang bertanggung jawab untuk beberapa area yang terlihat di layar, dan Pengontrol Tampilan Kontainer , yang, selain menampilkan diri mereka sendiri dan beberapa kontrol mereka, juga dapat menampilkan pengontrol tampilan anak yang terintegrasi di dalamnya dengan satu atau lain cara. .
Pengontrol tampilan wadah standar yang disertakan dengan Cocoa Touch meliputi: UINavigationConroller
, UITabBarController
, UISplitController
, UIPageController
, dan beberapa lainnya. Selain itu, pengguna dapat membuat pengontrol tampilan wadah kustom mereka sendiri mengikuti aturan Cocoa Touch yang dijelaskan dalam dokumentasi Apple.
Proses memperkenalkan pengendali tampilan standar ke pengendali tampilan wadah, serta integrasi pengendali tampilan ke tumpukan pengendali, kami akan memanggil komposisi dalam artikel ini.
Mengapa, kemudian, solusi standar untuk komposisi pengontrol tampilan ternyata tidak optimal bagi kami, dan kami mengembangkan perpustakaan yang memfasilitasi pekerjaan kami.
Mari kita lihat komposisi beberapa pengontrol tampilan wadah standar sebagai contoh:
Contoh komposisi dalam wadah standar
UINavigationController

let tableViewController = UITableViewController(style: .plain)
UITabBarController

let firstViewController = UITableViewController(style: .plain) let secondViewController = UIViewController()
UISplitViewController

let firstViewController = UITableViewController(style: .plain) let secondViewController = UIViewController()
Contoh integrasi (komposisi) pengendali tampilan pada stack
Menginstal root pengontrol tampilan
let window: UIWindow =
Modal presentasi pengendali tampilan
window.rootViewController.present(splitViewController, animated: animated, completion: nil)
Mengapa kami memutuskan untuk membuat perpustakaan untuk komposisi
Seperti yang dapat Anda lihat dari contoh di atas, tidak ada cara tunggal untuk mengintegrasikan pengontrol tampilan konvensional ke dalam wadah, seperti halnya tidak ada cara tunggal untuk membangun setumpuk pengontrol tampilan. Dan, jika Anda ingin sedikit mengubah tata letak aplikasi Anda atau cara Anda menavigasi di dalamnya, Anda akan memerlukan perubahan signifikan pada kode aplikasi, Anda juga akan memerlukan tautan ke objek kontainer sehingga Anda dapat memasukkan pengontrol tampilan Anda ke dalamnya, dll. Artinya, metode standar itu sendiri menyiratkan jumlah pekerjaan yang cukup besar, serta keberadaan tautan untuk melihat pengontrol untuk menghasilkan tindakan dan presentasi dari pengontrol lain.
Semua ini menambah sakit kepala pada berbagai metode penautan-dip ke aplikasi (misalnya, menggunakan tautan Universal), karena Anda harus menjawab pertanyaan: bagaimana jika pengontrol perlu diperlihatkan kepada pengguna karena dia mengklik tautan di safari sudah ditunjukkan, atau saya melihat pengontrol yang seharusnya menunjukkan bahwa itu belum dibuat , memaksa Anda untuk berjalan melalui pohon pengontrol tampilan dan menulis kode yang terkadang mata Anda mulai berdarah dan yang coba disembunyikan oleh pengembang iOS. Selain itu, tidak seperti arsitektur Android di mana setiap layar dibangun secara terpisah, di iOS, untuk menunjukkan beberapa bagian dari aplikasi segera setelah peluncuran, mungkin perlu untuk membangun setumpuk pengendali yang agak besar yang akan disembunyikan di bawah yang Anda perlihatkan berdasarkan permintaan.
Akan luar biasa hanya untuk memanggil metode seperti goToAccount()
, goToMenu()
atau goToProduct(withId: "012345")
ketika pengguna mengklik tombol atau ketika aplikasi goToProduct(withId: "012345")
tautan universal dari aplikasi lain dan tidak berpikir tentang mengintegrasikan pengontrol tampilan ini ke stack, mengetahui bahwa pembuat view controller ini telah menyediakan implementasi ini.
Selain itu, seringkali, aplikasi kami terdiri dari sejumlah besar layar yang dikembangkan oleh tim yang berbeda, dan untuk menuju ke salah satu layar selama proses pengembangan, Anda harus melalui layar lain yang mungkin belum dibuat. Di perusahaan kami, kami menggunakan pendekatan yang kami sebut cawan Petri . Artinya, dalam mode pengembangan, pengembang dan tester memiliki akses ke daftar semua layar aplikasi dan ia dapat pergi ke salah satu dari mereka (tentu saja, beberapa dari mereka mungkin memerlukan beberapa parameter input).

Anda dapat berinteraksi dengan mereka dan menguji secara individual, dan kemudian mengumpulkannya ke dalam aplikasi akhir untuk produksi. Pendekatan ini sangat memudahkan pengembangan, tetapi, seperti yang Anda lihat dari contoh di atas, komposisi neraka dimulai ketika Anda perlu menyimpan kode beberapa cara untuk mengintegrasikan pengontrol tampilan ke stack.
Tetap menambahkan bahwa semua ini akan dikalikan dengan N segera setelah tim pemasaran Anda menyatakan keinginan untuk melakukan pengujian A / B pada pengguna langsung dan memeriksa metode navigasi mana yang bekerja lebih baik, misalnya, bilah tab atau menu hamburger?
Mari kita potong kaki Susanin Mari kita tunjukkan 50% dari pengguna Tab Bar, dan ke menu Hamburger lainnya, dan dalam sebulan kami akan memberi tahu Anda pengguna mana yang melihat lebih banyak dari penawaran khusus kami?
Saya akan mencoba memberi tahu Anda bagaimana kami mendekati solusi untuk masalah ini dan akhirnya mengalokasikannya ke perpustakaan RouteComposer.
Susanin Rute komposer
Setelah menganalisis semua skenario komposisi dan navigasi, kami mencoba untuk abstrak kode yang diberikan dalam contoh di atas dan mengidentifikasi 3 entitas utama di mana perpustakaan RouteComposer beroperasi - Factory
, Finder
, Action
. Selain itu, pustaka berisi 3 entitas bantu yang bertanggung jawab atas penyetelan kecil yang mungkin diperlukan selama proses navigasi - RoutingInterceptor
, ContextTask
, PostRoutingTask
. Semua entitas ini harus dikonfigurasi dalam rantai dependensi dan ditransfer ke Router
y, objek yang akan membangun tumpukan pengontrol Anda.
Tapi, tentang masing-masing dari mereka dalam urutan:
Pabrik
Seperti namanya, Factory
bertanggung jawab untuk membuat pengontrol tampilan.
public protocol Factory { associatedtype ViewController: UIViewController associatedtype Context func build(with context: Context) throws -> ViewController }
Di sini penting untuk membuat reservasi tentang konsep konteks . Konteks dalam pustaka, kami menyebut semua yang diperlukan oleh pemirsa agar dapat dibuat. Misalnya, untuk memperlihatkan pengontrol tampilan yang menampilkan detail produk, Anda harus memasukkan productID tertentu ke dalamnya, misalnya, dalam bentuk String
. Inti dari konteks dapat berupa apa saja: objek, struktur, blok, atau tupel. Jika pengontrol Anda tidak memerlukan apa pun untuk dibuat - dapatkah konteksnya ditentukan sebagai Any?
dan instal dalam nil
.
Sebagai contoh:
class ProductViewControllerFactory: Factory { func build(with productID: UUID) throws -> ProductViewController { let productViewController = ProductViewController(nibName: "ProductViewController", bundle: nil) productViewController.productID = productID
Dari implementasi di atas menjadi jelas bahwa pabrik ini akan memuat gambar pengontrol dari file XIB dan menginstal productID yang ditransfer ke dalamnya. Selain protokol Factory
standar, perpustakaan menyediakan beberapa implementasi standar protokol ini untuk menyelamatkan Anda dari penulisan kode banal (khususnya, contoh di atas).
Lebih lanjut, saya akan menahan diri untuk tidak memberikan deskripsi protokol dan contoh implementasi mereka, karena Anda dapat membiasakan diri dengan mereka secara detail dengan mengunduh contoh yang datang dengan perpustakaan. Ada berbagai implementasi pabrik untuk pengontrol tampilan dan wadah konvensional, serta cara untuk mengonfigurasinya.
Aksi
Entitas Action
adalah deskripsi tentang cara mengintegrasikan pengontrol tampilan, yang akan dibangun oleh pabrik, ke dalam tumpukan. Pengontrol tampilan setelah pembuatan tidak dapat hanya menggantung di udara dan, oleh karena itu, setiap pabrik harus berisi Action
seperti yang dapat dilihat dari contoh di atas.
Implementasi Action
paling umum adalah presentasi modal dari controller:
class PresentModally: Action { func perform(viewController: UIViewController, on existingController: UIViewController, animated: Bool, completion: @escaping (_: ActionResult) -> Void) { guard existingController.presentedViewController == nil else { completion(.failure("\(existingController) is already presenting a view controller.")) return } existingController.present(viewController, animated: animated, completion: { completion(.continueRouting) }) } }
Pustaka berisi penerapan sebagian besar cara standar untuk mengintegrasikan pengontrol tampilan ke stack, dan Anda mungkin tidak perlu membuatnya sendiri sampai Anda menggunakan beberapa jenis pengontrol tampilan wadah kustom atau metode presentasi. Tetapi membuat Tindakan kustom seharusnya tidak menimbulkan masalah jika Anda membaca contoh.
Finder
Inti dari Finder
menjawab perute ke pertanyaan - Apakah sudah ada controller yang dibuat dan sudah ada di stack? Mungkin tidak ada yang diperlukan untuk dibuat dan cukup untuk menunjukkan apa yang sudah ada? .
public protocol Finder { associatedtype ViewController: UIViewController associatedtype Context func findViewController(with context: Context) -> ViewController? }
Jika Anda menyimpan tautan ke semua pengontrol tampilan yang Anda buat, maka dalam implementasi Finder
Anda, Anda bisa mengembalikan tautan ke pengontrol tampilan yang diinginkan. Tetapi paling sering hal ini tidak terjadi, karena tumpukan aplikasi, terutama jika itu besar, perubahannya cukup dinamis. Selain itu, Anda dapat memiliki beberapa pengontrol tampilan yang identik pada tumpukan yang menunjukkan entitas yang berbeda (misalnya, beberapa ProductViewControllers yang menunjukkan produk yang berbeda dengan productID yang berbeda), sehingga penerapan Finder
mungkin memerlukan implementasi khusus dan mencari pengontrol tampilan yang sesuai pada tumpukan. Pustaka memfasilitasi tugas ini dengan menyediakan StackIteratingFinder
sebagai ekstensi ke Finder
, sebuah protokol dengan pengaturan yang sesuai untuk menyederhanakan tugas ini. Dalam implementasi StackIteratingFinder
Anda hanya perlu menjawab pertanyaan - apakah view controller ini yang dicari oleh router atas permintaan Anda.
Contoh implementasi seperti itu:
class ProductViewControllerFinder: StackIteratingFinder { let options: SearchOptions init(options: SearchOptions = .currentAndUp) { self.options = options } func isTarget(_ productViewController: ProductViewController, with productID: UUID) -> Bool { return productViewController.productID == productID } }
Entitas Pembantu
RoutingInterceptor
RoutingInterceptor
memungkinkan Anda untuk melakukan beberapa tindakan sebelum memulai komposisi pengontrol tampilan dan memberi tahu router apakah mungkin untuk mengintegrasikan pengontrol tampilan pada stack. Contoh paling umum dari tugas semacam itu adalah otentikasi (tetapi sama sekali tidak biasa dalam implementasi). Misalnya, Anda ingin memperlihatkan pengontrol tampilan dengan detail akun pengguna, tetapi, untuk ini, pengguna harus masuk ke sistem. Anda dapat mengimplementasikan RoutingInterceptor
dan menambahkannya ke konfigurasi tampilan pengontrol rincian pengguna dan cek dalam: jika pengguna masuk, izinkan router melanjutkan navigasi, jika tidak, tunjukkan pengontrol tampilan yang meminta pengguna untuk masuk dan jika tindakan ini berhasil, izinkan router untuk melanjutkan navigasi atau membatalkan padanya jika pengguna menolak untuk masuk.
class LoginInterceptor: RoutingInterceptor { func execute(for destination: AppDestination, completion: @escaping (_: InterceptorResult) -> Void) { guard !LoginManager.sharedInstance.isUserLoggedIn else {
Implementasi dari RoutingInterceptor
dengan komentar terkandung dalam contoh yang disertakan dengan perpustakaan.
ContextTask
Entitas ContextTask
, jika Anda menyediakannya, dapat diterapkan secara terpisah untuk setiap pengontrol tampilan dalam konfigurasi, terlepas dari apakah itu baru saja dibuat oleh router atau ditemukan di stack, dan Anda hanya ingin memperbarui data di dalamnya dan atau menetapkan beberapa yang default parameter (misalnya, tampilkan tombol tutup atau tidak ditampilkan).
PostRoutingTask
Implementasi PostRoutingTask
akan dipanggil oleh router setelah berhasil menyelesaikan integrasi controller tampilan yang diminta ke stack. Dalam implementasinya, mudah untuk menambahkan berbagai analitik atau menarik berbagai layanan.
Secara lebih rinci dengan implementasi semua entitas yang dijelaskan dapat ditemukan dalam dokumentasi untuk perpustakaan serta dalam contoh terlampir.
PS: Jumlah entitas pelengkap yang dapat ditambahkan ke konfigurasi tidak terbatas.
Konfigurasi
Semua entitas yang dijelaskan adalah baik karena mereka memecah proses komposisi menjadi blok kecil, dapat dipertukarkan, dan dipercaya.
Sekarang mari kita beralih ke hal yang paling penting - ke konfigurasi, yaitu koneksi dari blok-blok ini satu sama lain. Untuk mengumpulkan blok-blok ini di antara mereka sendiri dan menggabungkan mereka ke dalam rantai langkah-langkah, perpustakaan menyediakan kelas pembangun StepAssembly
(untuk wadah - ContainerStepAssembly
). Implementasinya memungkinkan Anda untuk merangkai blok komposisi menjadi objek konfigurasi tunggal seperti manik-manik pada string, dan juga menunjukkan ketergantungan pada konfigurasi pengontrol tampilan lainnya. Apa yang harus dilakukan dengan konfigurasi di masa depan terserah Anda. Anda dapat memasukkannya ke router dengan parameter yang diperlukan dan itu akan membangun setumpuk pengontrol untuk Anda, Anda dapat menyimpannya ke kamus dan menggunakannya nanti dengan kunci - itu tergantung pada tugas spesifik Anda.
Pertimbangkan contoh sepele: Misalkan, dengan mengklik sel dalam daftar atau ketika aplikasi menerima tautan universal dari safari atau klien email, kita perlu memperlihatkan secara moderat pengontrol produk dengan productID tertentu. Dalam hal ini, pengontrol produk harus dibuat di dalam UINavigationController
sehingga dapat menampilkan nama dan tombol tutup pada panel kontrolnya. Selain itu, produk ini hanya dapat ditampilkan kepada pengguna yang masuk, jika tidak, undang mereka untuk masuk.
Jika Anda menguraikan contoh ini tanpa menggunakan pustaka, itu akan terlihat seperti ini:
class ProductArrayViewController: UITableViewController { let products: [UUID]? let analyticsManager = AnalyticsManager.sharedInstance
Contoh ini tidak termasuk penerapan tautan universal, yang akan memerlukan isolasi kode otorisasi dan mempertahankan konteks di mana pengguna harus diarahkan setelah, serta mencari, tiba-tiba pengguna mengklik tautan, dan produk ini sudah ditunjukkan kepadanya, yang pada akhirnya akan membuat kode sangat sulit dibaca.
Pertimbangkan konfigurasi contoh ini menggunakan perpustakaan:
let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory())
Jika Anda menerjemahkannya ke bahasa manusia:
- Periksa apakah pengguna sudah masuk, dan jika tidak menawarkan input kepadanya
- Jika pengguna telah berhasil masuk, lanjutkan
- Cari pengontrol tampilan produk yang disediakan oleh
Finder
- Jika ditemukan - buat terlihat dan selesai
- Jika tidak ditemukan - buat
UINavigationController
, integrasikan ke dalamnya view controller yang dibuat oleh ProductViewControllerFactory
menggunakan PushToNavigationAction
GenericActions.PresentModally
UINavigationController
GenericActions.PresentModally
menggunakan GenericActions.PresentModally
dari pengontrol tampilan saat ini
Konfigurasi memerlukan beberapa penelitian, seperti banyak solusi kompleks, misalnya, konsep AutoLayout
dan, pada pandangan pertama, mungkin tampak rumit dan berlebihan. Namun, sejumlah tugas yang harus diselesaikan dengan fragmen kode yang diberikan mencakup semua aspek mulai dari otorisasi hingga tautan dalam, dan membobol urutan tindakan memungkinkan untuk dengan mudah mengubah konfigurasi tanpa perlu membuat perubahan pada kode. Selain itu, penerapan StepAssembly
akan membantu Anda menghindari masalah dengan rantai langkah yang tidak lengkap, dan ketik kontrol - masalah dengan ketidakcocokan parameter input untuk pengontrol tampilan yang berbeda.
Pertimbangkan kode pseudo aplikasi lengkap di mana ProductArrayViewController
menampilkan daftar produk dan, jika pengguna memilih produk ini, menampilkannya tergantung pada apakah pengguna masuk atau tidak, atau menawarkan untuk masuk dan menampilkan setelah login berhasil:
Objek konfigurasi
class ProductArrayViewController: UITableViewController { let products: [UUID]?
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {
.
. , , , — ProductArrayViewController, UINavigationController HomeViewController — StepAssembly
from()
. RouteComposer
, ( ). , Configuration
. , A/B , .
Alih-alih sebuah kesimpulan
, 3 . , , . Fabric
, Finder
Action
. , — , , . , .
, , objective c Cocoa Touch, . iOS 9 12.
UIViewController
(MVC, MVVM, VIP, RIB, VIPER ..)
, , , . . .
.