Komposisi UIViewControllers dan navigasi di antara mereka (dan tidak hanya)


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) //        let navigationController = UINavigationController(rootViewController: tableViewController) // ... //        let detailViewController = UIViewController(nibName: "DetailViewController", bundle: nil) navigationController.pushViewController(detailViewController, animated: true) // ... //     navigationController.popToRootViewController(animated: true) 

UITabBarController



 let firstViewController = UITableViewController(style: .plain) let secondViewController = UIViewController() //   let tabBarController = UITabBarController() //         tabBarController.viewControllers = [firstViewController, secondViewController] //        tabBarController.selectedViewController = secondViewController 

UISplitViewController



 let firstViewController = UITableViewController(style: .plain) let secondViewController = UIViewController() //   let splitViewController = UISplitViewController() //        splitViewController.viewControllers = [firstViewController] //        splitViewController.showDetailViewController(secondViewController, sender: nil) 

Contoh integrasi (komposisi) pengendali tampilan pada stack


Menginstal root pengontrol tampilan


 let window: UIWindow = //... window.rootViewController = viewController window.makeKeyAndVisible() 

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 //  ,      `ContextAction`,     return productViewController } } 

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 { // ... //  LoginViewController       completion(.success)  completion(.failure("User has not been logged in.")) // ... return } completion(.success) } } 

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 //  UITableViewControllerDelegate override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let productID = products[indexPath.row] else { return } //   LoginInterceptor guard !LoginManager.sharedInstance.isUserLoggedIn else { //    LoginViewController         `showProduct(with: productID)` return } showProduct(with: productID) } func showProduct(with productID: String) { //   ProductViewControllerFactory let productViewController = ProductViewController(nibName: "ProductViewController", bundle: nil) //   ProductViewControllerContextTask productViewController.productID = productID //   NavigationControllerStep  PushToNavigationAction let navigationController = UINavigationController(rootViewController: productViewController) //   GenericActions.PresentModally present(alertController, animated: navigationController) { [weak self]   . ProductViewControllerPostTask self?.analyticsManager.trackProductView(productID: productID) } } } 

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()) //  : .adding(LoginInterceptor()) .adding(ProductViewControllerContextTask()) .adding(ProductViewControllerPostTask(analyticsManager: AnalyticsManager.sharedInstance)) //  : .using(PushToNavigationAction()) .from(NavigationControllerStep()) // NavigationControllerStep -> StepAssembly(finder: NilFinder(), factory: NavigationControllerFactory()) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble() 

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


 // `RoutingDestination`    .          . struct AppDestination: RoutingDestination { let finalStep: RoutingStep let context: Any? } struct Configuration { //     ,             static func productDestination(with productID: UUID) -> AppDestination { let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory()) .add(LoginInterceptor()) .add(ProductViewControllerContextTask()) .add(ProductViewControllerPostTask(analyticsManager: AnalyticsManager.sharedInstance)) .using(PushToNavigationAction()) .from(NavigationControllerStep()) .using(GenericActions.PresentModally()) .from(CurrentControllerStep()) .assemble() return AppDestination(finalStep: productScreen, context: productID) } } 


 class ProductArrayViewController: UITableViewController { let products: [UUID]? //... // DefaultRouter -  Router   ,   UIViewController   let router = DefaultRouter() override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let productID = products[indexPath.row] else { return } router.navigate(to: Configuration.productDestination(with: productID)) } } 


 @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { //... func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool { guard let productID = UniversalLinksManager.parse(url: url) else { return false } return DefaultRouter().navigate(to: Configuration.productDestination(with: productID)) == .handled } } 

.


. , , , — 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 ..)


, , , . . .


.

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


All Articles