EduFlow
أساسيات Swift لتطبيقات iOS
⁦0%⁩

الوحدة 1 · لغة Swift

المجموعات والتحكّم في التدفّق

17 دقراءة 4 داختر بحكمة

الدرس في لمحة

خزّن المصاريف في المصفوفات والقواميس والمجموعات، وتنقّل بينها بحلقة for-in، واتّخذ القرارات بجملة switch شاملة يفحصها الـ compiler عنك.

  1. 1المصفوفات: مرتّبة، مفهرسة، محدّدة النوع
  2. 2القواميس: بحث قد لا يجد شيئًا
  3. 3المجموعات: قيم فريدة
  4. 4اختيار المجموعة المناسبة
  5. 5switch: شاملة بالتصميم

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

المصروف الواحد قيمة. ومصاريف شهر كامل مجموعة (collection). يحتاج Pocket Budget إلى ثلاثة أشكال من المجموعات: قائمة مرتّبة بالمبالغ، وجدول بحث من التصنيف إلى الحدّ الشهري، وكيس من الوسوم الفريدة. تعطيك Swift هذه الثلاثة بالضبط، وكلها محدّدة النوع، فلا يمكن لمصفوفة مبالغ أن تحتوي String بالخطأ.

المصفوفات: مرتّبة، مفهرسة، محدّدة النوع

Swift
import Foundation

var amounts: [Decimal] = [12.5, 4.25, 60]
amounts.append(18)

print(amounts.count)     // 4
print(amounts[0])        // 12.5
print(amounts.first as Any)   // Optional(12.5)

تُقرأ [Decimal] «مصفوفة من Decimal». الفهارس تبدأ من 0. لاحظ الفرق بين amounts[0] وamounts.first: الوصول بالفهرس يثق بك ويُسقط التطبيق مع “Index out of range” إذا كانت المصفوفة فارغة، بينما first يعيد optional ويتيح لك التعامل مع الحالة الفارغة بالأدوات التي تعلّمتها في الدرس السابق.

في أغلب الأحيان لا تحتاج إلى الفهارس أصلًا. حلقة for-in تزور كل عنصر:

Swift
var total: Decimal = 0
for amount in amounts {
    total += amount
}
print(total)             // 94.75

for (index, amount) in amounts.enumerated() {
    print("\(index + 1). \(amount)")
}

تعطيك enumerated() أزواجًا من الموضع والقيمة، وهذه هي الطريقة الآمنة لترقيم الصفوف. والنطاقات تعمل مع for-in أيضًا، بنوعين:

Swift
for day in 1...30 { }               // closed range: 1 through 30
for i in 0..<amounts.count { }      // half-open: stops before count

النطاق المغلق يشمل حدّه الأعلى؛ أما النطاق نصف المفتوح فيتوقّف قبله بواحد، وهذا بالضبط ما تحتاجه فهارس المصفوفات.

القواميس: بحث قد لا يجد شيئًا

القاموس (dictionary) يربط مفاتيح بقيم. يحتفظ Pocket Budget بحدّ شهري لكل تصنيف:

Swift
let limits: [String: Decimal] = ["Food": 300, "Transport": 120]
var spent: [String: Decimal] = ["Food": 280]

let foodLimit = limits["Food"]          // Decimal?, Optional(300)
let funLimit = limits["Fun"] ?? 0       // Decimal, 0

spent["Fun", default: 0] += 15          // creates "Fun": 15

كل بحث يعيد optional، لأن المفتاح قد لا يكون موجودًا. إنها فكرة الأمان نفسها مرة أخرى، لكنها هذه المرة مبنية داخل المجموعة. والصيغة default: هي الطريقة المعتادة لحساب المجاميع التراكمية: تقرأ القيمة الموجودة أو تبدأ من القيمة الافتراضية التي حدّدتها، ثم تكتب النتيجة مرة أخرى.

المجموعات: قيم فريدة

Swift
var tags: Set<String> = ["work", "travel"]
tags.insert("work")          // already there, nothing changes
print(tags.count)            // 2
print(tags.contains("travel"))  // true

المجموعة (Set) لا ترتيب فيها ولا تكرار، ويبقى contains سريعًا مهما كثرت عناصرها. استخدمها للوسوم، والفلاتر المختارة، وفحوصات «هل رأيت هذا المعرّف من قبل؟».

اختيار المجموعة المناسبة

تحتاج إلى استخدم مثال من Pocket Budget
ترتيب، والتكرار مسموح، والوصول بالموضع مصفوفة (Array) مصاريف هذا الشهر، الأحدث أولًا
البحث عن قيمة بمفتاح قاموس (Dictionary) الحدّ الشهري لكل تصنيف
العضوية والتفرّد، والترتيب لا يهم مجموعة (Set) الوسوم على مصروف واحد

إن تردّدت، فابدأ بمصفوفة. إنها المجموعة التي تعرضها قوائم SwiftUI، وأسهلها قراءةً في الـ debugger. انتقل إلى القاموس حين تجد نفسك تبحث في مصفوفة عن «العنصر الذي يحمل هذا الاسم»، وإلى المجموعة حين تجد نفسك تفحص التكرار قبل كل إضافة.

