Coordinator: المفاهيم الأساسية، نمط المنسق للتنقل في iOS

المؤلف: IT Sectr نُشر: 2026-02-18 وقت القراءة: 9 دق

Coordinator (المنسق) — نمط معماري للتنقل ينقل منطق الانتقال بين الشاشات من ViewController إلى فئات منفصلة. تم اقتراح النمط بواسطة Soroush Khanlou في عام 2015 وحظي بانتشار واسع في مجتمع iOS. يدير Coordinator تدفق التطبيق: ينشئ ويعرض ViewController، وينقل البيانات بين الشاشات، ويتعامل مع إكمال التدفق. يحل النمط مشكلة Massive View Controller عن طريق إخراج التنقل من المتحكم. اعرف المزيد في المقال الأصلي عن Coordinator.

أهم النقاط

  • Coordinator — نمط تنقل يخرج منطق الانتقالات من ViewController
  • فصل المسؤوليات — ViewController يدير واجهة المستخدم، Coordinator يدير التنقل
  • Router — مكون مساعد لـ Coordinator لتجريد UINavigationController
  • Flow — سلسلة من الشاشات يديرها Coordinator واحد (مثل، onboarding)
  • التفويض — المنسقون الفرعيون يبلغون الأصل عبر delegate/protocol

ما هو Coordinator: جوهر نمط التنقل

Coordinator — نمط يتحمل مسؤولية التنقل في تطبيق iOS. في UIKit القياسي، يدير ViewController نفسه الانتقالات: present، push، show segue — جميع طرق التنقل تُستدعى من UIViewController. يقوم Coordinator باستخراج هذا المنطق: يُبلغ ViewController عن حدث (مثل «ضغط المستخدم على زر تسجيل الدخول»)، ويقرر Coordinator أي شاشة سيتم عرضها بعد ذلك. يبقى ViewController فقط مع منطق واجهة المستخدم ويفوض التنقل إلى المنسق.

هيكل النمط — CoordinatorProtocol مع طريقتي start() و finish(). start() — بداية التدفق: إنشاء أول ViewController وعرضه. finish() — إنهاء التدفق مع إخطار المنسق الأصل. Router — غلاف حول UINavigationController (أو UISplitViewController)، يوفر طرق show، push، pop، dismiss. لا يعمل المنسق مباشرة مع UINavigationController — فقط من خلال Router. هذا يسمح باختبار التنقل وتبديل إطار عمل واجهة المستخدم.

المكونالدورمثال
Coordinatorإدارة تدفق التنقلAuthCoordinator, ProfileCoordinator
Routerتجريد فوق UINavigationControllerpush, present, pop, dismiss
ViewControllerواجهة المستخدم + تفويض الأحداث لـ CoordinatorLoginViewController.delegate

المشكلات التي يحلها Coordinator — Massive View Controller (التنقل سبب شائع لتضخم المتحكم). في UIKit القياسي، يحتوي ViewController على prepareForSegue ومفوضي التنقل ومعالجة unwind segues. يزيل Coordinator هذا. الـ segues في storyboard هي اتصالات ثابتة بين الشاشات، يوفر Coordinator تنقلاً ديناميكياً مع شروط. يصبح اختبار التنقل ممكناً: يمكن اختبار Coordinator بدون واجهة مستخدم عن طريق التحقق من تسلسل استدعاءات Router.

Coordinator في Swift: تنفيذ مع Router و Flow

Coordinator الأساسي في Swift — بروتوكول مع نوع مرتبط لـ Router وطريقي start/finish. Router — بروتوكول يجرد UINavigationController. تنفيذ ملموس لـ Router يغلف UINavigationController ويفوض إليه الطرق. يقبل Coordinator الـ Router في init ويستخدمه للتنقل. يتم تخزين المنسقين الفرعيين في مصفوفة childCoordinators لإدارة دورة الحياة.

