تطوير الويب

إدارة حالة app بـ @Observable وframework Observation

4 min de lecture

📘 الدليل الرئيسي: تطوير تطبيق iOS بـ Swift وSwiftUI: بانوراما 2026. هذا الدليل يفترض إتقان أساسيات SwiftUI المُغطّاة في بناء أوّل view SwiftUI.

app غير تافه يتجاوز سريعًا نطاق شاشة واحدة. عدّة views تتقاسم إذًا حالة واحدة: قائمة مهامّ، الملف الشخصي للمستخدم، سلّة متجر، تاريخ chat. framework Observation، المُسَلَّم من Apple منذ iOS 17، يُوفّر الآلية الموصى بها لنمذجة هذه البيانات المشتركة. قطعته المركزية الـ macro @Observable التي تحلّ محلّ البروتوكول القديم ObservableObject، @Published، و@ObservedObject. يُفَصِّل هذا الدليل الهجرة والاستعمال الحديث، خطوة بخطوة، مع مشروع صغير لإدارة المهامّ.

المتطلّبات

  • Xcode 26 على macOS Tahoe.
  • هدف نشر iOS 17 كحدّ أدنى — @Observable يتطلّب framework Observation المُقَدَّم مع iOS 17.
  • أساسيات SwiftUI: @State، @Binding، modifiers.
  • الوقت المتوقّع: 60 إلى 90 دقيقة.

الخطوة 1 — فهم الفرق مع النموذج القديم

قبل iOS 17، كنّا نُعلن نموذجًا observable عبر بروتوكول ObservableObject، نُوسم كلّ خاصية مُراقَبة بـ @Published، والـ views تستهلكها عبر @ObservedObject أو @StateObject. SwiftUI كان يُعيد عرض الـ view مع كلّ تعديل لخاصية @Published واحدة، حتى لو لم تكن الـ view تستعمل هذه الخاصية.

الـ macro @Observable تُغيِّر هذه الآلية. في الترجمة، تُعيد كتابة الـ class لتتبّع كلّ وصول لكلّ خاصية من body الـ view فُرادى. النتيجة: فقط الـ view التي تقرأ فعلًا خاصية تُعاد عرضها حين تتغيّر هذه الخاصية. على app متوسّط، مكسب الأداء محسوس — إعادات عرض أقلّ، حسابات diff أقلّ، ارتدادات أقلّ في الهرمية.

بالمناسبة، الصياغة تُبَسَّط: لا @Published على كلّ خاصية، لا تمييز @ObservedObject / @StateObject، نعود إلى @State في الـ view التي تملك النموذج.

الخطوة 2 — إنشاء نموذج @Observable

النموذج class مَوسومة بـ macro @Observable. كلّ خصائصها تصير observable تلقائيًّا، إلّا تلك المَوسومة بـ @ObservationIgnored. هذا تغيير افتراضي: نختار ما لا نُراقبه، بدل ما نُراقبه.

import SwiftUI
import Observation

struct Tache: Identifiable {
    let id = UUID()
    var titre: String
    var faite: Bool = false
}

@Observable
final class ListeTaches {
    var taches: [Tache] = []
    var filtre: Filtre = .toutes

    enum Filtre {
        case toutes, enCours, terminees
    }

