
تحتوي معظم تطبيقات الهاتف المحمول على أكثر من اثني عشر شاشة وانتقالات معقدة ، بالإضافة إلى أجزاء من التطبيق ، مفصولة بالمعنى والغرض. لذلك ، هناك حاجة إلى تنظيم هيكل التنقل الصحيح للتطبيق ، والذي سيكون مرنًا ومريحًا وقابلًا للتوسيع ويوفر وصولًا مريحًا إلى أجزاء مختلفة من التطبيق ، وسيكون أيضًا حذرًا بشأن موارد النظام.
في هذه المقالة ، سنقوم بتصميم التنقل في التطبيق بطريقة تتجنب الأخطاء الأكثر شيوعًا التي تؤدي إلى تسرب الذاكرة ، وتلف البنية وتكسر هيكل التنقل.
تحتوي معظم التطبيقات على جزأين على الأقل: المصادقة (الدخول المسبق) والجزء الخاص (ما بعد تسجيل الدخول). قد تحتوي بعض التطبيقات على بنية أكثر تعقيدًا ، وملفات تعريف متعددة مع تسجيل دخول واحد ، وانتقالات مشروطة بعد بدء التطبيق (الروابط العميقة) ، إلخ.
عمليًا ، يتم استخدام نهجين بشكل أساسي للتنقل في التطبيق:
- مكدس تنقل واحد لكل من وحدات التحكم في العرض التقديمي (الحاضر) ووحدات التحكم في التنقل (الدفع) ، دون القدرة على العودة. يؤدي هذا الأسلوب إلى بقاء جميع وحدات التحكم ViewControllers السابقة في الذاكرة.
- يستخدم تبديل 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”) {
لن تستخدم بالتأكيد 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() {
وأخيرًا ، قم بإنشاء وحدة التحكم الرئيسية لتطبيق
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)
بمجرد إضافة
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?()
هذه الطريقة تشبه إلى حد كبير
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 .

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