لنفرض أنك مطور 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.

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 مباشرة.
والصورة التالية ترتب هذه الأدوات بحسب ما تفعله:

وثمن هذا أن الـ 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 ثانية لملف صغير، وأغلبها ترجمة وليس تنفيذاً، ولذلك وضع التفسير مفيد للتجربة السريعة وللسكربتات، وليس لما يعمل كثيراً.
والجدول التالي يجمع المقارنة لمن يأتي من لغة أخرى:
| الفكرة | Java | Python | TypeScript | Swift |
|---|---|---|---|---|
| ثابت ومتغير | final int x | لا يوجد ثابت | const وlet | let وvar |
| قيمة قد لا توجد | null أو Optional<T> | None | T | undefined | T? ويفحصها المترجم |
| نوع بيانات بسيط | record | @dataclass | interface | struct قيمة تنسخ |
| حالات مع بيانات | sealed interface | اتحاد أنواع | اتحاد مميز | enum بقيم مرتبطة |
| واجهة | interface | Protocol | interface | protocol مع extension |
| دالة مجهولة | x -> x * 2 | lambda x: x * 2 | x => x * 2 | { $0 * 2 } |
| الأخطاء | استثناءات | استثناءات | استثناءات | throws مع try صريحة |
| الذاكرة | GC | عد مراجع + GC | GC | ARC بدون 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 لقراءة معاملات سطر الأوامر، والصورة التالية تبين الشكل كاملاً:

// 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.
والصورة التالية تلخص الطريقين:

ماذا يحدث إذا شغلت كود 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.

المحرر 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 test | Swift 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.