swift
// Router — تجريد التنقل
protocol RouterProtocol: AnyObject {
    func push(_ viewController: UIViewController, animated: Bool)
    func pop(animated: Bool)
    func present(_ viewController: UIViewController, animated: Bool)
    func dismiss(animated: Bool)
}

final class NavigationRouter: RouterProtocol {
    private let navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func push(_ vc: UIViewController, animated: Bool) {
        navigationController.pushViewController(vc, animated: animated)
    }

    func pop(animated: Bool) {
        navigationController.popViewController(animated: animated)
    }

    func present(_ vc: UIViewController, animated: Bool) {
        navigationController.present(vc, animated: animated)
    }

    func dismiss(animated: Bool) {
        navigationController.dismiss(animated: animated)
    }
}

// Coordinator — إدارة التدفق
protocol CoordinatorProtocol: AnyObject {
    var childCoordinators: [CoordinatorProtocol] { get set }
    var router: RouterProtocol { get }
    func start()
    func finish()
}

class AuthCoordinator: CoordinatorProtocol {
    var childCoordinators: [CoordinatorProtocol] = []
    let router: RouterProtocol

    init(router: RouterProtocol) {
        self.router = router
    }

    func start() {
        let loginVC = LoginViewController()
        loginVC.onLogin = { [weak self] in
            self?.showHome()
        }
        router.push(loginVC, animated: true)
    }

    private func showHome() {
        let homeCoordinator = HomeCoordinator(router: router)
        childCoordinators.append(homeCoordinator)
        homeCoordinator.start()
    }

    func finish() {
        childCoordinators.removeAll()
        router.pop(animated: true)
    }
}

إنشاء Coordinator في AppDelegate/SceneDelegate — يقوم AppDelegate أو SceneDelegate بإنشاء UINavigationController، ويغلقه في NavigationRouter، وينشئ Coordinator جذر (AppCoordinator) ويستدعي start(). يقرر AppCoordinator ما إذا كان سيعرض onboarding، أو تسجيل الدخول، أو الشاشة الرئيسية — حسب حالة التطبيق. Coordinator هو نقطة الدخول الوحيدة للتنقل، ViewController لا يعرف عن الشاشات الأخرى.

المنسقون الفرعيون وتسلسل المنسقين

تسلسل المنسقين — AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator. يتم إنشاء المنسق الفرعي بواسطة الأصل ويتم تخزينه في مصفوفة childCoordinators. عندما يكمل المنسق الفرعي عمله، يستدعي finish() على الأصل، ويقوم الأصل بإزالته من childCoordinators. هذا يمنع تسرب الذاكرة: Coordinator لديه مرجع قوي إلى ViewController (عبر Router)، وبدون الإزالة من childCoordinators، لن يتم تحرير الكائن.

swift
// مفوض للتواصل Coordinator -> Parent
protocol AuthCoordinatorDelegate: AnyObject {
    func authCoordinatorFinished(_ coordinator: AuthCoordinator)
}

class AuthCoordinator: CoordinatorProtocol {
    weak var delegate: AuthCoordinatorDelegate?

    func finish() {
        delegate?.authCoordinatorFinished(self)
    }
}

// AppCoordinator — الأصل
class AppCoordinator: AuthCoordinatorDelegate {
    func startAuthFlow() {
        let authCoordinator = AuthCoordinator(router: router)
        authCoordinator.delegate = self
        childCoordinators.append(authCoordinator)
        authCoordinator.start()
    }

    func authCoordinatorFinished(_ coordinator: AuthCoordinator) {
        childCoordinators.removeAll { $0 is AuthCoordinator }
        startMainFlow()
    }
}

