الخلفية:
بالطبع ، يؤثر اختيار النهج المعماري على تنفيذ الملاحة وتنظيم نقل البيانات في المشروع ، ومع ذلك ، فإن النهج نفسه يتكون من عدد من الظروف: تكوين الفريق ، ووقت التسويق ، وحالة المواصفات الفنية ، وقابلية المشروع للتوسع ، إلخ.
- الاستخدام الإلزامي لـ MVVM ؛
- القدرة على إضافة شاشات جديدة بسرعة (وحدات التحكم ونماذج العرض الخاصة بهم) إلى عملية التنقل ؛
- يجب ألا تؤثر التغييرات في منطق الأعمال على التنقل ؛
- يجب ألا تؤثر التغييرات في التنقل على منطق الأعمال ؛
- القدرة على إعادة استخدام الشاشات بسرعة دون إجراء تصحيحات على التنقل ؛
- القدرة على الحصول بسرعة على فكرة عن الشاشات الموجودة ؛
- القدرة على الحصول بسرعة على فكرة عن التبعيات في المشروع ؛
- لا ترفع العتبة للمطورين لدخول المشروع.
أوضح ماذا تقصد
وتجدر الإشارة إلى أن الحل النهائي لم يتم تشكيله في يوم واحد ، ولا يخلو من عيوبه وهو أكثر ملاءمة للمشاريع الصغيرة والمتوسطة الحجم. للتوضيح ، يمكن الاطلاع على مشروع الاختبار هنا: github.com/ArturRuZ/NavigationDemo
1. لتتمكن من الحصول بسرعة على فكرة عن الشاشات الموجودة ، تقرر إنشاء تعداد باسم لا لبس فيه ControllersList.
enum ControllersList {
case textInputScreen
case textConfirmationScreen
}
2. لعدد من الأسباب ، لم يرغب المشروع في استخدام حلول الجهات الخارجية لـ DI ، وأردت الحصول على DI ، بما في ذلك القدرة على عرض التبعيات في المشروع بسرعة ، لذلك تقرر استخدام التجميع لكل شاشة منفصلة (مغلقة بواسطة بروتوكول التجميع) و RootAssembly باسم النطاق العام.
protocol Assembly {
func build() -> UIViewController
}
final class TextInputAssembly: Assembly {
func build() -> UIViewController {
let viewModel = TextInputViewModel()
return TextInputViewController(viewModel: viewModel)
}
}
final class TextConfirmationAssembly: Assembly {
private let text: String
init(text: String) {
self.text = text
}
func build() -> UIViewController {
let viewModel = TextConfirmationViewModel(text: text)
return TextConfirmationViewController(viewModel: viewModel)
}
}
3. لنقل البيانات بين الشاشات (حيثما تكون هناك حاجة فعلية لها) ، تحولت قائمة التحكم إلى تعداد مع القيم المرتبطة:
enum ControllersList {
case textInputScreen
case textConfirmationScreen(text: String)
}
4. لكي لا يؤثر منطق الأعمال على التنقل ، ولا على التنقل في منطق الأعمال ، وكذلك لإعادة الاستخدام السريع للشاشات ، كان من الضروري نقل التنقل إلى طبقة منفصلة. هكذا ظهر المنسق وبروتوكول التنسيق:
protocol Coordination {
func show(view: ControllersList, firstPosition: Bool)
func popFromCurrentController()
}
final class Coordinator {
private var navigationController = UINavigationController()
private var factory: ControllerBuilder?
private func navigateWithFirstPositionInStack(to: UIViewController) {
navigationController.viewControllers = [to]
}
private func navigate(to: UIViewController) {
navigationController.pushViewController(to, animated: true)
}
}
extension Coordinator: Coordination {
func popFromCurrentController() {
navigationController.popViewController(animated: true)
}
func show(view: ControllersList, firstPosition: Bool) {
guard let controller = factory?.buildController(for: view) else { return }
firstPosition ? navigateWithFirstPositionInStack(to: controller) : navigate(to: controller)
}
}
من المهم أن نلاحظ هنا أن البروتوكول يمكن أن يصف المزيد من الأساليب ، بما في ذلك. مثل المنسق ، يمكنه تنفيذ بروتوكولات مختلفة ، حسب الاحتياجات.
5. مع كل هذا ، أردت أيضًا تقييد مجموعة الإجراءات التي يتعين على المطور تنفيذها عن طريق إضافة شاشة جديدة إلى التطبيق. في الوقت الحالي ، كان مطلوبًا أن تتذكر أنه في مكان ما تحتاج إلى تسجيل التبعيات ، ومن الممكن القيام ببعض الإجراءات الأخرى حتى يعمل التنقل.
6. لم أرغب في إنشاء أجهزة توجيه ومنسقين إضافية على الإطلاق. علاوة على ذلك ، فإن إنشاء منطق إضافي للتنقل يمكن أن يعقد بشكل كبير إدراك التنقل وإعادة استخدام الشاشات. أدى كل هذا إلى سلسلة من التغييرات التي بدت في النهاية على النحو التالي:
//MARK - Dependences with controllers associations
fileprivate extension ControllersList {
typealias scope = AssemblyServices
var assembly: Assembly {
switch self {
case .textInputScreen:
return TextInputAssembly(coordinator: scope.coordinator)
case .textConfirmationScreen(let text):
return TextConfirmationAssembly(coordinator: scope.coordinator, text: text)
}
}
}
//MARK - Services all time in memory
fileprivate enum AssemblyServices {
static let coordinator: oordinationDependencesRegstration = Coordinator()
static let controllerFactory: ControllerBuilderDependencesRegistration = ControllerFacotry()
}
//MARL: - RootAssembly Implementation
final class RootAssembly {
fileprivate typealias scope = AssemblyServices
private func registerPropertyDependences() {
// this place for propery dependences
}
}
// MARK: - AssemblyDataSource implementation
extension RootAssembly: AssemblyDataSource {
func getAssembly(key: ControllersList) -> Assembly? {
return key.assembly
}
}
الآن ، عند إنشاء شاشة جديدة ، كان على المطور فقط إجراء تغييرات على قائمة التحكم ، ثم أظهر المترجم نفسه أين كان من الضروري إجراء التغييرات. لم تؤثر إضافة شاشات جديدة إلى قائمة ControllersList على مخطط التنقل الحالي بأي شكل من الأشكال ، وكان من السهل اتباع منطق إدارة التبعية. أيضًا ، باستخدام ControllersList ، يمكنك بسهولة العثور على جميع نقاط الدخول إلى شاشة معينة ، وأصبح من السهل الآن إعادة استخدام الشاشات.
خاتمة
هذا المثال هو تنفيذ مبسط للفكرة ولا يغطي جميع حالات الاستخدام ؛ ومع ذلك ، فقد أثبت النهج نفسه أنه مرن تمامًا وقابل للتكيف.
عيوب هذا النهج هي كما يلي:
- , , . ControllersList NavigationEvents, , ;
- , ;
- , , . , .
تؤثر معظم المنشورات حول التنقل ونقل البيانات في تطبيقات IOS إما على استخدام المنسقين والموجهات (لكل أو مجموعة من الشاشات) ، أو التنقل من خلال segue ، و singleton ، وما إلى ذلك ، ولكن لم يناسبني أي من هذه الخيارات لهذا أو ذاك. أسباب.
ربما يناسبك هذا النهج لحل المشكلات ، شكرًا على وقتك!