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

الوحدة 2 · تصميم البيانات بالأنواع

الـ enums مع القيم المرتبطة

16 دقراءة 4 داملأ الفراغات

الدرس في لمحة

استبدل النصوص الهشّة بالـ enums، وأرفق بيانات بكل حالة عبر القيم المرتبطة، ودع جمل switch الشاملة تجد كل مكان يحتاج إلى معالجة حالة جديدة.

  1. 1الـ enums ذات القيم الخام
  2. 2جمل switch الشاملة ميزة
  3. 3القيم المرتبطة: حالات تحمل بيانات
  4. 4استخراج البيانات من جديد
  5. 5enums بدل فوضى القيم المنطقية

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

في القسم السابق كان تصنيف المصروف String. وهذا يعمل إلى أن يكتب أحدهم "Fod"، أو "food" في مكان و"Food" في مكان آخر، فيُسقط المجموع الشهري نصف مشترياتك من البقالة بصمت. لم يكن في النوع ما يحدّد النصوص الصالحة. أما الـ enum (التعداد) فيقول ذلك بالضبط: هذه هي القائمة الكاملة للقيم الممكنة، ولا شيء غيرها مسموح.

الـ enums ذات القيم الخام

Category.swift
enum Category: String, CaseIterable {
    case food
    case transport
    case housing
    case fun

    var title: String {
        rawValue.capitalized
    }

    var symbolName: String {
        switch self {
        case .food: "fork.knife"
        case .transport: "bus"
        case .housing: "house"
        case .fun: "ticket"
        }
    }
}

صار لـ Category أربع قيم بالضبط. اكتب .fod وستفشل الترجمة. والجزء : String يعطي كل حالة قيمة خام (raw value)، وهي افتراضيًا اسم الحالة كنص (Category.food.rawValue تساوي "food")، وهي ما ستحفظه على القرص وتقرؤه من JSON لاحقًا. أما الاتجاه المعاكس فقابل للفشل، لأن ليس كل نص تصنيفًا: Category(rawValue: "fun") تعطي .fun، وCategory(rawValue: "Fun") تعطي nil.

يطلب CaseIterable من الـ compiler أن يولّد Category.allCases، وهي مصفوفة بكل الحالات بترتيب تعريفها. سيمرّ منتقي التصنيفات في Pocket Budget عليها، فإضافة حالة تضيف صفًا إلى المنتقي دون أي جهد.

يمكن أن تحتوي الـ enums خصائص محسوبة وmethods، تمامًا مثل الـ structs. تربط symbolName كل حالة باسم أيقونة من SF Symbols، والـ switch بداخلها بلا default.

جمل switch الشاملة ميزة

لنفترض أنك أضفت case gifts الشهر القادم. ستفشل الترجمة فورًا عند symbolName مع “switch must be exhaustive”، وعند كل switch أخرى على Category في المشروع. لقد أعدّ الـ compiler قائمة مهامك. قارن ذلك بنسخة النصوص، حيث يقع التصنيف الجديد بهدوء في أي فرع default كان موجودًا، ويعرض الأيقونة الخطأ في الإنتاج.

القيم المرتبطة: حالات تحمل بيانات

القيم الخام ثابتة لكل حالة. لكن بعض البيانات تختلف من instance إلى آخر. يسجّل Pocket Budget طريقة دفع المصروف، وكل طريقة تحتاج تفاصيل مختلفة:

PaymentMethod.swift
enum PaymentMethod {
    case cash
    case card(lastFour: String)
    case transfer(bank: String, reference: String?)
}

let lunch: PaymentMethod = .cash
let groceries: PaymentMethod = .card(lastFour: "4242")
let rent: PaymentMethod = .transfer(bank: "Monzo", reference: "RENT-10")

لكل حالة شكلها الخاص. الدفع النقدي لا يحمل شيئًا، والدفع بالبطاقة يحمل أربعة أرقام، والتحويل يحمل اسم البنك ومرجعًا اختياريًا. لو استخدمت struct لاحتجت إلى ثلاث خصائص optional وتعليق يشرح أيّها يُملأ ومتى. أما الـ enum فيجعل التركيبات غير الصالحة، مثل دفع نقدي معه اسم بنك، مستحيلة البناء أصلًا.

يتفرّع الـ enum المسمّى PaymentMethod إلى ثلاث حالات: cash بلا بيانات، وcard تحمل lastFour، وtransfer تحمل bank ومرجعًا اختياريًا reference. PaymentMethod .cash بلا بيانات .card lastFour .transfer bank, reference?
enum واحد وثلاث حالات بأشكال مختلفة

