لغات البرمجة عملياً الأدلة التقنية

Swift للمطورين: الأدوات والصياغة Syntax على Linux

ثبتنا Swift 6.4.2 على Linux وبنينا مكتبة لحساب السعر والضريبة مع أداة سطر أوامر واختبارات، لترى اللغة مقارنة بما تعرفه وتعرف ما يحتاج Mac وما لا يحتاجه.

Swift للمطورين: الأدوات والصياغة Syntax على Linux

لنفرض أنك مطور Backend تكتب بـ Java أو Python أو TypeScript منذ سنوات، وتعمل في متجر إلكتروني صغير يبيع التمور والقهوة، وتطبيق المتجر على iPhone مكتوب بـ Swift كتبه زميل انتقل إلى شركة أخرى قبل شهرين. وفي صباح أحد الأيام يصلك من قسم المحاسبة أن الفاتورة في التطبيق تظهر ضريبة القيمة المضافة 4.45 ريال على طلب قيمته 29.70، بينما الموقع والسيرفر يحسبانها 4.46، والفرق هللة واحدة، ولكنها هللة تتكرر في آلاف الفواتير وتظهر في كل مطابقة شهرية. ويطلب منك المدير التقني أن تنقل حساب السعر والضريبة إلى مكتبة Swift صغيرة يشترك فيها التطبيق وأداة سطر أوامر يستخدمها فريق المحاسبة، وأن تعمل اختباراتها في CI على Linux لأن الشركة لا تملك أجهزة Mac للسيرفرات. وأنت لم تكتب سطراً واحداً بـ Swift في حياتك، وكل ما تعرفه عنها أنها «لغة Apple».

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

وسوف نناقش في هذا المقال ما يلي:

  • لماذا ظهرت Swift، وما المشكلة التي حلتها بعد Objective-C، ولماذا صارت مفتوحة المصدر وتعمل على Linux.
  • أين تعمل Swift اليوم، وما الذي يحتاج جهاز Mac وXcode، وما الذي يعمل على أي جهاز.
  • تثبيت الأدوات Toolchain بالإصدار 6.4.2، وما الذي تجده داخل الـ Image الرسمية.
  • جولة في الصياغة Syntax مقارنة بـ Java وPython وTypeScript، بمثال يعمل ومخرجه.
  • مدير الحزم SwiftPM وشكل المشروع Project Layout، والملف Package.swift والملف Package.resolved.
  • البناء والتشغيل والاختبار بـ Swift Testing، والتنسيق والفحص بـ swift format.
  • البناء للإنتاج Release Build والربط الثابت Static Linking، وماذا يحدث إذا شغلت كود Debug في الإنتاج.
  • المحرر وأدوات اللغة، والمشاكل التي ظهرت في التجربة، وقائمة تحقق.

لماذا ظهرت Swift؟ من Objective-C إلى لغة مفتوحة المصدر Open Source

قبل Swift كانت تطبيقات Apple تكتب بلغة Objective-C، وهي لغة من الثمانينات تضيف رسائل Smalltalk فوق لغة C، ولذلك ورثت من C كل مشاكلها: المؤشرات Pointers التي يمكن أن تشير إلى ذاكرة حررت، والقيم nil التي يمكن أن تصل إلى أي مكان دون أن يعترض المترجم، والملفات الرأسية Headers التي تكتب فيها كل دالة مرتين. وكان المطور الجديد الذي يأتي من Java أو JavaScript يرى الأقواس المربعة في كل سطر ويشعر أنه يتعلم لغة من عصر آخر، وكانت الأخطاء التي تصل إلى المستخدم في الغالب من نوع واحد: قيمة لم يتوقعها أحد، أو ذاكرة استخدمت بعد تحريرها.

وفي مؤتمر WWDC في يونيو 2014 أعلنت Apple عن Swift، وكان هدفها المعلن لغة آمنة وسريعة وسهلة القراءة: الأنواع تستنتج بدل أن تكتب في كل سطر، والقيمة التي قد تكون غير موجودة لها نوع خاص هو الاختياري Optional، والمصفوفة تفحص حدودها، والأعداد الصحيحة لا تتجاوز حدها بصمت، والذاكرة تدار تلقائياً بعداد المراجع ARC بدون جامع قمامة Garbage Collector يوقف البرنامج. ثم في 3 ديسمبر 2015 فتحت Apple مصدر اللغة وأطلقت موقع swift.org، ومعه نسخة تعمل على Linux، ومدير حزم جديد هو Swift Package Manager، ومن هنا بدأت Swift تخرج من عالم Apple إلى السيرفر.

وبعدها مرت اللغة بمحطات يجب أن تعرفها لأنك سوف تراها في الكود القديم الذي تقرؤه:

  • Swift 3 في 2016: تغيير كبير في أسماء الدوال جعل الكود القديم لا يبنى إلا بعد تعديله، ولذلك ترى في المشاريع القديمة أدوات ترحيل Migration وتعليقات عن «تحويل الكود إلى Swift 3».
  • Swift 5 في 2019: استقرار الواجهة الثنائية ABI Stability على أنظمة Apple، أي أن مكتبة Swift صارت جزءاً من النظام نفسه، وهذا موضح في إعلان Swift 5.
  • Swift 5.5 في 2021: دخول async/await والممثلين Actors والتزامن المنظم Structured Concurrency، وهو موضوع المقال الثالث في هذه المرحلة.
  • Swift 6 في سبتمبر 2024: وضع لغة جديد يحول تحذيرات سباق البيانات Data Race إلى أخطاء ترجمة، والأخطاء ذات النوع المحدد Typed Throws، ومكتبة Swift Testing الجديدة، بحسب إعلان Swift 6.
  • Swift 6.4 في 15 سبتمبر 2026: الإصدار الذي نستخدمه هنا، وأهم ما يهمك فيه أن نظام البناء Swift Build صار الافتراضي في SwiftPM على كل الأنظمة، ومكتبة Subprocess وصلت 1.0، وصار بإمكانك استدعاء دوال async داخل defer، كما في إعلان Swift 6.4.

وقد يتساءل البعض: هل Swift على السيرفر تجربة هواة، أم شيء يعمل فعلاً في الإنتاج؟ والإجابة جاءت من Apple نفسها، حيث نشر فريقها في يونيو 2025 قصة نقل خدمة Password Monitoring من Java إلى Swift، وهي خدمة على Linux وKubernetes تستقبل مليارات الطلبات يومياً، وكتبت بإطار Vapor الذي سوف نستخدمه في المقال الرابع. وذكر الفريق أن الخدمة الجديدة أعطت زيادة في الإنتاجية Throughput قريبة من 40%، وأن الذاكرة التي تحتاجها كل نسخة صارت بالمئات من الميجابايتات بدل عشرات الجيجابايتات التي كانت تحتاجها نسخة Java في الذروة، وأن الكود نقص بنسبة 85%، وأن هذا حرر قرابة 50% من السعة لأعمال أخرى. ولاحظ أن الأرقام من فريق Apple عن خدمته هو، وليست مقارنة عامة بين اللغتين، ولكنها تخبرك أن Swift على Linux ليست تجربة.

أين تعمل Swift اليوم؟ Apple و Linux والسيرفر Server-side Swift

أول ما يجب أن تفصله في ذهنك أن Swift اللغة شيء، وأدوات بناء تطبيقات Apple شيء آخر. فاللغة نفسها ومكتبتها القياسية Standard Library ومدير الحزم والمترجم كلها مفتوحة المصدر، وتعمل على Linux وWindows وmacOS، وحتى على WebAssembly والمتحكمات الصغيرة Embedded. أما واجهات تطبيقات iPhone وiPad وMac، أي SwiftUI وUIKit وAppKit، والمحاكي Simulator، وتوقيع التطبيق ورفعه إلى App Store، فهذه كلها من Apple ولا تعمل إلا داخل Xcode على جهاز Mac.

لوحتان: في اليمين ما يعمل على أي جهاز وهو اللغة والمكتبة القياسية والمترجم وSwiftPM وFoundation وSwift Testing على Linux وWindows وmacOS، ومنه السيرفر بـ Vapor وأدوات سطر الأوامر والمكتبات المشتركة، وفي اليسار ما يحتاج Mac وXcode وهو SwiftUI وUIKit وAppKit والمحاكي وتوقيع التطبيق وApp Store، وبينهما سهم يبين أن المكتبة المشتركة PriceKit يمكن أن يستخدمها التطبيق والسيرفر معاً
اللغة وأدواتها تعمل على Linux، وواجهات تطبيقات Apple تحتاج Xcode على Mac
⚠️
كل التجارب في هذه المرحلة تعمل على Linux داخل Image الرسمية swift:6.4.2-noble، ولا يوجد فيها جهاز Mac ولا Xcode، ولذلك فهي عن اللغة نفسها وعن Swift على السيرفر. إذا كان عملك بناء واجهات iPhone أو Mac بـ SwiftUI أو UIKit، وتشغيلها على المحاكي، ورفعها إلى المتجر، فأنت تحتاج Xcode على Mac، وكل ما تتعلمه هنا عن الأنواع والذاكرة والتزامن والأخطاء ينطبق هناك كما هو، لأنها نفس اللغة ونفس المترجم.

وهذا الفصل هو الذي يجعل قصة المتجر ممكنة، فمكتبة حساب السعر لا تحتاج أي شيء من واجهات Apple، وإنما أعداد صحيحة ونصوص، ولذلك يمكن أن تكتب وتختبر على Linux في CI، ثم يضيفها مطور التطبيق إلى مشروع Xcode كاعتمادية Dependency عادية. وفي الجهة الأخرى من القرار، إذا كان سؤالك هل تكتب التطبيق بـ Swift أصلاً أم بإطار متعدد المنصات، فقد ناقشنا ذلك في مقال تطبيق الجوال: أصيل Native أم Flutter أم React Native، وإذا كنت تفكر في مشاركة الكود بين لغات مختلفة في نفس الشركة فراجع البرمجة بعدة لغات Polyglot.

تثبيت الأدوات Toolchain: swiftly و Docker و Xcode

على Linux أمامك ثلاث طرق لتثبيت Swift، ولكل طريقة مكانها:

  • swiftly: أداة رسمية لتثبيت إصدارات Swift والتنقل بينها، مثل nvm في Node.js أو pyenv في Python، وهي الطريقة التي توصي بها صفحة التثبيت على Linux لجهاز التطوير، ومستودعها swiftlang/swiftly.
  • الـ Image الرسمية في Docker: وهي swift على Docker Hub، وفيها الأداة كاملة على Ubuntu أو Debian أو RHEL، وهي ما نستخدمه هنا لأنها لا تغير شيئاً في جهازك، ولأنها نفس البيئة التي سوف تبني فيها الـ Image في CI والإنتاج.
  • Xcode على Mac: يأتي معه مترجم Swift، وهو الطريق الوحيد لتطبيقات Apple كما ذكرنا.

وعلى Windows توجد نسخة رسمية أيضاً، ولكن إذا كان هدفك السيرفر فالأفضل أن تعمل داخل Docker أو WSL، لأن السيرفر الذي سوف يشغل الكود هو Linux في النهاية.

Swift 6.4.2 داخل Docker: ما الذي في الـ Image؟

نبدأ بأبسط أمر، وهو أن نسأل الـ Container عن إصداره وعن الأدوات التي فيه:

docker pull swift:6.4.2-noble
docker run --rm swift:6.4.2-noble bash -c 'swift --version; uname -m; head -1 /etc/os-release
  ls /usr/bin | grep -E "^swift|^sourcekit|^lldb|^clang$"'

والمخرج سوف يكون كما يلي:

Swift version 6.4.2 (swift-6.4.2-RELEASE)
Target: x86_64-unknown-linux-gnu
x86_64
PRETTY_NAME="Ubuntu 24.04.5 LTS"
clang lldb lldb-argdumper lldb-dap lldb-server sourcekit-lsp swift swift-api-checker.py
swift-api-digester swift-autolink-extract swift-build swift-build-sdk-interfaces swift-build-tool
swift-cache-tool swift-demangle swift-driver swift-experimental-sdk swift-format swift-frontend
swift-help swift-legacy-driver swift-package swift-package-collection swift-package-registry
swift-plugin-server swift-run swift-sdk swift-symbolgraph-extract swift-synthesize-interface
swift-test swiftc swiftc-legacy-driver

في المخرج أعلاه لاحظ التالي:

  • Target: x86_64-unknown-linux-gnu هو ما يسمى الثلاثية Target Triple: المعالج، ثم المورد، ثم النظام والمكتبة. وهذا ما يكتب له المترجم الكود، وعلى Mac بمعالج Apple سوف ترى arm64-apple-macosx.
  • الأمر swift واجهة واحدة لكل شيء: swift build وswift test وswift run وswift package وswift format، ووراء كل واحد منها برنامج منفصل تراه في القائمة مثل swift-build وswift-test.
  • swiftc هو المترجم الذي تستدعيه مباشرة لملف واحد، وswift-frontend هو المترجم الفعلي الذي يستدعيه الجميع، وswift-driver هو الذي يقسم العمل بينهما.
  • lldb هو المصحح Debugger، وsourcekit-lsp هو خادم اللغة Language Server الذي يعطي المحرر الإكمال والتنقل، وclang موجود لأن Swift تستطيع أن تستورد مكتبات C مباشرة.

والصورة التالية ترتب هذه الأدوات بحسب ما تفعله:

مخطط لأدوات Swift: في الأعلى الأمر swift كواجهة واحدة يتفرع منه build وrun وtest وpackage وformat، وتحته SwiftPM الذي يقرأ Package.swift ويحل الاعتماديات ويستدعي Swift Build، ثم المترجم swiftc وswift-frontend الذي يحول الكود إلى SIL ثم LLVM IR ثم كود الآلة، وبجانبه lldb للتصحيح وsourcekit-lsp للمحرر وswift-format للتنسيق، وفي الأسفل المكتبات التي تعمل وقت التشغيل libswiftCore وFoundation وDispatch وSwift Testing
كل الأدوات في Toolchain واحدة: أمر واحد في الواجهة، وبرامج منفصلة وراءه

وثمن هذا أن الـ Image كبيرة، فحجمها على القرص بعد السحب 5.52 GB، لأن فيها المترجم وLLVM وclang والمصحح والمكتبات بنسختها الكاملة. وهذا لا يهم في جهاز التطوير، ولكنه يهم جداً عندما تبني Image الإنتاج، وسوف نرى في المقال الرابع كيف نبني في Image الأدوات ثم ننقل البرنامج وحده إلى Image صغيرة.

جولة سريعة في الصياغة Syntax مقارنة بما تعرفه

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

الثابت let والمتغير var والاستنتاج Type Inference

let storeName = "متجر التمور"
var itemsSold = 0
itemsSold += 3
let price: Int = 990          // halalas, never Double for money

في Swift تكتب let لقيمة لا تتغير، وvar لمتغير، وهذا مثل const وlet في TypeScript، أو final في Java، ولكن الفرق أن Swift تشجعك بقوة على let، فإذا كتبت var ولم تغير القيمة أبداً أعطاك المترجم تحذيراً يطلب منك تحويلها. والنوع يستنتج من القيمة، فالمترجم يعرف أن storeName نص وitemsSold عدد صحيح، ولكن اللغة ثابتة الأنواع Statically Typed، أي أن itemsSold سوف يبقى Int طول عمره، ولن تستطيع أن تضع فيه نصاً كما في Python. وإذا أردت أن تكتب النوع فهو بعد اسم المتغير ونقطتين كما في TypeScript تماماً.

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

الهيكل struct: قيمة Value Type بدون class

struct Product {
    let id: Int
    let nameAr: String
    var priceHalalas: Int
    var stock: Int
}

إذا كنت قادماً من Java فأول ما تبحث عنه هو الكلاس، ولكن في Swift أغلب الأنواع التي تكتبها تكون struct، وهي نوع قيمة Value Type، أي أن الإسناد ينسخ القيمة كلها، بينما class نوع مرجعي Reference Type يشترك فيه كل من يحمل المرجع. ولاحظ أننا لم نكتب دالة بناء Constructor، فالمترجم يولد لك دالة بناء بكل الخصائص بأسمائها، ولذلك تكتب Product(id: 1, nameAr: "تمر سكري", priceHalalas: 990, stock: 10). والمكتبة القياسية نفسها مبنية على هذه الفكرة، فالنص String والمصفوفة Array والقاموس Dictionary كلها struct، وهذا يختلف تماماً عن Java وPython حيث كل شيء تقريباً مرجع، وهو موضوع المقال الثاني بالتفصيل.