المجموعات الثلاث كلها قيم، لا كائنات مشتركة. var copy = amounts تعطيك مصفوفة مستقلّة: الإضافة إلى copy لا تمسّ amounts. لهذا السلوك اسم، سلوك القيمة (value semantics)، وله درس خاص في القسم التالي لأنه يحدّد طريقة تصميمك لكل نوع في نموذج بياناتك.

switch: شاملة بالتصميم

جملة switch في Swift أكثر بكثير من سلسلة فحوصات مساواة. فهي تطابق النطاقات، وتربط القيم، وتضيف الشروط، والـ compiler يصرّ على معالجة كل قيمة ممكنة. هكذا يختار Pocket Budget رسالة الحالة بناءً على النسبة المستهلكة من الميزانية:

BudgetStatus.swift
func status(percentUsed: Int) -> String {
    switch percentUsed {
    case ..<0:
        return "Check your numbers"
    case 0..<50:
        return "Plenty left"
    case 50..<90:
        return "On track"
    case 90...100:
        return "Nearly at your limit"
    default:
        return "Over budget"
    }
}

print(status(percentUsed: 95))   // Nearly at your limit

هناك فرقان عن لغات عائلة C. أولًا، لا يوجد break ولا انتقال عرضي إلى الحالة التالية: متى طابقت حالة، انتهت الـ switch. ثانيًا، احذف default وستفشل الترجمة مع “switch must be exhaustive”، لأن النوع Int له قيم لا تغطّيها حالاتك. يبدو هذا الخطأ مزعجًا مع الأعداد الصحيحة. لكن مع الـ enums، في القسم التالي، يصبح أنفع خطأ في اللغة: أضف حالة جديدة إلى نموذجك، وسيعرض لك الـ compiler كل switch نسيت تحديثها.

خط أعداد مقسوم إلى خمس حالات: أقل من 0، ومن 0 حتى ما قبل 50، ومن 50 حتى ما قبل 90، ومن 90 حتى 100، وdefault لكل ما فوق 100. معًا تغطّي كل عدد صحيح. ..<0 0..<50 50..<90 90...100 default النطاقات لا تتداخل، والحالة default تلتقط كل Int آخر
كيف تغطّي نطاقات الحالة في Pocket Budget كل قيمة Int

يمكن للحالات أيضًا أن تربط القيمة المطابَقة وتختبرها بـ where:

Swift
let used: Decimal = 15
let limit: Decimal = 10

switch used {
case 0:
    print("Nothing spent yet")
case let amount where amount > limit:
    print("Over by \(amount - limit)")
default:
    print("Within budget")
}

وفي Swift 5.9 صار ممكنًا استخدام switch وif كتعبيرات، فتعمل الصيغة let label = switch percentUsed { … } حين تنتج كل حالة قيمة واحدة. استخدم الصيغة الأوضح قراءةً؛ قاعدة الشمول واحدة في الحالتين.

صار بإمكانك الآن أن تحمل بيانات شهر كامل وتتّخذ قرارات بشأنها. حلقات التكرار في هذا الدرس تعمل، لكنها مكتوبة بالطريقة الطويلة. بعد ذلك ستجمع المنطق في دوال، وتستبدل معظم هذه الحلقات بـ closures مثل filter وmap وreduce.

أهم النقاط

  • المصفوفة مرتّبة وفهارسها تبدأ من 0؛ والقراءة بعد نهايتها تُسقط التطبيق، لذلك فضّل first وlast وfor-in على الفهارس اليدوية.
  • البحث في القاموس يعيد دائمًا optional، لأن المفتاح قد لا يكون موجودًا؛ والصيغة dict[key, default: 0] تعطيك قيمة بديلة.
  • المجموعة (Set) تخزّن قيمًا فريدة، وفحص contains فيها سريع، ولا ترتيب لعناصرها.
  • جملة switch في Swift يجب أن تغطّي كل قيمة ممكنة، ولا تنتقل ضمنيًا إلى الحالة التالية، ويمكنها مطابقة النطاقات وشروط where.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

لديك let limits = ["Food": 300]. ما نوع limits["Fun"]؟

السؤال 2 من 4

كتبت switch على تصنيف من النوع String، ووضعت حالتين فقط لـ “Food” و"Transport". ماذا يقول الـ compiler؟

السؤال 3 من 4

أيّ مجموعة تناسب «الوسوم التي أضافها المستخدم إلى مصروف، دون تكرار»؟

السؤال 4 من 4

لديك let amounts: [Decimal] = []. ماذا يفعل amounts[0] وقت التشغيل؟

تدريب

اختر بحكمة

توقّع التقرير الشهري

اقرأ هذا المقطع من التقرير الشهري في Pocket Budget وتوقّع بالضبط ما يطبعه. مرّ على كل تصنيف بالترتيب، وتتبّع ما يعيده كل بحث في القاموسين.

Swift
let limits: [String: Decimal] = ["Food": 300, "Transport": 120]
var spent: [String: Decimal] = ["Food": 280]
spent["Fun", default: 0] += 15

for category in ["Food", "Transport", "Fun"] {
    let used = spent[category] ?? 0
    let limit = limits[category] ?? 0
    switch used {
    case 0:
        print("\(category): nothing yet")
    case let amount where amount > limit:
        print("\(category): over by \(amount - limit)")
    default:
        print("\(category): ok")
    }
}
توقّع التقرير الشهري