إدارة childCoordinators — إزالة Coordinator من المصفوفة هي الطريقة الوحيدة لتحريره. إذا نسيت إزالة Coordinator مكتمل، فإنه يبقى في الذاكرة مع ViewControllers الخاصة به. الأساليب الموصى بها: didMove(toParent:) للأصل، callback عند الإكمال، أو ناشر Combine للإزالة التلقائية. لا يحدد نمط Coordinator آلية الإخطار — delegate أو closure أو Combine — الاختيار للمطور.

طرق نقل البيانات بين المنسقين

نقل البيانات عبر المفوض — يحدد Coordinator الفرعي بروتوكول مفوض مع طرق يتم من خلالها تمرير النتائج: func authCoordinator(_:didLoginWith user: User). ينفذ الأصل البروتوكول ويستلم البيانات عند اكتمال التدفق الفرعي. هذا آمن من حيث النوع وصريح. العيب: كل Coordinator فرعي يتطلب بروتوكولاً منفصلاً. للمشاريع التي تحتوي على 10+ منسقين، يؤدي هذا إلى زيادة في عدد الملفات.

نقل البيانات عبر نوع Result — تقبل طريقة finish المعامل Result، حيث Output هو نوع عام لنتيجة التدفق. Coordinator — عام مع نوع نتيجة مرتبط. start() مع callback: start(completion: @escaping (Output) -> Void). هذا يقلل من الكود: لا حاجة لكتابة بروتوكول منفصل لكل منسق. RxSwift/Combine: ينشر Coordinator النتيجة عبر PassthroughSubject/Publisher. يعتمد الاختيار على النهج المعماري للفريق.

الطريقةالمزاياالعيوب
Delegateآمن من حيث النوع، صريح، بروتوكولات منفصلةبروتوكولات كثيرة، الكثير من الكود المتكرر
Closureمدمج، ملفات أقلصعب تصحيح أخطاء retain cycle
Combine/Rxتفاعلي، سهل الدمجاعتماد على مكتبة، أصعب في التصحيح

طبقة بيانات مشتركة — لا ينقل المنسقون البيانات مباشرة، بل يستخدمون خدمة/مستودعاً مشتركاً. AuthCoordinator يحفظ الرمز المميز في Keychain/UserDefaults، ProfileCoordinator يقرأ من هناك. يتواصل المنسقون من خلال حالة مشتركة (حاوية Dependency Injection) بدلاً من الاستدعاءات المباشرة. هذا يقلل من الاقتران بين المنسقين، لكنه يخلق تبعيات ضمنية على الحالة المشتركة.

مقارنة Coordinator مع Router و VIPER و MVVM-C

Coordinator مقابل Router — Router هو مكون من Coordinator يجرد UINavigationController. Coordinator مسؤول عن التدفق (أي شاشة سيتم عرضها)، Router — عن الآلية (كيفية العرض: push/present). Router هو «الكيفية»، Coordinator هو «ماذا». يمكن استخدام Router بدون Coordinator (مثل، Navigator singleton)، لكن Coordinator بدون Router هو مجرد ViewController بتجريد آخر. عادةً ما يتم استخدام كلا النمطين معاً.

Coordinator مقابل VIPER — VIPER يحتوي على مكون Wireframe مسؤول عن التنقل — مشابه لـ Coordinator. في VIPER، Wireframe جزء من الوحدة، Coordinator هو طبقة منفصلة فوق الوحدات. وحدة VIPER (View-Interactor-Presenter-Entity-Router) تتضمن التنقل كجزء من الوحدة. Coordinator خارجي بالنسبة للوحدات: يقوم بإنشائها وربطها، لكنه ليس جزءاً منها. Coordinator أكثر مرونة لإعادة استخدام الشاشات في تدفقات مختلفة.

MVVM-C — امتداد لـ MVVM مع Coordinator. لا يعرف ViewModel عن Coordinator مباشرة — يفوض ViewController التنقل عبر ViewModel، ويستدعي ViewModel المنسق عبر بروتوكول. MVVM-C هو النهج القياسي لمشاريع iOS مع SwiftUI: يدير Coordinator NavigationStack أو fullScreenCover، ويستدعي ViewModel المنسق عن طريق نشر الحالة. لا توصي Apple باستخدام Coordinator لـ SwiftUI — NavigationStack و NavigationPath هما آليتا تنقل مضمنتان.