التعداد enum مع القيم المرتبطة Associated Values والـ switch الشامل

enum StockCheck {
    case ok
    case short(available: Int)
    case unknownProduct
}

func check(_ p: Product?, qty: Int) -> StockCheck {
    guard let p else { return .unknownProduct }      // optional unwrapping
    return p.stock >= qty ? .ok : .short(available: p.stock)
}

التعداد enum في Swift أقوى بكثير منه في Java أو C#، فكل حالة يمكن أن تحمل قيماً خاصة بها، مثل short(available:) التي تحمل الكمية المتوفرة، وهذا ما يسمى في لغات أخرى النوع المجموع Sum Type أو الاتحاد المميز Discriminated Union، ويشبه ما تكتبه في TypeScript بـ { kind: "short", available: number }. والفائدة الكبيرة تظهر مع switch، فالمترجم يجبرك أن تعالج كل الحالات، وإذا أضاف زميلك حالة جديدة بعد شهر فسوف يتوقف البناء في كل مكان لم يعالجها، بدل أن تمر الحالة الجديدة بصمت.

ولاحظ أيضاً الدالة: المعامل الأول مكتوب _ p، وهذا يعني أن المستدعي لا يكتب اسمه، بينما الثاني qty يكتب اسمه عند الاستدعاء، فيصبح الاستدعاء check(product, qty: 3). وهذه أسماء المعاملات Argument Labels، وهي من أكثر ما يستغربه القادم من Java، ولكنها تجعل الاستدعاء يقرأ كجملة، وهي جزء من اسم الدالة نفسه. أما Product? فهو الاختياري Optional، أي منتج أو لا شيء، وguard let p else { ... } تقول: إذا كانت القيمة موجودة فخذها باسم p غير اختياري، وإلا اخرج من الدالة، وهذا أسلوب Swift المعتاد في الخروج المبكر Early Exit.

البروتوكولات Protocols والامتدادات Extensions

protocol Priced { var priceHalalas: Int { get } }
extension Product: Priced {}
extension Priced {
    var sar: String {
        "\(priceHalalas / 100).\(String(format2: priceHalalas % 100)) ر.س"
    }
}

البروتوكول protocol هو ما تسميه Java وC# واجهة Interface، أي قائمة متطلبات. والجديد هنا شيئان: الأول أن الامتداد extension يضيف سلوكاً لنوع موجود دون أن تعدل تعريفه، حتى لو كان النوع من مكتبة أخرى مثل String، والثاني أن الامتداد على البروتوكول نفسه يعطي تنفيذاً افتراضياً لكل من يطبقه، فكل نوع يطبق Priced صار عنده sar مجاناً. ولذلك تسمع أن Swift لغة «موجهة بالبروتوكولات» Protocol-Oriented، أي أنك تبني السلوك من بروتوكولات صغيرة وامتدادات بدل شجرة وراثة طويلة من الكلاسات. وفي السطر أيضاً ترى إقحام النص String Interpolation بالصيغة \(...)، وهو ما تكتبه في TypeScript بـ ${...}.

الإغلاقات Closures والمصفوفات والقواميس

let inStock = products.filter { $0.stock > 0 }.map(\.nameAr)
let total = products.reduce(0) { $0 + $1.priceHalalas * $1.stock }
let byId = Dictionary(uniqueKeysWithValues: products.map { ($0.id, $0) })

الإغلاق Closure هو الدالة المجهولة، أي الـ Lambda في Java وPython والـ Arrow Function في TypeScript. وعندما يكون الإغلاق آخر معامل تكتبه خارج الأقواس، وهذا ما يسمى Trailing Closure، ولذلك ترى filter { ... } بدون أقواس عادية. و$0 و$1 أسماء مختصرة للمعاملات، و\.nameAr مسار خاصية Key Path يعني «خذ nameAr من كل عنصر». وهذه السطور الثلاثة تعطيك أسماء المتوفر، وقيمة المخزون كله، وقاموساً من الرقم إلى المنتج.

النصوص Strings: الحرف Character وليس البايت

let name = "قهوة ☕️"
print("الحروف:", name.count, "| بايتات UTF-8:", name.utf8.count, "| وحدات UTF-16:", name.utf16.count)

وهذا الجزء مهم لكل من يكتب نصوصاً عربية، فالنص في Swift سلسلة من الحروف كما يراها الإنسان، وتسمى Grapheme Clusters، وليس سلسلة بايتات كما في C ولا وحدات UTF-16 كما في Java وJavaScript. ولهذا السبب لن تستطيع أن تكتب name[0]، لأن الوصول إلى الحرف رقم 1000 يحتاج المرور على كل ما قبله، واللغة لا تريد أن تخفي عنك هذه التكلفة خلف صيغة تبدو رخيصة.

تشغيل الجولة Tour Run: المخرج سطراً بسطر

نشغل الملف كاملاً:

docker run --rm -v "$PWD:/lab" -w /lab swift:6.4.2-noble bash -c 'time swift tour.swift'

والمخرج سوف يكون كما يلي:

المتجر: متجر التمور | مبيعات: 3
المتوفر: ["تمر سكري", "هيل"]
قيمة المخزون: 18900 هللة
المنتج 1: متوفر
المنتج 2: المتوفر 0 فقط
المنتج 9: غير موجود
الأصل: 10 النسخة: 0
بعد البيع: 7 9.90 ر.س
الأرخص: تمر سكري
الحروف: 6 | بايتات UTF-8: 15 | وحدات UTF-16: 7

real	0m5.615s

في المخرج أعلاه لاحظ التالي:

  • قيمة المخزون 18900 هللة هي 990 × 10 للتمر، و4500 × 0 للقهوة، و2250 × 4 للهيل، أي 9900 + 0 + 9000.
  • المنتج 9 غير موجود، لأن byId[9] يرجع اختيارياً فارغاً nil، والدالة check عالجته في guard دون أي استثناء. والقاموس في Swift لا يرمي خطأ عند مفتاح غير موجود كما في Python، وإنما يرجع Optional ويجبرك أن تقرر.
  • السطر «الأصل: 10 النسخة: 0» هو أهم سطر في الجولة: نسخنا المنتج الأول إلى copy وغيرنا مخزونه، ولم يتغير الأصل، لأن struct قيمة. ولو كان Product كلاساً لكان الاثنان صفراً.
  • «الحروف: 6» لأن ☕️ حرف واحد للإنسان، مع أنه في UTF-8 ستة بايتات، أي ثلاثة للفنجان وثلاثة لمحدد الشكل Variation Selector، وكل حرف عربي بايتان، فالمجموع 4 × 2 + 1 للمسافة + 6 = 15.
  • التشغيل أخذ 5.6 ثانية لملف صغير، وأغلبها ترجمة وليس تنفيذاً، ولذلك وضع التفسير مفيد للتجربة السريعة وللسكربتات، وليس لما يعمل كثيراً.

والجدول التالي يجمع المقارنة لمن يأتي من لغة أخرى:

الفكرةJavaPythonTypeScriptSwift
ثابت ومتغيرfinal int xلا يوجد ثابتconst وletlet وvar
قيمة قد لا توجدnull أو Optional<T>NoneT | undefinedT? ويفحصها المترجم
نوع بيانات بسيطrecord@dataclassinterfacestruct قيمة تنسخ
حالات مع بياناتsealed interfaceاتحاد أنواعاتحاد مميزenum بقيم مرتبطة
واجهةinterfaceProtocolinterfaceprotocol مع extension
دالة مجهولةx -> x * 2lambda x: x * 2x => x * 2{ $0 * 2 }
الأخطاءاستثناءاتاستثناءاتاستثناءاتthrows مع try صريحة
الذاكرةGCعد مراجع + GCGCARC بدون GC

مدير الحزم SwiftPM وشكل المشروع Project Layout

