تنظيم التنقل في تطبيقات iOS باستخدام Root Controller



تحتوي معظم تطبيقات الهاتف المحمول على أكثر من اثني عشر شاشة وانتقالات معقدة ، بالإضافة إلى أجزاء من التطبيق ، مفصولة بالمعنى والغرض. لذلك ، هناك حاجة إلى تنظيم هيكل التنقل الصحيح للتطبيق ، والذي سيكون مرنًا ومريحًا وقابلًا للتوسيع ويوفر وصولًا مريحًا إلى أجزاء مختلفة من التطبيق ، وسيكون أيضًا حذرًا بشأن موارد النظام.

في هذه المقالة ، سنقوم بتصميم التنقل في التطبيق بطريقة تتجنب الأخطاء الأكثر شيوعًا التي تؤدي إلى تسرب الذاكرة ، وتلف البنية وتكسر هيكل التنقل.

تحتوي معظم التطبيقات على جزأين على الأقل: المصادقة (الدخول المسبق) والجزء الخاص (ما بعد تسجيل الدخول). قد تحتوي بعض التطبيقات على بنية أكثر تعقيدًا ، وملفات تعريف متعددة مع تسجيل دخول واحد ، وانتقالات مشروطة بعد بدء التطبيق (الروابط العميقة) ، إلخ.

عمليًا ، يتم استخدام نهجين بشكل أساسي للتنقل في التطبيق:

  1. مكدس تنقل واحد لكل من وحدات التحكم في العرض التقديمي (الحاضر) ووحدات التحكم في التنقل (الدفع) ، دون القدرة على العودة. يؤدي هذا الأسلوب إلى بقاء جميع وحدات التحكم ViewControllers السابقة في الذاكرة.
  2. يستخدم تبديل window.rootViewController. مع هذا النهج ، يتم تدمير جميع وحدات التحكم ViewControllers السابقة في الذاكرة ، ولكن هذا لا يبدو الأفضل من وجهة نظر واجهة المستخدم. أيضا ، لا يسمح لك بالتحرك ذهابا وإيابا إذا لزم الأمر.

الآن دعنا نرى كيف يمكنك إنشاء هيكل سهل الصيانة يسمح لك بالتبديل بسلاسة بين أجزاء مختلفة من التطبيق ، دون رمز السباغيتي والتنقل السهل.

لنفترض أننا نكتب طلبًا يتكون من:

  • الشاشة الأساسية (شاشة البداية ): هذه هي الشاشة الأولى التي تراها ، بمجرد بدء التطبيق ، يمكنك إضافة الرسوم المتحركة ، على سبيل المثال ، أو إجراء أي طلبات API أساسية.
  • جزء المصادقة : تسجيل الدخول ، التسجيل ، إعادة تعيين كلمة المرور ، تأكيد البريد الإلكتروني ، إلخ. عادة ما يتم حفظ جلسة عمل المستخدم ، لذلك ليست هناك حاجة لإدخال تسجيل دخول في كل مرة يبدأ فيها التطبيق.
  • الجزء الرئيسي : منطق الأعمال للتطبيق الرئيسي

يتم عزل كل هذه الأجزاء من التطبيق عن بعضها البعض وكل منها موجود في حزمة التنقل الخاصة به. لذلك ، قد نحتاج إلى التحولات التالية:

  • شاشة البداية -> شاشة المصادقة ، إذا كانت الجلسة الحالية للمستخدم النشط غائبة.
  • شاشة البداية -> الشاشة الرئيسية ، إذا كان المستخدم قد قام بالفعل بتسجيل الدخول إلى التطبيق في وقت سابق وكانت هناك جلسة نشطة.
  • الشاشة الرئيسية -> شاشة المصادقة ، في حالة تسجيل خروج المستخدم


الإعداد الأساسي

عندما يبدأ التطبيق ، نحتاج إلى تهيئة RootViewController ، والذي سيتم تحميله أولاً. يمكن القيام بذلك مع التعليمات البرمجية ومن خلال Interface Builder. قم بإنشاء مشروع جديد في xCode وسيتم فعل كل هذا بالفعل افتراضيًا: main.storyboard مرتبط بالفعل بـ window.rootViewController .

ولكن من أجل التركيز على الموضوع الرئيسي للمقالة ، لن نستخدم القصص المصورة في مشروعنا. لذلك ، احذف main.storyboard ، وقم أيضًا بمسح حقل "الواجهة الرئيسية" ضمن الأهداف -> عام -> معلومات النشر:



الآن دعنا نغير طريقة didFinishLaunchingWithOptions في AppDelegate بحيث تبدو كالتالي:

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

سيقوم التطبيق الآن بتشغيل RootViewController أولاً . إعادة تسمية ViewController الأساسي إلى RootViewController :

 class RootViewController: UIViewController { } 

سيكون هذا هو وحدة التحكم الرئيسية المسؤولة عن جميع التحولات بين أقسام التطبيق المختلفة. لذلك ، سنحتاج إلى رابط إليه في كل مرة نريد إجراء النقل. للقيام بذلك ، قم بإضافة الامتداد إلى AppDelegate :

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

إن الاسترداد القسري للخيار في هذه الحالة له ما يبرره ، لأن RootViewController لا يتغير ، وإذا حدث ذلك عن طريق الصدفة ، فإن تعطل التطبيق هو وضع طبيعي.

لذا ، لدينا الآن رابط إلى RootViewController من أي مكان في التطبيق:

 let rootViewController = AppDelegate.shared.rootViewController 

الآن دعونا ننشئ المزيد من وحدات التحكم التي نحتاجها: SplashViewController و LoginViewController و MainViewController .