استخراج البيانات من جديد

تقرأ القيم المرتبطة عبر مطابقة الأنماط (pattern matching) داخل switch:

Swift
func label(for method: PaymentMethod) -> String {
    switch method {
    case .cash:
        return "Cash"
    case .card(let lastFour):
        return "Card ending \(lastFour)"
    case .transfer(let bank, let reference?):
        return "\(bank) transfer, ref \(reference)"
    case .transfer(let bank, nil):
        return "\(bank) transfer"
    }
}

label(for: groceries)   // "Card ending 4242"

تربط let lastFour القيمة المرتبطة بثابت جديد داخل تلك الحالة. وحالتا .transfer تُظهران إلى أي مدى تصل الأنماط: let reference? لا تطابق إلا حين يكون للمرجع الاختياري قيمة، وتفكّه، وnil تطابق حين لا تكون له قيمة. وما زال الـ compiler يتحقّق من أن الحالتين معًا تغطّيان كل الاحتمالات.

حين تهمّك حالة واحدة فقط، تكون if case أقصر من switch كاملة:

Swift
if case .card(let lastFour) = groceries {
    print("Remind me to check card \(lastFour)")
}

enums بدل فوضى القيم المنطقية

الـ enums هي الأداة الصحيحة كلما كانت القيمة «واحدة بالضبط من هذه». المثال الكلاسيكي هو تحميل البيانات. المسوّدة الأولى تبدو عادةً كثلاث خصائص منفصلة: isLoading وrates وerrorMessage. لا شيء يمنع أن تُضبط الثلاث معًا، فتضطر كل شاشة إلى تخمين أيّها يفوز. enum واحد يُلغي التخمين:

Swift
enum LoadState {
    case idle
    case loading
    case loaded([String: Decimal])
    case failed(String)
}

صارت الأسعار موجودة فقط في الحالة .loaded، والرسالة فقط في .failed، وتعالج switch واحدة في الـ view كل حالة مرة واحدة. ستبني هذا بالضبط حين يبدأ Pocket Budget بجلب أسعار الصرف. استخدم enum كلما وجدت نفسك تقارن نصوصًا أو تتلاعب بقيم منطقية لا يجب أن تكون صحيحة معًا أبدًا.

صارت أنواعك الآن قادرة على وصف بيانات Pocket Budget بدقة. بعد ذلك ستمنحها قدرات مشتركة عبر الـ protocols، مثل «قابل للمقارنة» أو «له معرّف ثابت»، وتضيف سلوكًا إلى أنواع موجودة عبر الـ extensions.

أهم النقاط

  • الـ enum يسرد كل القيم الصالحة لنوع ما، فيتحوّل خطأ إملائي مثل “Fod” إلى خطأ ترجمة بدل أن يكون خطأً صامتًا.
  • القيم الخام (enum Category: String) تعطي كل حالة نصًا أو رقمًا مخزّنًا للحفظ وفكّ الترميز.
  • القيم المرتبطة تُرفق بيانات مختلفة بحالات مختلفة، مثل آخر أربعة أرقام في .card.
  • اكتب جمل switch على الـ enums بلا default، فإضافة حالة جديدة تجعل الـ compiler يسرد كل switch يجب أن تحدّثها.

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

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

لديك enum Category: String { case food, transport }. ماذا تعيد Category(rawValue: "Food")؟

السؤال 2 من 3

أضفت case gifts إلى Category. كل جملة switch على Category تستخدم حالات صريحة دون default. ماذا يحدث في الترجمة التالية؟

السؤال 3 من 3

أيّ تعريف يصف «مدفوع بالبطاقة، ونعرف آخر أربعة أرقام منها»؟

تدريب

املأ الفراغات

صمّم التصنيفات وطرق الدفع

أكمل الـ enums الاثنين في Pocket Budget. يجب أن يوفّر Category قائمة بكل حالاته حتى يعرضها المنتقي، ويجب أن تقرأ label(for:) آخر أربعة أرقام من الدفع بالبطاقة.

enum Category: String, { case food, transport, fun } print(Category..count) // 3 enum PaymentMethod { case cash case card(lastFour: String) } func label(for method: PaymentMethod) -> String { switch method { case .cash: return "Cash" case .card( lastFour): return "Card ending \(lastFour)" } }