الملف الواحد جيد للتجربة، ولكن مكتبة حساب السعر التي طلبها المدير التقني تحتاج مشروعاً حقيقياً: مكتبة يستخدمها التطبيق، وأداة سطر أوامر للمحاسبة، واختبارات. وفي Swift لا تحتاج أداة خارجية لذلك كما تحتاج Maven أو Gradle في Java، فمدير الحزم Swift Package Manager، واختصاراً SwiftPM، جزء من الـ Toolchain نفسها، ويقوم بعمل npm وأداة البناء معاً. وقد شرحنا فكرة مديري الحزم بشكل عام في مقال مديرو الحزم Package Managers، والفرق بين مدير الحزم وأداة البناء في أدوات البناء Build Tools.

الأمر swift package init: ما الذي ينشئه؟

mkdir hello && cd hello
swift package init --name hello --type executable
find . -type f | sort

والمخرج سوف يكون كما يلي:

Creating executable package: hello
Creating Package.swift
Creating .gitignore
Creating Sources
Creating Sources/hello/hello.swift
Creating Tests/
Creating Tests/helloTests/
Creating Tests/helloTests/helloTests.swift
---
./.build/.buildSystem_debug
./.build/CACHEDIR.TAG
./.gitignore
./Package.swift
./Sources/hello/hello.swift
./Tests/helloTests/helloTests.swift

والملف Package.swift الذي أنشأه هو:

// swift-tools-version: 6.4
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "hello",
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
            name: "hello",
            swiftSettings: [
                .enableUpcomingFeature("ApproachableConcurrency"),
            ],
        ),
        .testTarget(
            name: "helloTests",
            dependencies: ["hello"],
            swiftSettings: [
                .enableUpcomingFeature("ApproachableConcurrency"),
            ],
        ),
    ]
)

في الإعداد أعلاه لاحظ التالي:

  • السطر الأول ليس تعليقاً عادياً، فـ swift-tools-version: 6.4 يحدد أقل إصدار من الأدوات يقرأ هذا الملف، ومن يملك إصداراً أقدم سوف يرى خطأ واضحاً بدل سلوك غريب. وإذا كان فريق التطبيق يستخدم Xcode بإصدار أقدم فاكتب الرقم الذي يناسبهم، ولذلك كتبنا في مشروعنا 6.2.
  • الملف نفسه كود Swift وليس JSON أو XML، فهو يستورد PackageDescription ويعرف قيمة اسمها package، وهذا يعني أن المحرر يكمله لك ويفحصه المترجم.
  • الأهداف Targets هي الوحدات التي تبنى، وكل هدف مجلد في Sources أو Tests بنفس الاسم، وكل هدف هو وحدة Module لها مجال أسماء Namespace خاص بها، وهذا ما يقابل الحزمة Package في Java.
  • القالب في 6.4 يفعل الميزة القادمة ApproachableConcurrency التي تخفف قواعد التزامن في الكود البسيط، وسوف نشرحها في مقال التزامن، ويضع اختباراً جاهزاً بمكتبة Swift Testing، أي أن الاختبار صار من اليوم الأول وليس إضافة لاحقة.
  • المجلد .build ظهر فوراً، وهو مكان كل ما يبنى، ويجب أن يكون في .gitignore، والقالب أضافه فعلاً.

أما الملف hello.swift فهو:

@main
struct hello {
    static func main() {
        print("Hello, world!")
    }
}

والوسم @main يحدد نقطة البداية، مثل public static void main في Java. ويمكنك بدلاً منه أن تسمي الملف main.swift وتكتب الكود في أعلى الملف مباشرة كما فعلنا في tour.swift.

الملف Package.swift: الأهداف Targets والمنتجات Products والاعتماديات Dependencies

مشروع المتجر أكبر قليلاً، فيه ثلاثة أهداف واعتمادية خارجية واحدة هي swift-argument-parser لقراءة معاملات سطر الأوامر، والصورة التالية تبين الشكل كاملاً:

في اليمين شجرة مجلدات مشروع prices: الملف Package.swift والملف Package.resolved والملف .swift-format، والمجلد Sources وفيه PriceKit وفيه Price.swift، وprices وفيه main.swift، والمجلد Tests وفيه PriceKitTests وفيه PriceTests.swift، وفي اليسار مخطط الأهداف: المكتبة PriceKit في الوسط، ويعتمد عليها الهدف التنفيذي prices والهدف PriceKitTests، والهدف prices يعتمد أيضاً على ArgumentParser من حزمة خارجية مقفلة على الإصدار 1.8.2، ومنتج واحد هو الأمر prices
كل هدف Target مجلد ووحدة Module، والمنتج Product هو ما يراه من يستخدم الحزمة
// swift-tools-version: 6.2
import PackageDescription

let package = Package(
    name: "prices",
    products: [
        .executable(name: "prices", targets: ["prices"]),
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.6.0"),
    ],
    targets: [
        .target(name: "PriceKit"),
        .executableTarget(
            name: "prices",
            dependencies: [
                "PriceKit",
                .product(name: "ArgumentParser", package: "swift-argument-parser"),
            ]
        ),
        .testTarget(name: "PriceKitTests", dependencies: ["PriceKit"]),
    ]
)

والفرق بين الهدف والمنتج أن الهدف وحدة داخلية تبنى، والمنتج Product هو ما تعرضه الحزمة لغيرها: مكتبة .library أو برنامج .executable. ولذلك عندما يضيف مطور التطبيق حزمتنا إلى مشروعه سوف يرى المنتجات فقط. والاعتمادية تكتب برابط مستودع Git وقاعدة إصدار، وfrom: "1.6.0" تعني «1.6.0 أو أحدث، ما دام الرقم الأول 1»، أي الإصدار الدلالي Semantic Versioning الذي تعرفه من npm بالعلامة ^. ولا يوجد في SwiftPM سجل مركزي مثل npmjs.com، وإنما يسحب الكود من Git مباشرة، ولكن يوجد فهرس للبحث هو Swift Package Index.

والمكتبة PriceKit فيها نوع واحد هو Price يحمل المبلغ بالهللات ويحسب الضريبة بالأعداد الصحيحة:

/// Money in halalas (1 SAR = 100 halalas). Integers only: no rounding surprises.
public struct Price: Equatable, Sendable, CustomStringConvertible {
    public let halalas: Int

    public init(halalas: Int) { self.halalas = halalas }

    /// Parses "9.90" or "9.9" or "12" into halalas. Returns nil for bad input.
    public init?(sar text: String) {
        let parts = text.split(separator: ".", omittingEmptySubsequences: false)
        guard parts.count <= 2, let whole = Int(parts[0]), whole >= 0 else { return nil }
        var frac = 0
        if parts.count == 2 {
            let f = parts[1]
            guard (1...2).contains(f.count), let v = Int(f) else { return nil }
            frac = f.count == 1 ? v * 10 : v
        }
        self.halalas = whole * 100 + frac
    }

    /// 15% VAT, rounded half up, computed in integers.
    public var vat: Price { Price(halalas: (halalas * 15 + 50) / 100) }

    public static func * (p: Price, qty: Int) -> Price { Price(halalas: p.halalas * qty) }
    public static func + (a: Price, b: Price) -> Price { Price(halalas: a.halalas + b.halalas) }

    public var description: String {
        let h = halalas % 100
        return "\(halalas / 100).\(h < 10 ? "0" : "")\(h)"
    }
}

في الكود أعلاه لاحظ التالي:

  • public ضرورية، لأن الوصول الافتراضي في Swift هو internal، أي داخل نفس الوحدة فقط، فإذا نسيتها لن يرى الهدف prices النوع أصلاً. وهذا عكس Python حيث كل شيء ظاهر، وقريب من Java حيث الافتراضي داخل الحزمة.
  • init? دالة بناء قابلة للفشل Failable Initializer، ترجع Price?، فالسعر «abc» لا يرمي استثناء وإنما يعطي nil، والمستدعي مجبر أن يتعامل مع ذلك.
  • تعريف المعاملات * و+ على النوع نفسه هو زيادة تحميل المعاملات Operator Overloading، وهي في Swift دوال ثابتة static عادية.
  • البروتوكولات بعد النقطتين: Equatable يولد المترجم لها المقارنة == تلقائياً، وSendable تقول إن النوع آمن للمشاركة بين المهام المتزامنة، وCustomStringConvertible تحدد شكله عند الطباعة.
  • الضريبة (halalas * 15 + 50) / 100 هي التقريب لأقرب هللة مع تقريب النصف للأعلى Round Half Up، لأن القسمة الصحيحة في Swift تحذف الجزء العشري، وإضافة 50 قبل القسمة على 100 تجعل النصف يصعد. وهذا بالضبط ما كان مختلفاً في التطبيق القديم الذي كان يحسب بـ Double.

الملف Package.resolved وقفل الإصدارات Lockfile

بعد أول بناء ظهر بجانب Package.swift ملف اسمه Package.resolved، وهذا ما يقابل package-lock.json في npm:

.
└── swift-argument-parser<https://github.com/apple/[email protected]>(traits: default)
{
  "originHash" : "7bd42b2152db90188febb3ad8ab0823d63bd7f4d9f947fd6e9d82e8ebee3cb7d",
  "pins" : [
    {
      "identity" : "swift-argument-parser",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-argument-parser.git",
      "state" : {
        "revision" : "6a52f3251125d74daf04fcbd5e6f08a75d074382",
        "version" : "1.8.2"
      }
    }
  ],
  "version" : 3
}

السطر الأول من الأمر swift package show-dependencies، وهو يقول إن قاعدة «1.6.0 أو أحدث» انتهت بالإصدار 1.8.2، والملف يقفل هذا الإصدار برقم الـ Commit نفسه في revision، لذلك فإن من يبني بعدك سوف يحصل على نفس الكود بالضبط حتى لو صدر 1.9.0 غداً. ولهذا السبب يجب أن يدخل هذا الملف في Git للبرامج والخدمات، وهو ما سوف نعتمد عليه في الـ Dockerfile في المقال الرابع حيث ننسخه قبل الكود لتبقى طبقة الاعتماديات في الذاكرة المؤقتة Cache.

البناء Build والتشغيل Run: swift build و swift run

نبني المشروع داخل الـ Container، ونضع مخرجات البناء في Volume منفصل حتى لا يمتلئ مجلد المشروع بمئات الميجابايتات:

docker volume create slc-sw-build
docker run --rm -v "$PWD:/lab" -v slc-sw-build:/build -w /lab/prices swift:6.4.2-noble \
  bash -c 'time swift build --scratch-path /build/prices'

والمخرج سوف يكون كما يلي، بعد حذف سطور التقدم المتكررة:

[49 / 64]
[50 / 65] ArgumentParser
[64 / 75] PriceKit
[68 / 76] prices-product
[70 / 78] ArgumentParserToolInfo
[81 / 81] prices-product
Build complete! (52.28 secs)
real	3m0.854s
user	1m23.836s
sys	0m42.985s

في المخرج أعلاه لاحظ التالي:

  • الأرقام بين القوسين مثل [64 / 75] هي المهام التي انتهت من مجموع مهام البناء، والاسم بعدها هو الهدف الذي يعمل عليه الآن، ولاحظ أن ArgumentParser بني من الكود المصدري، فـ SwiftPM لا ينزل مكتبات مترجمة مسبقاً في العادة، وإنما يبني كل اعتمادية على جهازك.
  • الأداة تقول إن البناء أخذ 52 ثانية، بينما الأمر time يقول ثلاث دقائق، والفرق كله قبل البناء: سحب مستودع swift-argument-parser من GitHub وحل الإصدارات. وهذا يحدث مرة واحدة، ثم يبقى في مجلد البناء.
  • الخيار --scratch-path يحدد مكان مجلد .build، وفائدته هنا أن المجلد صار في Volume داخل Docker، وليس في مجلد Windows المشترك الذي يكون بطيئاً جداً مع آلاف الملفات الصغيرة.

ثم نشغل الأداة:

swift run --scratch-path /build/prices prices 9.90 --qty 3
swift run --scratch-path /build/prices prices 9.999
"$(swift build --scratch-path /build/prices --show-bin-path)/prices" abc; echo "exit=$?"
"$(swift build --scratch-path /build/prices --show-bin-path)/prices" --help

والمخرج سوف يكون كما يلي:

Build complete! (14.04 secs)
المجموع: 29.70 | الضريبة: 4.46 | الإجمالي: 34.16 ر.س

Build complete! (11.35 secs)
Error: سعر غير صحيح: 9.999
Usage: prices <unit> [--qty <qty>]
  See 'prices --help' for more information.

Error: سعر غير صحيح: abc
Usage: prices <unit> [--qty <qty>]
  See 'prices --help' for more information.
exit=64

OVERVIEW: يحسب السعر مع ضريبة القيمة المضافة 15%

USAGE: prices <unit> [--qty <qty>]

ARGUMENTS:
  <unit>                  سعر الوحدة بالريال، مثل 9.90

OPTIONS:
  -q, --qty <qty>         الكمية (default: 1)
  -h, --help              Show help information.

في المخرج أعلاه لاحظ التالي:

  • السطر الأول هو جواب مشكلة المحاسبة: 9.90 × 3 = 29.70، والضريبة 4.455 صارت 4.46 بالتقريب للأعلى، والإجمالي 34.16، وهو نفس ما يحسبه السيرفر.
  • كل swift run يتأكد أولاً أن البناء محدث، ولذلك أخذ 11 إلى 14 ثانية حتى بدون أي تغيير، وأغلبها فحص الملفات على المجلد المشترك. وفي العمل اليومي شغل البرنامج المبني مباشرة من المسار الذي يعطيه --show-bin-path كما في السطر الثالث.
  • رمز الخروج 64 عند الخطأ هو EX_USAGE المعروف في Unix، والمكتبة وضعته لأننا رمينا ValidationError، وهذا يعني أن السكربتات التي تستدعي الأداة تستطيع أن تفرق بين خطأ المستخدم والعطل.
  • صفحة المساعدة ولدت كلها من الكود: النص العربي من abstract وhelp، والقيمة الافتراضية للكمية من var qty: Int = 1، والاختصار -q من .shortAndLong.

والكود الذي ولد كل ذلك قصير:

import ArgumentParser
import PriceKit

@main
struct Prices: ParsableCommand {
    static let configuration = CommandConfiguration(abstract: "يحسب السعر مع ضريبة القيمة المضافة 15%")

    @Argument(help: "سعر الوحدة بالريال، مثل 9.90")
    var unit: String

    @Option(name: .shortAndLong, help: "الكمية")
    var qty: Int = 1

    func run() throws {
        guard let price = Price(sar: unit) else {
            throw ValidationError("سعر غير صحيح: \(unit)")
        }
        let subtotal = price * qty
        let vat = subtotal.vat
        print("المجموع: \(subtotal) | الضريبة: \(vat) | الإجمالي: \(subtotal + vat) ر.س")
    }
}

والكلمات التي تبدأ بـ @ مثل @Argument و@Option هي أغلفة الخصائص Property Wrappers، أي نوع يغلف الخاصية ويضيف سلوكاً حولها، وهنا السلوك هو قراءة القيمة من سطر الأوامر وتحويلها إلى النوع المطلوب. وتشبه في شكلها الـ Annotations في Java والـ Decorators في Python، ولكنها تعمل وقت الترجمة وليس بالانعكاس Reflection وقت التشغيل. ولاحظ throws بعد run()، فالدالة تعلن أنها قد ترمي خطأ، والمترجم يجبر كل من يستدعيها أن يكتب try، وهذا موضوع المقال الثاني.

الاختبارات Testing: Swift Testing و XCTest

في Swift مكتبتان للاختبار. الأولى XCTest، وهي القديمة المأخوذة من عالم Objective-C، وتكتب فيها كلاساً يرث من XCTestCase ودوال تبدأ بكلمة test وتستخدم XCTAssertEqual. والثانية Swift Testing التي دخلت مع Swift 6، وهي ما يضعه القالب الآن، وتكتب فيها دوال عادية عليها الوسم @Test وتتحقق بالماكرو #expect. وفي Swift 6.4 صار بإمكانك أن تخلط بينهما في نفس الاختبار أثناء الترحيل، وهذا موضح في إعلان الإصدار.

import Testing
@testable import PriceKit

@Test func parsesSar() {
    #expect(Price(sar: "9.90")?.halalas == 990)
    #expect(Price(sar: "9.9")?.halalas == 990)
    #expect(Price(sar: "12")?.halalas == 1200)
}

@Test(arguments: ["", "abc", "9.999", "-1", "1.2.3"])
func rejectsBadInput(_ text: String) {
    #expect(Price(sar: text) == nil)
}

@Test func vatRoundsHalfUp() {
    let subtotal = Price(sar: "9.90")! * 3
    #expect(subtotal.halalas == 2970)
    #expect(subtotal.vat.halalas == 446)          // 445.5 -> 446
    #expect((subtotal + subtotal.vat).description == "34.16")
}

و@testable import تسمح للاختبار برؤية ما هو internal في المكتبة، و@Test(arguments:) اختبار بمعاملات Parameterized Test، أي أن الدالة تعمل مرة لكل قيمة في المصفوفة كاختبار مستقل. نشغل الاختبارات:

time swift test --scratch-path /build/prices

والمخرج سوف يكون كما يلي:

Build complete! (28.25 secs)
Test Suite 'All tests' started at 2026-10-31 07:35:07.840
Test Suite 'Debug-linux-x86_64.xctest' started at 2026-10-31 07:35:07.844
Test Suite 'Debug-linux-x86_64.xctest' passed at 2026-10-31 07:35:07.844
	 Executed 0 tests, with 0 failures (0 unexpected) in 0.0 (0.0) seconds
Test Suite 'All tests' passed at 2026-10-31 07:35:07.844
	 Executed 0 tests, with 0 failures (0 unexpected) in 0.0 (0.0) seconds
◇ Test run started.
↳ Testing Library Version: 6.4.2 (288a2328ff84644)
↳ Target Platform: x86_64-unknown-linux-gnu
◇ Test rejectsBadInput(_:) started.
◇ Test parsesSar() started.
◇ Test vatRoundsHalfUp() started.
◇ Test case passing 1 argument text → "" to rejectsBadInput(_:) started.
◇ Test case passing 1 argument text → "abc" to rejectsBadInput(_:) started.
◇ Test case passing 1 argument text → "9.999" to rejectsBadInput(_:) started.
◇ Test case passing 1 argument text → "-1" to rejectsBadInput(_:) started.
◇ Test case passing 1 argument text → "1.2.3" to rejectsBadInput(_:) started.
✔ Test rejectsBadInput(_:) with 5 test cases passed after 0.001 seconds.
✔ Test parsesSar() passed after 0.001 seconds.
✔ Test vatRoundsHalfUp() passed after 0.001 seconds.
✔ Test run with 3 tests in 0 suites passed after 0.022 seconds.

real	0m58.682s

في المخرج أعلاه لاحظ التالي:

  • القسم الأول «Executed 0 tests» ليس خطأ، وإنما XCTest تعمل أولاً ولا تجد اختبارات لها، ثم تعمل Swift Testing. فالأمر swift test يشغل المكتبتين معاً دائماً، وإذا رأيت صفراً في القسم الأول وظننت أن اختباراتك لم تعمل فاقرأ القسم الثاني.
  • الاختبارات الثلاثة بدأت معاً تقريباً، لأن Swift Testing تشغل الاختبارات بالتوازي افتراضياً، وهذا يعني أن الاختبار الذي يعتمد على حالة مشتركة مثل قاعدة بيانات واحدة يجب أن يعلن ذلك، وسوف نرى الخيار .serialized في اختبارات الخدمة.
  • كل قيمة من القيم الخمس في rejectsBadInput ظهرت كحالة مستقلة باسمها، فإذا فشلت «9.999» وحدها عرفت ذلك من المخرج مباشرة.
  • الاختبارات نفسها أخذت 0.022 ثانية، ولكن الأمر أخذ قرابة دقيقة، وأغلبها بناء هدف الاختبار وأداة التشغيل لأول مرة.

ولكي ترى ما يحدث عند الفشل غيرنا القيمة المتوقعة من 446 إلى 445، وهي بالضبط القيمة التي كان يعطيها التطبيق القديم:

◇ Test vatRoundsHalfUp() started.
✘ Test vatRoundsHalfUp() recorded an issue at PriceTests.swift:18:5: Expectation failed: subtotal.vat.halalas == 445
↳ subtotal.vat.halalas == 445 → false
↳   subtotal.vat.halalas → 446
✘ Test vatRoundsHalfUp() failed after 0.001 seconds with 1 issue.
✘ Test run with 1 test in 0 suites failed after 0.116 seconds with 1 issue.
Note: Some test targets reported failures:
  - PriceKitTests (Swift Testing)

ولاحظ أن رسالة الفشل لا تقول «توقعت 445 ووجدت 446» فقط، وإنما تفكك التعبير نفسه: الطرف الأيسر subtotal.vat.halalas قيمته 446. وهذا لأن #expect ماكرو Macro، أي كود يعمل وقت الترجمة ويرى التعبير الذي كتبته كاملاً، فيستطيع أن يطبع كل جزء منه، وهذا ما كان يحتاج في XCTest دالة مختلفة لكل نوع من المقارنات. وقد شرحنا أنواع الاختبارات وأين يقف كل نوع في مقال أنواع الاختبارات للمبتدئين.

التنسيق Formatter والفحص Linter: swift format

منذ Swift 6 صار المنسق swift-format جزءاً من الـ Toolchain، فلا تحتاج تثبيته منفصلاً، ويعمل بأمرين: swift format لإعادة كتابة الملفات، وswift format lint لفحصها دون تغييرها، وهو ما تضعه في CI. شغلنا الفحص على المشروع:

swift format lint --recursive Sources Tests

والمخرج سوف يكون كما يلي، أول عشرين سطراً:

Sources/PriceKit/Price.swift:3:1: warning: [Indentation] unindent by 2 spaces
Sources/PriceKit/Price.swift:5:1: warning: [Indentation] unindent by 2 spaces
Sources/PriceKit/Price.swift:7:1: warning: [Indentation] unindent by 2 spaces
Sources/PriceKit/Price.swift:8:1: warning: [Indentation] unindent by 2 spaces
Sources/PriceKit/Price.swift:9:1: warning: [Indentation] unindent by 4 spaces
Sources/PriceKit/Price.swift:13:1: warning: [Indentation] unindent by 6 spaces
...

وهذه أول مشكلة حقيقية واجهناها في التجربة: كل سطر في المشروع «خطأ»، والسبب أن الإعداد الافتراضي لـ swift-format مسافتان للإزاحة، بينما القالب الذي ينشئه swift package init وكود المكتبة القياسية وأغلب الكود الذي تقرؤه على الإنترنت بأربع مسافات. والحل ليس أن تعيد تنسيق الكود كله بمسافتين، وإنما أن تضع ملف إعداد في جذر المشروع اسمه .swift-format يقرؤه المنسق تلقائياً:

{
  "version": 1,
  "indentation": { "spaces": 4 },
  "lineLength": 120
}

وبعد الإعداد بقي تحذير واحد:

Tests/PriceKitTests/PriceTests.swift:18:41: warning: [Spacing] remove 8 spaces
exit=0

وهو المسافات الكثيرة قبل التعليق في سطر الضريبة، أي أن المنسق يمنع محاذاة التعليقات يدوياً، وهذه قاعدة يمكن أن تقبلها أو تعطلها في نفس الملف. ولاحظ أن رمز الخروج صفر رغم التحذير، فإذا أردت أن يفشل CI عند أي تحذير فأضف الخيار --strict. وإذا كان فريقك يستخدم SwiftLint من قبل، وهو الأشهر في مشاريع iOS، فهو أداة منفصلة بقواعد أكثر، وأغلب الفرق اليوم تستخدم swift-format للتنسيق وSwiftLint لقواعد الأسلوب إذا احتاجتها.

البناء للإنتاج Release Build والربط الثابت Static Linking

كل ما بنيناه حتى الآن بوضع التصحيح Debug، وهو الافتراضي، وفيه التحسينات معطلة ومعلومات التصحيح كاملة. وأداة المحاسبة سوف تعمل على سيرفر، فنبنيها بوضع الإنتاج ونرى حجمها وما تحتاجه:

