Organisasi navigasi di iOS-aplikasi menggunakan Root Controller



Sebagian besar aplikasi seluler berisi lebih dari selusin layar, transisi kompleks, serta bagian-bagian aplikasi, dipisahkan oleh makna dan tujuan. Oleh karena itu, ada kebutuhan untuk mengatur struktur navigasi yang benar untuk aplikasi, yang akan fleksibel, nyaman, dapat diperluas, menyediakan akses yang nyaman ke berbagai bagian aplikasi, dan juga akan berhati-hati tentang sumber daya sistem.

Pada artikel ini, kami akan merancang navigasi dalam aplikasi sedemikian rupa untuk menghindari kesalahan paling umum yang menyebabkan kebocoran memori, merusak arsitektur dan menghancurkan struktur navigasi.

Sebagian besar aplikasi memiliki setidaknya dua bagian: otentikasi (pra-masuk) dan bagian pribadi (pasca-masuk). Beberapa aplikasi mungkin memiliki struktur yang lebih kompleks, banyak profil dengan login tunggal, transisi kondisional setelah memulai aplikasi (tautan dalam), dll.

Dalam praktiknya, dua pendekatan terutama digunakan untuk menavigasi aplikasi:

  1. Satu tumpukan navigasi untuk pengontrol presentasi (sekarang) dan pengontrol navigasi (push), tanpa kemampuan untuk kembali. Pendekatan ini menyebabkan semua ViewControllers sebelumnya tetap berada dalam memori.
  2. Menggunakan toggle window.rootViewController. Dengan pendekatan ini, semua ViewControllers sebelumnya hancur dalam memori, tetapi ini tidak terlihat yang terbaik dari sudut pandang UI. Juga, itu tidak memungkinkan Anda untuk bergerak maju dan mundur jika perlu.

Sekarang mari kita lihat bagaimana Anda dapat membuat struktur yang mudah dipelihara yang memungkinkan Anda untuk beralih secara mulus antara berbagai bagian aplikasi, tanpa kode spageti dan navigasi yang mudah.

Mari kita bayangkan bahwa kita sedang menulis aplikasi yang terdiri dari:

  • Layar utama (Layar splash ): ini adalah layar pertama yang Anda lihat, segera setelah aplikasi dimulai, Anda dapat menambahkan, misalnya, animasi atau membuat permintaan API primer apa pun.
  • Bagian otentikasi : login, registrasi, setel ulang kata sandi, konfirmasi email, dll. Sesi kerja pengguna biasanya disimpan, jadi tidak perlu memasukkan login setiap kali aplikasi dimulai.
  • Bagian utama : logika bisnis dari aplikasi utama

Semua bagian aplikasi ini terisolasi satu sama lain dan masing-masing ada di tumpukan navigasinya sendiri. Karena itu, kita mungkin memerlukan transisi berikut:

  • Layar splash -> Layar otentikasi , jika sesi saat ini dari pengguna aktif tidak ada.
  • Layar splash -> Layar utama , jika pengguna telah masuk ke aplikasi lebih awal dan ada sesi aktif.
  • Layar utama -> layar Otentikasi , jika pengguna keluar


Pengaturan dasar

Ketika aplikasi dimulai, kita perlu menginisialisasi RootViewController , yang akan dimuat terlebih dahulu. Ini dapat dilakukan dengan kode dan melalui Interface Builder. Buat proyek baru di xCode dan semua ini sudah dilakukan secara default: main.storyboard sudah terikat ke window.rootViewController .

Tetapi untuk fokus pada topik utama artikel, kami tidak akan menggunakan storyboard dalam proyek kami. Oleh karena itu, hapus main.storyboard , dan kosongkan bidang "Antarmuka Utama" di bawah Target -> Umum -> Info penerapan:



Sekarang mari kita ubah metode didFinishLaunchingWithOptions di AppDelegate sehingga terlihat seperti ini:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = RootViewController() window?.makeKeyAndVisible() return true } 

Sekarang aplikasi pertama akan meluncurkan RootViewController . Ganti nama basis ViewController ke RootViewController :

 class RootViewController: UIViewController { } 

Ini akan menjadi pengontrol utama yang bertanggung jawab untuk semua transisi di antara berbagai bagian aplikasi. Oleh karena itu, kita akan memerlukan tautan ke sana setiap kali kita ingin melakukan transisi. Untuk melakukan ini, tambahkan ekstensi ke AppDelegate :

 extension AppDelegate { static var shared: AppDelegate { return UIApplication.shared.delegate as! AppDelegate } var rootViewController: RootViewController { return window!.rootViewController as! RootViewController } } 

Pengambilan paksa opsi dalam kasus ini dibenarkan, karena RootViewController tidak berubah, dan jika ini terjadi secara tidak sengaja, maka aplikasi crash adalah situasi normal.

Jadi, sekarang kita memiliki tautan ke RootViewController dari mana saja di aplikasi:

 let rootViewController = AppDelegate.shared.rootViewController 

Sekarang mari kita buat beberapa kontroler lagi yang kita butuhkan: SplashViewController, LoginViewController, dan MainViewController .

Splash Screen adalah layar pertama yang akan dilihat pengguna setelah memulai aplikasi. Pada saat ini, semua permintaan API yang diperlukan biasanya dibuat, aktivitas sesi pengguna diperiksa, dll. Untuk menampilkan tindakan latar belakang yang sedang berlangsung, gunakan UIActivityIndicatorView :

 class SplashViewController: UIViewController { private let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge) override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = UIColor.white view.addSubview(activityIndicator) activityIndicator.frame = view.bounds activityIndicator.backgroundColor = UIColor(white: 0, alpha: 0.4) makeServiceCall() } private func makeServiceCall() { } } 

Untuk mensimulasikan permintaan API, tambahkan metode DispatchQueue.main.asyncAfter dengan penundaan selama 3 detik:

 private func makeServiceCall() { activityIndicator.startAnimating() DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(3)) { self.activityIndicator.stopAnimating() } } 

Kami percaya bahwa sesi pengguna juga diatur dalam permintaan ini. Dalam aplikasi kami, kami menggunakan UserDefaults untuk ini:

 private func makeServiceCall() { activityIndicator.startAnimating() DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(3)) { self.activityIndicator.stopAnimating() if UserDefaults.standard.bool(forKey: “LOGGED_IN”) { // navigate to protected page } else { // navigate to login screen } } } 

Anda pasti tidak akan menggunakan UserDefaults untuk menyimpan status sesi pengguna dalam versi rilis program. Kami menggunakan pengaturan lokal dalam proyek kami untuk menyederhanakan pemahaman dan tidak melampaui topik utama artikel.

Buat LoginViewController . Ini akan digunakan untuk mengautentikasi pengguna jika sesi pengguna saat ini tidak aktif. Anda dapat menambahkan UI khusus Anda ke controller, tetapi saya akan menambahkan di sini hanya judul layar dan tombol login di Bilah Navigasi.

 class LoginViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = UIColor.white title = "Login Screen" let loginButton = UIBarButtonItem(title: "Log In", style: .plain, target: self, action: #selector(login)) navigationItem.setLeftBarButton(loginButton, animated: true) } @objc private func login() { // store the user session (example only, not for the production) UserDefaults.standard.set(true, forKey: "LOGGED_IN") // navigate to the Main Screen } } 

Dan akhirnya, buat pengontrol utama aplikasi MainViewController :

 class MainViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = UIColor.lightGray // to visually distinguish the protected part title = “Main Screen” let logoutButton = UIBarButtonItem(title: “Log Out”, style: .plain, target: self, action: #selector(logout)) navigationItem.setLeftBarButton(logoutButton, animated: true) } @objc private func logout() { // clear the user session (example only, not for the production) UserDefaults.standard.set(false, forKey: “LOGGED_IN”) // navigate to the Main Screen } } 

Navigasi root

Sekarang kembali ke RootViewController .
Seperti yang kami katakan sebelumnya, RootViewController adalah satu-satunya objek yang bertanggung jawab untuk transisi antara tumpukan pengontrol independen yang berbeda. Untuk mengetahui keadaan aplikasi saat ini, kami akan membuat variabel untuk menyimpan ViewController saat ini:

 class RootViewController: UIViewController { private var current: UIViewController } 

Tambahkan initializer kelas dan buat ViewController pertama yang ingin kita muat saat aplikasi dimulai. Dalam kasus kami, ini akan menjadi SplashViewController :

 class RootViewController: UIViewController { private var current: UIViewController init() { self.current = SplashViewController() super.init(nibName: nil, bundle: nil) } } 

Di viewDidLoad, tambahkan viewController saat ini ke RootViewController :

 class RootViewController: UIViewController { ... override func viewDidLoad() { super.viewDidLoad() addChildViewController(current) // 1 current.view.frame = view.bounds // 2 view.addSubview(current.view) // 3 current.didMove(toParentViewController: self) // 4 } } 

Setelah kami menambahkan childViewController (1), kami menyesuaikan ukurannya dengan mengatur current.view.frame ke view.bounds (2).

Jika kita melewati garis ini, viewController akan tetap ditempatkan dengan benar dalam banyak kasus, tetapi masalah dapat terjadi jika ukuran bingkai berubah.

Tambahkan subview baru (3) dan panggil metode didMove (toParentViewController :). Ini akan menyelesaikan operasi add controller. Segera setelah RootViewController melakukan boot , segera setelah itu SplashViewController ditampilkan.

Sekarang Anda dapat menambahkan beberapa metode untuk navigasi dalam aplikasi. Kami akan menampilkan LoginViewController tanpa animasi apa pun, MainViewController akan menggunakan animasi dengan peredupan halus, dan transisi layar ketika memutuskan koneksi pengguna akan memiliki efek slide.

 class RootViewController: UIViewController { ... func showLoginScreen() { let new = UINavigationController(rootViewController: LoginViewController()) // 1 addChildViewController(new) // 2 new.view.frame = view.bounds // 3 view.addSubview(new.view) // 4 new.didMove(toParentViewController: self) // 5 current.willMove(toParentViewController: nil) // 6 current.view.removeFromSuperview()] // 7 current.removeFromParentViewController() // 8 current = new // 9 } 

Buat LoginViewController (1), tambahkan sebagai pengontrol anak (2), atur bingkai (3). Tambahkan tampilan LoginController sebagai subview (4) dan panggil metode didMove (5). Selanjutnya, siapkan pengontrol saat ini untuk dihapus menggunakan metode willMove (6). Akhirnya, hapus tampilan saat ini dari superview (7), dan hapus controller saat ini dari RootViewController (8). Ingatlah untuk memperbarui nilai controller saat ini (9).

Sekarang mari kita buat metode switchToMainScreen :

 func switchToMainScreen() { let mainViewController = MainViewController() let mainScreen = UINavigationController(rootViewController: mainViewController) ... } 

Animasi transisi memerlukan metode yang berbeda:

 private func animateFadeTransition(to new: UIViewController, completion: (() -> Void)? = nil) { current.willMove(toParentViewController: nil) addChildViewController(new) transition(from: current, to: new, duration: 0.3, options: [.transitionCrossDissolve, .curveEaseOut], animations: { }) { completed in self.current.removeFromParentViewController() new.didMove(toParentViewController: self) self.current = new completion?() //1 } } 

Metode ini sangat mirip dengan showLoginScreen , tetapi semua langkah terakhir dilakukan setelah animasi selesai. Untuk memberi tahu metode panggilan dari akhir transisi, di bagian paling akhir kita sebut closure (1).

Sekarang versi final dari metode switchToMainScreen akan terlihat seperti ini:

 func switchToMainScreen() { let mainViewController = MainViewController() let mainScreen = UINavigationController(rootViewController: mainViewController) animateFadeTransition(to: mainScreen) } 

Dan akhirnya, mari kita buat metode terakhir yang akan bertanggung jawab untuk transisi dari MainViewController ke LoginViewController :

 func switchToLogout() { let loginViewController = LoginViewController() let logoutScreen = UINavigationController(rootViewController: loginViewController) animateDismissTransition(to: logoutScreen) } 

Metode AnimateDismissTransition menyediakan animasi slide:

 private func animateDismissTransition(to new: UIViewController, completion: (() -> Void)? = nil) { new.view.frame = CGRect(x: -view.bounds.width, y: 0, width: view.bounds.width, height: view.bounds.height) current.willMove(toParentViewController: nil) addChildViewController(new) transition(from: current, to: new, duration: 0.3, options: [], animations: { new.view.frame = self.view.bounds }) { completed in self.current.removeFromParentViewController() new.didMove(toParentViewController: self) self.current = new completion?() } } 

Ini hanya dua contoh animasi, menggunakan pendekatan yang sama Anda dapat membuat animasi kompleks yang Anda butuhkan

Untuk menyelesaikan konfigurasi, tambahkan pemanggilan metode dengan animasi dari SplashViewController, LoginViewController, dan MainViewController :

 class SplashViewController: UIViewController { ... private func makeServiceCall() { if UserDefaults.standard.bool(forKey: “LOGGED_IN”) { // navigate to protected page AppDelegate.shared.rootViewController.switchToMainScreen() } else { // navigate to login screen AppDelegate.shared.rootViewController.switchToLogout() } } } class LoginViewController: UIViewController { ... @objc private func login() { ... AppDelegate.shared.rootViewController.switchToMainScreen() } } class MainViewController: UIViewController { ... @objc private func logout() { ... AppDelegate.shared.rootViewController.switchToLogout() } } 

Kompilasi, jalankan aplikasi dan periksa operasinya dengan dua cara:

- ketika pengguna sudah memiliki sesi aktif saat ini (masuk)
- ketika tidak ada sesi aktif dan otentikasi diperlukan

Dalam kedua kasus, Anda akan melihat transisi ke layar yang diinginkan, segera setelah memuat SplashScreen .



Hasilnya, kami menciptakan model uji kecil aplikasi, dengan navigasi melalui modul utamanya. Jika Anda perlu memperluas kemampuan aplikasi, menambahkan modul tambahan dan transisi di antara mereka, Anda selalu dapat dengan cepat dan mudah memperluas dan skala sistem navigasi ini.

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


All Articles