شاشة البداية هي الشاشة الأولى التي سيشاهدها المستخدم بعد بدء التطبيق. في هذا الوقت ، عادةً ما يتم إجراء جميع طلبات واجهة برمجة التطبيقات الضرورية ، ويتم التحقق من نشاط جلسة المستخدم ، إلخ. لعرض إجراءات الخلفية الجارية ، استخدم 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() { } } 

لمحاكاة طلبات واجهة برمجة التطبيقات ، أضف طريقة DispatchQueue.main.asyncAfter مع تأخير لمدة 3 ثوانٍ:

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

نعتقد أن جلسة المستخدم يتم تعيينها أيضًا في هذه الطلبات. في تطبيقنا ، نستخدم افتراضيات المستخدم لهذا:

 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 } } } 

لن تستخدم بالتأكيد UserDefaults لحفظ حالة جلسة المستخدم في نسخة إصدار البرنامج. نستخدم الإعدادات المحلية في مشروعنا لتبسيط الفهم وعدم تجاوز الموضوع الرئيسي للمقال.

قم بإنشاء LoginViewController . سيتم استخدامه لمصادقة المستخدم إذا كانت جلسة المستخدم الحالية غير نشطة. يمكنك إضافة واجهة المستخدم المخصصة إلى وحدة التحكم ، ولكن سأضيف هنا فقط عنوان الشاشة وزر تسجيل الدخول في شريط التنقل.

 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 } } 

وأخيرًا ، قم بإنشاء وحدة التحكم الرئيسية لتطبيق 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 } } 

التنقل الجذر

الآن نعود إلى RootViewController .
كما قلنا سابقًا ، RootViewController هو الكائن الوحيد المسؤول عن التحولات بين مكدسات وحدة تحكم مستقلة مختلفة. لكي تكون على دراية بالحالة الحالية للتطبيق ، سنقوم بإنشاء متغير نقوم فيه بتخزين ViewController الحالي:

 class RootViewController: UIViewController { private var current: UIViewController } 

أضف مُهيئ الفصل وأنشئ أول ViewController الذي نريد تحميله عند بدء التطبيق. في حالتنا ، سيكون SplashViewController :

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

في viewDidLoad ، أضف viewController الحالي إلى 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 } } 

بمجرد إضافة childViewController (1) ، نقوم بتعديل حجمه من خلال تعيين current.view.frame على view.bounds (2).

إذا تخطينا هذا الخط ، فسيظل وضع viewController صحيحًا في معظم الحالات ، ولكن قد تحدث مشاكل إذا تغير حجم الإطار .

إضافة عرض فرعي جديد (3) واستدعاء الأسلوب didMove (toParentViewController :). سيؤدي هذا إلى إكمال عملية إضافة وحدة تحكم. بمجرد تشغيل RootViewController ، يتم عرض SplashViewController مباشرة بعد ذلك.

الآن يمكنك إضافة عدة طرق للتنقل في التطبيق. سنقوم بعرض LoginViewController بدون أي رسوم متحركة ، وسيستخدم MainViewController الرسوم المتحركة مع تعتيم سلس ، وسوف يكون لانتقال الشاشات عند قطع اتصال المستخدم تأثير الشريحة.

 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 } 

إنشاء LoginViewController (1) ، والإضافة كوحدة تحكم تابعة (2) ، وتعيين الإطار (3). أضف طريقة عرض LoginController كعرض فرعي (4) واستدعي طريقة didMove (5). بعد ذلك ، قم بإعداد وحدة التحكم الحالية للإزالة باستخدام طريقة willMove (6). أخيرًا ، احذف العرض الحالي من superview (7) ، واحذف وحدة التحكم الحالية من RootViewController (8). تذكر تحديث قيمة وحدة التحكم الحالية (9).

الآن دعنا ننشئ طريقة switchToMainScreen :

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

تتطلب تحريك الانتقال طريقة مختلفة:

 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 } } 

هذه الطريقة تشبه إلى حد كبير showLoginScreen ، ولكن يتم تنفيذ جميع الخطوات الأخيرة بعد اكتمال الرسم المتحرك. من أجل إخطار طريقة الاستدعاء بنهاية الانتقال ، في النهاية ندعو الإغلاق (1).

الآن ستبدو النسخة النهائية من طريقة switchToMainScreen كما يلي:

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

وأخيرًا ، دعنا ننشئ الطريقة الأخيرة التي ستكون مسؤولة عن الانتقال من MainViewController إلى LoginViewController :

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

يوفر أسلوب AnimateDismissTransition حركة الشرائح:

 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?() } } 

هذان مثالان فقط للرسوم المتحركة ، باستخدام نفس النهج يمكنك إنشاء أي رسوم متحركة معقدة تتطلبها

لإكمال التكوين ، أضف استدعاءات الأسلوب مع الرسوم المتحركة من SplashViewController و LoginViewController و 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() } } 

تجميع وتشغيل التطبيق والتحقق من تشغيله بطريقتين:

- عندما يكون لدى المستخدم بالفعل جلسة حالية نشطة (تسجيل الدخول)
- عندما لا تكون هناك جلسة نشطة ومطلوب المصادقة

في كلتا الحالتين ، يجب أن ترى انتقالًا إلى الشاشة المطلوبة ، مباشرة بعد تحميل SplashScreen .



ونتيجة لذلك ، أنشأنا نموذج اختبار صغير للتطبيق ، مع التنقل من خلال وحداته الرئيسية. إذا كنت بحاجة إلى توسيع إمكانيات التطبيق ، وإضافة وحدات انتقالية إضافية بينها ، فيمكنك دائمًا توسيع نظام التنقل هذا وتوسيع نطاقه بسرعة وسهولة.

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


All Articles