swift build --scratch-path /build/prices -c release
BIN="$(swift build --scratch-path /build/prices -c release --show-bin-path)/prices"
ls -la "$BIN"; ldd "$BIN" | grep -iE "swift|foundation|dispatch"
swift build --scratch-path /build/prices -c release --static-swift-stdlib
ls -la "$BIN"; ldd "$BIN" | grep -ciE "swift"

والمخرج سوف يكون كما يلي:

Build complete! (124.79 secs)
real	2m56.521s
3630712 /build/prices/out/Products/Release-linux-x86_64/prices
libswiftCore.so
libswiftObservation.so
libswift_Concurrency.so
libswift_StringProcessing.so
libswift_RegexParser.so
libswiftGlibc.so
libFoundationEssentials.so
libdispatch.so
libswift_Builtin_float.so
libswiftSynchronization.so
libBlocksRuntime.so
Build complete! (141.01 secs)
real	2m31.857s
24823296 /build/prices/out/Products/Release-linux-x86_64/prices
swift libs linked dynamically: 0

في المخرج أعلاه لاحظ التالي:

  • البناء للإنتاج أبطأ بكثير من التصحيح، دقيقتان مقابل أقل من دقيقة، لأن المترجم يحسن الكود عبر الوحدات كلها، وهذا ثمن تدفعه مرة في CI وليس في كل تعديل.
  • المسار out/Products/Release-linux-x86_64 جديد في Swift 6.4، لأن SwiftPM صار يستخدم نظام البناء Swift Build، وكان المسار في الإصدارات السابقة release/ مباشرة. لذلك لا تكتب المسار في سكربتاتك، وإنما خذه دائماً من --show-bin-path.
  • البرنامج العادي حجمه 3.6 MB، ولكنه يحتاج إحدى عشرة مكتبة من مكتبات Swift وقت التشغيل Runtime، مثل libswiftCore.so وlibswift_Concurrency.so وlibFoundationEssentials.so، فإذا نسخته إلى سيرفر Ubuntu عادي فلن يعمل.
  • مع --static-swift-stdlib صار الحجم 24.8 MB، ولكنه لا يحتاج أي مكتبة من Swift، وإنما مكتبات النظام العادية مثل glibc، وهذا يجعله يعمل على أي Ubuntu بنفس الإصدار. وإذا أردت برنامجاً لا يعتمد حتى على glibc فهناك Static Linux SDK الذي يربط بـ musl.

والصورة التالية تلخص الطريقين:

مقارنة بين بناءين للإنتاج: في اليمين الربط الديناميكي حيث البرنامج 3.6 MB ويحتاج إحدى عشرة مكتبة Swift مثل libswiftCore وlibswift_Concurrency وlibFoundationEssentials وlibdispatch موجودة على الجهاز، ولذلك يحتاج Image فيها Swift Runtime مثل swift:6.4.2-noble-slim، وفي اليسار الربط الثابت بالخيار --static-swift-stdlib حيث البرنامج 24.8 MB ولا يحتاج أي مكتبة Swift ويعمل على ubuntu:noble عادي، وفي الأسفل ملاحظة بأن الـ Image الكاملة للأدوات 5.52 GB للبناء فقط
برنامج صغير يحتاج مكتبات Swift، أو برنامج أكبر يحمل مكتباته معه

ماذا يحدث إذا شغلت كود Debug في الإنتاج؟

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

let start = ContinuousClock.now
var total = 0
for i in 0..<200_000_000 { total &+= i % 7 }
let elapsed = ContinuousClock.now - start
print("total=\(total) elapsed=\(elapsed)")
swiftc -Onone bench.swift -o /tmp/b0 && swiftc -O bench.swift -o /tmp/b1
for i in 1 2; do echo "-Onone: $(/tmp/b0)"; echo "-O:     $(/tmp/b1)"; done
echo "interpreted: $(swift bench.swift)"

والمخرج سوف يكون كما يلي:

-Onone: total=599999994 elapsed=28.20423716 seconds
-O:     total=599999994 elapsed=0.520476748 seconds
-Onone: total=599999994 elapsed=28.71307653 seconds
-O:     total=599999994 elapsed=0.762876094 seconds
interpreted: total=599999994 elapsed=8.816966273 seconds

نفس الكود ونفس النتيجة، ولكن وضع التصحيح أخذ 28 ثانية والإنتاج نصف ثانية تقريباً، أي أبطأ بأكثر من خمسين مرة. والفرق بين التشغيلين في الإنتاج، 0.52 و0.76، هو ضجيج الجهاز المضيف، فهذا Container على جهاز تطوير يعمل عليه غيره، ولذلك نكرر كل قياس ولا نأخذ رقماً واحداً. ووضع التفسير، أي swift bench.swift، جاء بين الاثنين بثماني ثوان وثمانية أعشار، فهو مريح للتجربة السريعة ولكنه ليس طريقة لتشغيل ما يهمك أداؤه. ولاحظ &+= بدل +=، وهي الجمع الذي يلتف عند التجاوز Wrapping بدل أن يوقف البرنامج، وسوف نرى في المقال الثاني ماذا يحدث مع += العادية إذا تجاوزت القيمة الحد.

والدرس العملي: لا تحكم على أداء Swift من تجربة بوضع التصحيح، ولا تنشر برنامجاً للإنتاج بدون -c release، وإذا شكا زميل من أن «Swift بطيئة» فأول سؤال تسأله هو كيف بنى البرنامج. وقد شرحنا ماذا يفعل المترجم في كل مرحلة من مراحل التحسين في مقال المترجم والمفسر و JIT.

رسم أعمدة أفقية لنفس الحلقة التي تجمع 200 مليون رقم: بناء التصحيح -Onone أخذ 28.2 و28.7 ثانية، والتفسير بالأمر swift أخذ 8.8 ثانية، وبناء الإنتاج -O أخذ 0.52 و0.76 ثانية، مع شرح أن التصحيح يبقي كل فحص واستدعاء كدالة حقيقية، وأن الفرق بين تشغيلي الإنتاج ضجيج الجهاز المضيف
نفس الكود: أكثر من خمسين ضعفاً بين وضع التصحيح ووضع الإنتاج

المحرر IDE والـ LSP: VS Code و sourcekit-lsp و Xcode

على Mac سوف يستخدم أغلب الناس Xcode، ولكن على Linux وWindows الخيار الرسمي هو إضافة Swift في VS Code، وهي تعمل فوق sourcekit-lsp الموجود في الـ Toolchain، أي نفس خادم اللغة الذي رأيناه في قائمة الأدوات، ولذلك تعمل نفس الإضافة في أي محرر يدعم بروتوكول LSP مثل Neovim وEmacs، وتعطيك الإكمال والانتقال إلى التعريف وتشغيل الاختبارات والتصحيح بـ lldb. وفي Swift 6.4 صارت الإضافة منشورة أيضاً في سجل Open VSX، فتعمل في المحررات المبنية على VS Code مثل Cursor. والنقطة العملية أن sourcekit-lsp يحتاج أن يبني المشروع مرة ليفهم الوحدات والاعتماديات، فإذا فتحت المشروع ورأيت أن الإكمال لا يعمل في الدقائق الأولى فانتظر انتهاء البناء أولاً.

مشاكل واجهناها أثناء التجربة Troubleshooting

  • كل سطر خطأ في swift format: الإزاحة الافتراضية مسافتان والكود بأربع، والحل ملف .swift-format في جذر المشروع كما رأينا، وضعه في المستودع من اليوم الأول حتى لا يختلف المطورون.
  • تحذير safeExec: signal(32, SIG_DFL) failed: Invalid argument: ظهر مرتين مع كل swift run وswift build داخل Docker على Windows، وهو تحذير من SwiftPM عند إعادة ضبط إشارات النظام قبل تشغيل البرنامج، ولم يؤثر على أي نتيجة، ولذلك حذفناه من المخرجات بـ grep -v. إذا رأيته فلا تبحث عن خطأ في كودك.
  • البناء البطيء على المجلد المشترك: أول بناء أخذ ثلاث دقائق، وكل swift run بعده أكثر من عشر ثوان بدون أي تغيير، والسبب أن الكود في مجلد Windows مربوط داخل الـ Container، وكل فحص لتاريخ ملف يعبر هذا الربط. ونقل مجلد البناء إلى Volume بالخيار --scratch-path خفف المشكلة كثيراً، والحل الأفضل على Windows أن يكون المشروع كله داخل WSL أو داخل Volume.
  • المسار الجديد للبرنامج المبني: سكربت قديم يفترض .build/release/prices لن يجد البرنامج مع Swift 6.4، والحل --show-bin-path دائماً.

أخطاء شائعة Common Mistakes في الأيام الأولى

  • نسيان public في المكتبة: الوصول الافتراضي internal، فيظهر خطأ «cannot find type in scope» في الهدف الآخر رغم أنك أضفت الاعتمادية، والحل أن تعلن عن كل ما يجب أن يراه غيرك بـ public.
  • البحث عن الكلاس أولاً: القادم من Java يكتب كل شيء class، والأسلوب الصحيح في Swift أن تبدأ بـ struct ولا تنتقل إلى class إلا عندما تحتاج هوية مشتركة، وهذا موضوع المقال الثاني.
  • علامة التعجب ! لإسكات المترجم: عندما يرفض المترجم اختيارياً يكتب المبتدئ ! بعده، وهذا يحول الخطأ من خطأ ترجمة إلى توقف البرنامج في الإنتاج، والصحيح if let أو guard let أو ??.
  • قياس الأداء بوضع التصحيح: رأينا الفرق خمسين ضعفاً، فلا تقارن Swift بغيرها إلا ببناء -c release.
  • عدم إدخال Package.resolved في Git: فيبني CI إصداراً مختلفاً من الاعتماديات عن جهازك، وتظهر أخطاء لا تراها عندك.
  • افتراض أن كل مكتبات iOS تعمل على Linux: أي حزمة تستورد UIKit أو SwiftUI أو CoreData لن تبنى على Linux، فاحرص أن تبقى المكتبة المشتركة على المكتبة القياسية وFoundation فقط.

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

قائمة تحقق Checklist: مشروع Swift جاهز للفريق

البندالأمر أو الملفلماذا
إصدار ثابت للأدواتswift:6.4.2-noble أو swiftlyنفس المترجم على جهازك وفي CI
أقل إصدار يقرأ الحزمة// swift-tools-version:من يملك أدوات أقدم يرى خطأ واضحاً
قفل الاعتمادياتPackage.resolved في Gitنفس الكود في كل بناء
إعداد المنسق.swift-formatقاعدة واحدة للفريق، وlint --strict في CI
الاختباراتswift testSwift Testing وXCTest معاً
بناء الإنتاجswift build -c releaseالتصحيح أبطأ بعشرات المرات
مسار البرنامج--show-bin-pathالمسار تغير في 6.4
مكتبات وقت التشغيلldd أو --static-swift-stdlibالبرنامج العادي يحتاج Swift Runtime
كود مشترك مع التطبيقالمكتبة القياسية وFoundation فقطلا UIKit ولا SwiftUI في ما يبنى على Linux

تدريب عملي

  • أضف إلى الأداة خياراً --no-vat من نوع @Flag، واكتب اختباراً للحساب بدون ضريبة، ثم اقرأ صفحة --help الجديدة.
  • غير قاعدة الاعتمادية إلى exact: "1.6.0"، وشغل swift package update، ولاحظ كيف تغير Package.resolved.
  • احسب الضريبة في دالة جديدة بـ Double وrounded()، واكتب لها اختباراً بمعاملات لعشرين سعراً، وابحث عن أول سعر يختلف فيه الناتج عن الحساب الصحيح.
  • شغل swift format --in-place --recursive Sources بدون ملف الإعداد، وانظر بـ git diff كم سطراً تغير.

الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

  • Swift لغة مفتوحة المصدر منذ 2015 وتعمل على Linux، وواجهات تطبيقات Apple وحدها هي التي تحتاج Xcode على Mac، ولذلك يمكن أن تكتب المكتبات المشتركة والسيرفر وأدوات سطر الأوامر وتختبرها على Linux.
  • الـ Toolchain واحدة: المترجم وSwiftPM وswift-format وsourcekit-lsp وlldb وSwift Testing، والأمر swift هو الواجهة لها كلها، والـ Image الرسمية 5.52 GB للبناء فقط.
  • أغلب أنواعك struct تنسخ عند الإسناد، وenum يحمل بيانات ويجبرك switch على معالجة كل حالة، والاختياري T? يجعل غياب القيمة شيئاً يفحصه المترجم.
  • المشروع ملف Package.swift بأهداف ومنتجات واعتماديات من Git، والملف Package.resolved يقفل الإصدارات ويجب أن يدخل المستودع.
  • swift test يشغل XCTest ثم Swift Testing، و#expect يفكك التعبير عند الفشل، والاختبارات تعمل بالتوازي افتراضياً.
  • بناء الإنتاج أسرع من التصحيح بأكثر من خمسين مرة في حلقة بسيطة، والبرنامج العادي يحتاج مكتبات Swift وقت التشغيل، والربط الثابت يعطي برنامجاً أكبر يعمل على أي Ubuntu.

والمقال التالي في هذه المرحلة عن الأنواع والذاكرة والأخطاء في Swift، وفيه نقيس ARC ونرى تسرب الذاكرة بأعيننا، ونرى كيف يتوقف البرنامج عند ! على قيمة فارغة. وهذه مقالات تكمل ما بدأناه هنا:

دليلكيف تختار لغة البرمجة Programming Language لمشروعك؟نختار لغة خدمة حقيقية بأسئلة الفريق والتوظيف والمكتبات والرخصة، ثم نكتبها بأربع لغات ونقيس البناء والحجم والذاكرة والطلبات في الثانية بأنفسنا.دليلالبرمجة بعدة لغات Polyglot: متى تفيد ومتى تعقد مشروعك؟نستدعي نفس الدالة من Python بخمس طرق، عبر FFI إلى C وGo وعبر HTTP وgRPC، ونقيس كلفة كل حد بين لغتين وتعقيد البناء وما يحدث عندما يتعطل الطرف الآخر.دليلأنماط البرمجة وأنظمة الأنواع: لماذا تختلف اللغات؟نحل مسألة واحدة بعدة أساليب برمجة، ونرى هل تكتشف اللغة أخطاءك قبل التشغيل أم بعده، وماذا يعني ذلك عند اختيار لغة لمشروعك.دليلالمترجم Compiler والمفسر Interpreter و JIT و WebAssemblyنشغل دالة واحدة بلغة C وPython وJavaScript وWebAssembly على نفس الجهاز، ونفتح ما كتبه كل مترجم لنعرف لماذا اختلف الوقت من ثلث ثانية إلى دقيقة.دليلمديرو الحزم Package Managers: npm و pip و Maven و Cargoنجرب npm وpip وMaven وCargo على مشروع واحد بأربع لغات، فنقيس كم حزمة تأتي مع كل سطر، ونرى كيف يجعل ملف القفل التثبيت متطابقاً، وكيف خدع حرف واحد في اسم مكتبة مطوري Rust.دليلأدوات البناء Build Tools وماذا يحدث عندما تبني مشروعكما الذي يحدث بين ضغطة البناء والملف الناتج، ولماذا ينجح البناء على جهازك ويفشل على سيرفر CI، ومتى يعطيك نفس النتيجة مرتين.دليلأنواع الاختبارات للمبتدئين: من Unit إلى E2E مع Playwrightما الذي يكشفه كل نوع من الاختبارات وما الذي يفوته، وكم اختباراً تكتب من كل نوع، وأين يعمل كل منها في خط CI.دليلتطبيق الجوال: أصيل Native أم Flutter أم React Native؟بنينا نفس تطبيق الطلبات ثلاث مرات بلغة Kotlin وبإطاري Flutter وReact Native، وقسنا الحجم وزمن البناء والإقلاع والذاكرة لنعرف من أين يأتي كل فرق وكيف تختار.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال، وتنفيذ التجربة بـ Swift 6.4.2 في الـ Image الرسمية swift:6.4.2-noble على Docker، مع swift-argument-parser 1.8.2.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.