swift
// MVVM-C: ViewModel يستدعي Coordinator عبر بروتوكول
protocol AuthNavigationProtocol: AnyObject {
    func showMainScreen()
    func showForgotPassword()
}

class AuthViewModel: ObservableObject {
    weak var navigation: AuthNavigationProtocol?

    func loginTapped() {
        // منطق...
        navigation?.showMainScreen()
    }
}

الأسئلة الشائعة

هل نحتاج Coordinator لـ SwiftUI؟

لـ SwiftUI، التنقل المضمن (NavigationStack, NavigationPath) غالباً ما يحل محل Coordinator. توصي Apple بالتنقل المعتمد على المسار (path-based). Coordinator مفيد للتدفقات المعقدة ذات الشروط العميقة (onboarding-تسجيل الدخول-الشاشة الرئيسية حسب الدور). للتطبيقات البسيطة على SwiftUI، Coordinator زائد عن الحاجة — استخدم NavigationPath.

هل Coordinator هو Router؟

لا، هما نمطان مختلفان. Coordinator يدير تدفق التنقل: يقرر أي شاشة سيتم عرضها، وينشئ ViewControllers ويربط بينها. Router هو تجريد فوق UINavigationController: push, present, pop, dismiss. يستخدم Coordinator الـ Router لتنفيذ التنقل. في بعض التطبيقات، يتضمن Router منطق Coordinator (Router-per-screen)، لكن هذا انحراف عن النمط الأصلي.

كيف نتجنب retain cycle في Coordinator؟

مصدران رئيسيان للتسرب: childCoordinators (الأصل يبقي الفرعي ناسياً إزالته) و Router (UINavigationController يبقي ViewController). الحل: أزل Coordinator الفرعي من المصفوفة دائماً عند finish(). استخدم مرجع weak للمفوض. لـ Router — لا تحتفظ بمرجع قوي لـ UINavigationController إذا كان موجوداً بالفعل في تسلسل window. اختبر deinit الخاص بـ Coordinator.

متى يكون Coordinator زائداً عن الحاجة؟

للتطبيقات التي تحتوي على 3-5 شاشات، Coordinator زائد عن الحاجة — segue أو navigationController.pushViewController البسيط أسهل. لتطبيقات SwiftUI مع NavigationStack — أيضاً زائد عن الحاجة. Coordinator مبرر للتطبيقات ذات 15+ شاشة، والتدفقات المعقدة (onboarding مع تفرعات، تسجيل الدخول مع استعادة كلمة المرور) والمشاريع المختلطة UIKit/SwiftUI.

كيف نختبر Coordinator؟

Mock Router — تحقق من أي الطرق تم استدعاؤها وبأي معاملات. تحقق من childCoordinators: بعد start() المصفوفة غير فارغة، بعد finish() — فارغة. يتم اختبار Coordinator بدون واجهة مستخدم: Router هو بروتوكول، و mock الخاص به لا يتطلب UIKit. استخدم XCTestExpectation للتدفق غير المتزامن. في Android — اختبار مشابه لـ NavigationController و NavHost مع تنقل mock.

الخلاصة

  • Coordinator — نمط تنقل يخرج الانتقالات من ViewController إلى فئة منفصلة
  • Router — تجريد لـ UINavigationController يستخدمه Coordinator
  • التسلسل — منسقون آباء وأبناء مع تفويض النتائج
  • MVVM-C — النهج القياسي لمشاريع UIKit مع Coordinator
  • SwiftUI — التنقل المضمن عبر NavigationStack يحل محل Coordinator
  • الاختبار — يتم اختبار Coordinator عبر mock Router بدون UIKit

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا