مستودع واحد مع Swift: تنظيم الكود عمليًا
\n\nتطوير iOS الحديث يواجه بشكل متزايد تحدي توسيع قاعدة الكود. عندما ينمو المشروع ليصل إلى عشرات الوحدات والمكتبات المشتركة والعديد من التطبيقات (iOS، macOS، watchOS)، يبدأ النموذج التقليدي مع المستودعات المنفصلة (polyrepo) في إبطاء التطوير. هنا يأتي دور المستودع الواحد (Monorepo) - وهو مستودع واحد يحتوي على كود جميع مكونات النظام البيئي. في هذه المقالة، سنشرح عمليًا كيفية تنظيم مستودع واحد لمشاريع Swift باستخدام الأدوات الحديثة: Swift Package Manager (SPM) وTuist وخطوط أنابيب CI/CD الفعالة.
\n\n1. لماذا نحتاج إلى مستودع واحد في نظام Swift البيئي؟
\n\nقبل الغوص في التنفيذ، من المهم فهم المزايا الرئيسية لهذا النهج:
\n\n- \n
- إصدار موحد - جميع الوحدات والتطبيقات موجودة في commit واحد، مما يلغي مشكلة "تبعية الجحيم" (dependency hell). \n
- إعادة هيكلة مبسطة - تغيير API في المكتبة يظهر فورًا لجميع المستهلكين؛ المترجم يسلط الضوء على الأخطاء ضمن PR واحد. \n
- تنقل شفاف - المطورون يرون كود المشروع بالكامل دون التبديل بين المستودعات. \n
- تحسين CI/CD - يمكن إعداد بناء للوحدات المعدلة فقط (targeted builds)، مما يوفر الوقت والموارد. \n
ومع ذلك، يتطلب المستودع الواحد انضباطًا: بنية معيارية واضحة وأدوات تمنع الفوضى. بالنسبة لـ Swift، أفضل خيار اليوم هو Tuist - أداة تدير مشاريع Xcode بشكل تصريحي، أو Swift Package Manager النقي للهيكل المعياري.
\n\n2. هيكل المستودع الواحد مع Swift Package Manager
\n\nلنبدأ بالتنظيم الأساسي. لنفترض أن لدينا تطبيقين: MyAppiOS و MyAppMacOS، بالإضافة إلى مجموعة من المكتبات الداخلية. يمكن أن يبدو هيكل الملفات كالتالي:
Monorepo/\n├── Apps/\n│ ├── MyAppiOS/\n│ │ ├── Sources/\n│ │ └── Package.swift (أو project.yml لـ Tuist)\n│ └── MyAppMacOS/\n│ ├── Sources/\n│ └── Package.swift\n├── Libraries/\n│ ├── Networking/\n│ │ ├── Sources/\n│ │ └── Package.swift\n│ ├── UIComponents/\n│ │ ├── Sources/\n│ │ └── Package.swift\n│ └── Core/\n│ ├── Sources/\n│ └── Package.swift\n├── Tests/\n│ ├── NetworkingTests/\n│ └── UIComponentsTests/\n├── Package.swift (الملف الجذري)\n└── Tuist/ (إذا استخدمنا Tuist)\n └── Project.swift\n\n\nالملف الجذري Package.swift يربط كل شيء معًا. مثال على الملف:
// swift-tools-version:5.9\nimport PackageDescription\n\nlet package = Package(\n name: "MyMonorepo",\n platforms: [\n .iOS(.v16), .macOS(.v13)\n ],\n products: [\n .library(name: "Networking", targets: ["Networking"]),\n .library(name: "UIComponents", targets: ["UIComponents"]),\n .library(name: "Core", targets: ["Core"]),\n .executable(name: "MyAppiOS", targets: ["MyAppiOS"]),\n .executable(name: "MyAppMacOS", targets: ["MyAppMacOS"])\n ],\n dependencies: [\n // التبعيات الخارجية\n .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0")\n ],\n targets: [\n .target(\n name: "Networking",\n dependencies: ["Core", .product(name: "Alamofire", package: "Alamofire")],\n path: "Libraries/Networking/Sources"\n ),\n .target(\n name: "UIComponents",\n dependencies: ["Core"],\n path: "Libraries/UIComponents/Sources"\n ),\n .target(\n name: "Core",\n path: "Libraries/Core/Sources"\n ),\n .executableTarget(\n name: "MyAppiOS",\n dependencies: ["Networking", "UIComponents"],\n path: "Apps/MyAppiOS/Sources"\n