    var tachesAffichees: [Tache] {
        switch filtre {
        case .toutes:    return taches
        case .enCours:   return taches.filter { !${0.faite }
        case .terminees: return taches.filter { ${0.faite }
        }
    }

    func ajouter(_ titre: String) {
        guard !titre.isEmpty else { return }
        taches.append(Tache(titre: titre))
    }

    func basculer(_ id: UUID) {
        if let idx = taches.firstIndex(where: { ${0.id == id }) {
            taches[idx].faite.toggle()
        }
    }

    func supprimer(at offsets: IndexSet) {
        taches.remove(atOffsets: offsets)
    }
}

الكلمة final ليست إلزامية لكنّها موصى بها: تُشير إلى المُجَمِّع أنّ هذه الـ class لن تُورَّث، ممّا يفتح تحسينات. الخاصية المحسوبة tachesAffichees هي أيضًا مُراقَبة — SwiftUI سيُعيد حساب الـ view حين تتغيّر taches أو filtre.

الخطوة 3 — استهلاك النموذج في view

لتملك view مثيلًا لنموذج observable وتحفظه حيًّا، نستعمل @State — لا @StateObject كما في العالم القديم. هذا التغيير يُحَيِّر في أوّل تماسّ لكنّه يُبَسِّط النموذج الذهني: @State يُدير كلّ ما تملكه الـ view.

struct ListeView: View {
    @State private var modele = ListeTaches()
    @State private var nouveauTitre: String = ""

    var body: some View {
        VStack {
            HStack {
                TextField("Nouvelle tâche", text: $nouveauTitre)
                    .textFieldStyle(.roundedBorder)
                Button("Ajouter") {
                    modele.ajouter(nouveauTitre)
                    nouveauTitre = ""
                }
                .disabled(nouveauTitre.isEmpty)
            }
            .padding()

            Picker("Filtre", selection: $modele.filtre) {
                Text("Toutes").tag(ListeTaches.Filtre.toutes)
                Text("En cours").tag(ListeTaches.Filtre.enCours)
                Text("Terminées").tag(ListeTaches.Filtre.terminees)
            }
            .pickerStyle(.segmented)
            .padding(.horizontal)

            List {
                ForEach(modele.tachesAffichees) { t in
                    HStack {
                        Image(systemName: t.faite ? "checkmark.circle.fill" : "circle")
                            .foregroundStyle(t.faite ? .green : .gray)
                            .onTapGesture { modele.basculer(t.id) }
                        Text(t.titre)
                            .strikethrough(t.faite)
                    }
                }
                .onDelete(perform: modele.supprimer)
            }
        }
    }
}

الـ binding $modele.filtre يعمل مباشرة — Swift يستنبط تلقائيًّا binding على أيّ خاصية لنوع @Observable يملكه @State. لا حاجة لـ wrapper وسيط. هذا التبسيط أحد أسباب جعل الانتقال إلى @Observable يجعل الكود أكثر قراءة محسوسًا.

الخطوة 4 — مشاركة النموذج بين views عبر @Bindable

sub-view تحتاج تعديل النموذج يمكنها استلامه كمعامل عادي واستعماله عبر @Bindable. هذا الـ wrapper يُنشئ bindings نحو خصائص النموذج، مكافئ وظيفي لـ $model.property للأبّ.

struct EditeurTache: View {
    @Bindable var tache: Tache  // ⚠ لا يعمل إلّا إن كانت Tache class @Observable

    var body: some View {
        Form {
            TextField("Titre", text: $tache.titre)
            Toggle("Terminée", isOn: $tache.faite)
        }
    }
}

انتبه: في مشروعنا، Tache هي struct. لاستعمال @Bindable، يجب إمّا تحويلها إلى class @Observable، أو تمرير binding @Binding var tache: Tache من الأبّ. الخيار يتعلّق بالحاجة — لبيانات عابرة منسوخة عند التحرير، struct + binding يكفي؛ لبيانات مشتركة بين عدّة شاشات، class @Observable أكثر عملية.

الخطوة 5 — حقن النموذج في كامل الهرمية عبر @Environment

حين يجب الوصول لنفس النموذج من عدّة شاشات (مثلًا سلّة مرئية من عدّة صفحات متجر)، نحقنه في البيئة بدل تمريره يدويًّا. هذه الآلية تحلّ محلّ @EnvironmentObject القديم.

@main
struct MonAppApp: App {
    @State private var modele = ListeTaches()

    var body: some Scene {
        WindowGroup {
            ListeView()
                .environment(modele)
        }
    }
}

struct AutreEcran: View {
    @Environment(ListeTaches.self) private var modele

    var body: some View {
        Text("(modele.taches.count) tâches au total")
    }
}

الدالّة .environment(modele) تجعل المثيل متاحًا لكلّ الـ sub-views. @Environment(ListeTaches.self) يسترد المثيل؛ إن لم يُوَفَّر، app تنهار في runtime — تسوية مقبولة للاختزال. لنماذج اختيارية، أعلِن الخاصية ListeTaches?.

الخطوة 6 — هجرة مشروع قائم من ObservableObject

لمشروع legacy يستعمل ObservableObject و@Published، الهجرة تتبع مخطّطًا آليًّا.

  • استبدل class X: ObservableObject { … } بـ @Observable final class X { … } — أضف import Observation.
  • احذف كلّ @Published — تصير عديمة الفائدة.
  • في الـ views التي كانت تملك النموذج: @StateObject private var modele = X() يصير @State private var modele = X().
  • في الـ views المُستهلِكة دون امتلاك: @ObservedObject var modele: X يصير ببساطة var modele: X (أو @Bindable إن أردت bindings).
  • @EnvironmentObject var modele: X يصير @Environment(X.self) private var modele.
  • الحقن في الهرمية يمرّ من .environmentObject(modele) إلى .environment(modele).

الهجرة يمكن أن تتمّ شاشة شاشة؛ الآليّتان تتعايشان دون تداخل في نفس المشروع.

الخطوة 7 — اختبار إعادة العرض الانتقائية

للتحقّق من مكسب الأداء لـ @Observable، يمكن مقارنة عدد إعادات العرض قبل وبعد. حيلة عملية تسجيل كلّ استدعاء لـ body.

struct CompteurView: View {
    var modele: ListeTaches

    var body: some View {
        let _ = print("CompteurView re-rendue à (Date())")
        return Text("(modele.taches.count) tâches")
    }
}

السطر let _ = print(…) يستغلّ أنّ body يُرجع View؛ يمكن إدراج آثار جانبية قبل return. عند التنفيذ، نُلاحظ أنّ CompteurView لا يُعاد عرضها إلّا حين يتغيّر taches.count — لا حين نُبَدِّل الفلتر مثلًا. مع ObservableObject القديم، كلّ تعديل @Published كان يُعيد عرض الـ view.

الخطوة 8 — استعمال Observations للتفاعل على المدى الطويل

iOS 26 يُقَدِّم struct Observations تُتيح لكائن غير-View الاشتراك في تغييرات نموذج @Observable. هذه الآلية تحلّ محلّ Combine subscriptions القديمة في عدّة حالات وتبقى Swift نقي، دون اعتمادية على Combine.

import Observation

final class StatistiquesService {
    private let modele: ListeTaches

    init(modele: ListeTaches) {
        self.modele = modele
        Task { await ecouter() }
    }

    private func ecouter() async {
        let stream = Observations { [modele] in
            modele.taches.filter { ${0.faite }.count
        }
        for await termineeCount in stream {
            print("Tâches terminées : (termineeCount)")
        }
    }
}

Observations يأخذ closure تحسب قيمة تعتمد على خصائص observable؛ الـ stream يُصدر القيمة الراهنة ثم كلّ قيمة جديدة عند تغيّر تبعية. مفيد جدًّا لآثار جانبية مُستديمة (logging، تزامن، إشعارات) غير مرتبطة بـ view محدّدة. متاح على iOS 26 وأحدث فقط.

متى نختار @Observable بدل @State بسيط؟

قاعدة الإبهام في SwiftUI البدء بسيطًا. بيانات محلّية لشاشة (نصّ مُدخَل في نموذج، فهرس التبويب الراهن، علم تأكيد) تنتمي لـ @State. بمجرّد أن تتجاوز البيانات هذه الشاشة، أو تُقرأ من view أخرى لا تنحدر خطّيًّا من شاشة الأصل، يبدأ ميدان @Observable. سلّة نتصفّحها من قائمة المنتجات ومن شاشة الدفع هي النموذج الأصلي: البيانات لا تخصّ أيًّا من الـ views، بل تعيش في نموذج مشترك.

الفخّ الكلاسيكي توقّع الأمور مبكّرًا وتحويل حالة محلّية صرفة إلى نموذج observable « تحسّبًا ». هذا التضخّم مكلف: يُدخل class، تهيئة، وأحيانًا حقن عبر @Environment، حيث ثلاث خصائص @State كانت تكفي. الانضباط السليم ترقية حالة محلّية إلى نموذج observable فقط حين تبدأ view ثانية بقراءتها أو تعديلها.

أخطاء شائعة مع @Observable

الخطأ السبب الحلّ
« Cannot find type ‘@Observable’ in scope » لا استيراد لـ framework أضف import Observation في رأس الملفّ
app تنهار عند الإقلاع بـ « No observable object of type X » @Environment(X.self) دون حقن في الهرمية أضف .environment(instance) في الأبّ
الـ view لا تُحدَّث النموذج struct مُعَدَّل محلّيًّا @Observable لا يعمل إلّا على class؛ حوِّل إن لزم
« Cannot use mutating member on immutable value: ‘modele' » محاولة تعديل خاصية لـ let class إن كان modele class، let يكفي للتعديل (المرجع لا يتغيّر)
« Argument passed to call that takes no arguments » خلط بين @Bindable و@Binding @Bindable لـ class @Observable، @Binding لقيمة
هدف iOS 16 أو سابق @Observable يتطلّب iOS 17+ ارفع Deployment Target أو ابقَ على ObservableObject

أسئلة شائعة

هل يجب هجرة كامل المشروع فورًا إلى @Observable؟ لا. الهجرة يمكن أن تكون تدريجية — نموذجًا نموذجًا. الآليّتان تتعايشان. هاجِر أوّلًا النماذج الأكثر مشاركة وتلك التي تُسَبِّب إعادات عرض مفرطة بطيئة محسوسة.

هل @Observable يعمل على iOS 16؟ لا. الـ macro وframework Observation يتطلّبان iOS 17 كحدّ أدنى. لاستهداف iOS 16 أو سابق، ابقَ على ObservableObject.

الفرق بين @State و@Bindable و@Environment؟ @State يملك المثيل في الـ view. @Bindable يستقبل مثيلًا مُنشأً سلفًا ويعرض bindings. @Environment(X.self) يسترد مثيلًا مُحقَن أعلى الهرمية.

أيمكن مراقبة خاصية struct؟ لا، @Observable لا ينطبق إلّا على classes. متّسق مع الدلالة: struct تُنسَخ مع كلّ إسناد، لا توجد هويّة مشتركة للمراقبة. لمراقبة قيمة، نستعمل @State في الـ view التي تملكها.

كيف نختبر نموذج @Observable؟ نموذج @Observable class عادية؛ نُنشئها في اختبار، نستدعي دوالّها، نتحقّق من الحالة الناتجة. لا mock observable خاصّ يلزم تركيبه.

أيّ فرق مع Combine؟ Combine يبقى مفيدًا لـ pipelines تحويل البيانات (debounce، throttle، merge). لمراقبة حالة مشتركة بين views، @Observable أبسط وأعلى أداءً. الـ frameworkان يمكن تعايشهما.

الأدلّة الموصى بها بعد هذا

مصادر رسمية

  • توثيق Observation — المرجع الرسمي.
  • Migrating from ObservableObject to the Observable macro — دليل رسمي خطوة بخطوة.
  • WWDC23 — Discover Observation in SwiftUI — جلسة العرض الأوّلي.

🔝 العودة إلى الدليل الرئيسي.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité