‏@FetchRequest في SwiftUI — ما هو، جلب Core Data والآلية

المؤلف: IT Sectr نُشر: 2026-06-25 وقت القراءة: 8 دق

‏@FetchRequest في SwiftUI — property wrapper لجلب البيانات تلقائياً من Core Data مع تحديث تفاعلي لواجهة المستخدم. يستقبل wrapper كائن NSFetchRequest مع مسندات التصفية والترتيب، والنتيجة — مصفوفة من NSManagedObject — يتم تحديثها تلقائياً عند أي تغييرات في سياق Core Data. وفقاً لوثائق Apple Developer (2025)، @FetchRequest يتطلب managedObjectContext في بيئة SwiftUI ويدعم المسندات الديناميكية، مما يوفر اتصالاً تفاعلياً بين Core Data وواجهة المستخدم.

النقاط الرئيسية

  • @FetchRequest — property wrapper للجلب التفاعلي لـ Core Data في SwiftUI
  • تحديث تلقائي لواجهة المستخدم عند إضافة أو حذف أو تعديل السجلات في Core Data
  • المسندات والترتيب يتم تعيينها في المُهيئ أو عبر معاملات property wrapper
  • managedObjectContext يجب أن يكون متاحاً عبر @Environment في تسلسل View
  • المسندات الديناميكية تسمح بتغيير شروط التصفية أثناء التشغيل

ما هو @FetchRequest في SwiftUI؟

@FetchRequest هو property wrapper في SwiftUI يقوم بإنشاء وإدارة FetchRequest من Core Data. عند التهيئة، يستقبل wrapper نوع الكيان، مصفوفة NSSortDescriptor للترتيب، و NSPredicate اختياري للتصفية. يتم تخزين النتيجة كمصفوفة من كائنات نوع NSManagedObject المحدد ويتم تحديثها تلقائياً عند حدوث تغييرات في طبقة الثبات.

الميزة الرئيسية لـ @FetchRequest هي التفاعلية. عندما تتغير البيانات في Core Data (save, insert, delete, update)، يقوم SwiftUI تلقائياً بإجراء جلب جديد وإعادة رسم جميع Views التي تستخدم هذا الفيتش ريكويست. لا يحتاج المطور إلى استدعاء refresh يدوياً أو الاشتراك في NSManagedObjectContextDidSaveNotification أو إعادة تحميل البيانات. SwiftUI يدير دورة المزامنة بأكملها.

لكي يعمل @FetchRequest، يلزم NSManagedObjectContext متاح في بيئة SwiftUI عبر @Environment(\.managedObjectContext). يتم تمرير السياق عادةً من جذر التطبيق، حيث يتم إنشاء NSPersistentContainer. النمط القياسي هو استخدام ViewModel أو struct App لتكوين مكدس Core Data وتمرير السياق عبر معدل .environment().

الترتيب ومسندات FetchRequest

@FetchRequest يقبل عدة معاملات لتكوين الجلب. SortDescriptors — مصفوفة من NSSortDescriptor تحدد ترتيب السجلات. Predicate — NSPredicate للتصفية، مثلاً جلب المهام ذات الحالة «مكتمل» فقط. Animation — حركة لتحديث القائمة عند تغير البيانات.

تستخدم مسندات Core Data تنسيق تعبيرات شبيهة بـ SQL: NSPredicate(format: «status == %@», «completed»). يمكن دمج شروط متعددة عبر AND و OR و NOT. SortDescriptors تحدد الترتيب: NSSortDescriptor(keyPath: \Task.dueDate, ascending: true). للترتيب حسب عدة حقول، يتم تمرير مصفوفة من الواصفات — الأول يُطبق كأساسي، والباقي كثانوي.

swift
struct TaskListView: View {
    @Environment(\.managedObjectContext) var viewContext

    @FetchRequest(
        sortDescriptors: [
            NSSortDescriptor(keyPath: \.Task.dueDate, ascending: true),
            NSSortDescriptor(keyPath: \.Task.priority, ascending: false)
        ],
        predicate: NSPredicate(format: "isCompleted == NO"),
        animation: .default
    )
    var tasks: FetchedResults<Task>

    var body: some View {
        List(tasks) { task in
            Text(task.title ?? "")
        }
    }
}

في المثال، @FetchRequest يجلب جميع المهام غير المكتملة (isCompleted == NO)، مرتبة أولاً حسب التاريخ (تصاعدي)، ثم حسب الأولوية (تنازلي). FetchedResults<Task> هي مجموعة محدثة تلقائياً تتصرف كمصفوفة مع ForEach و List. تضمن الحركة .default ظهوراً/اختفاءً سلساً للسجلات عند التغييرات.

@FetchRequest مع List والبيانات الديناميكية

@FetchRequest يتناسب تماماً مع List و ForEach لعرض قوائم Core Data الديناميكية. نتيجة الجلب (FetchedResults) تتوافق مع بروتوكول RandomAccessCollection، لذا يمكن استخدامها مباشرة في List و ForEach. عند إضافة أو حذف أو تعديل السجلات، يتم إعادة رسم القائمة تلقائياً مع حركة.

لحذف السجلات، يتم استخدام طريقة السياق delete. للإضافة، يتم إنشاء NSManagedObject جديد واستدعاء save. تتم جميع التغييرات عبر viewContext، الذي يضمن مع @FetchRequest اتساق البيانات. للتحرير، يتم استخدام @ObservedObject أو @Bindable لكائن Core Data محدد.

swift
struct TaskListWithActions: View {
    @Environment(\.managedObjectContext) var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \.Task.createdAt,
                                               ascending: false)]
    )
    var tasks: FetchedResults<Task>

    var body: some View {
        List {
            ForEach(tasks) { task in
                HStack {
                    Text(task.title ?? "")
                    Spacer()
                    Image(systemName: task.isCompleted
                               ? "checkmark.circle.fill"
                               : "circle")
                }
                .onTapGesture { toggleTask(task) }
            }
            .onDelete(perform: deleteTasks)
        }
        .toolbar {
            Button(systemImage: "plus") { addTask() }
        }
    }

    private func addTask() {
        let newTask = Task(context: viewContext)
        newTask.title = "New task \(tasks.count + 1)"
        newTask.createdAt = Date()
        try? viewContext.save()
    }

    private func toggleTask(_ task: Task) {
        task.isCompleted.toggle()
        try? viewContext.save()
    }

    private func deleteTasks(at offsets: IndexSet) {
        for index in offsets {
            viewContext.delete(tasks[index])
        }
        try? viewContext.save()
    }
}

هذا المثال يوضح CRUD كامل مع @FetchRequest. الإضافة: إنشاء Task عبر السياق و save(). الحذف: delete() على السياق و save(). التحديث: تبديل خاصية و save(). بعد كل save()، @FetchRequest يقوم تلقائياً بإعادة جلب البيانات، ويتم تحديث List بحركة. توفر المعدّلات onDelete و onTapGesture تفاعلات iOS القياسية.

أمثلة كود @FetchRequest

لننظر إلى مثال كامل لتطبيق يوميات مع @FetchRequest لجلب السجلات. يستخدم نموذج البحث مسنداً ديناميكياً: عند إدخال النص، يقوم FetchRequest تلقائياً بتحديث النتائج، وتصفية السجلات حسب العنوان والمحتوى. الترتيب حسب تاريخ الإنشاء، من الأحدث إلى الأقدم.

swift
struct JournalEntryView: View {
    @Environment(\.managedObjectContext) var viewContext
    @State private var searchText = ""

    private var searchPredicate: NSPredicate? {
        guard !searchText.isEmpty else { return nil }
        return NSPredicate(format: "title CONTAINS[c] %@", searchText)
    }

    @FetchRequest var entries: FetchedResults<JournalEntry>

    init(searchText: String) {
        let sort = [NSSortDescriptor(keyPath: \.JournalEntry.date,
                                           ascending: false)]
        let pred: NSPredicate? = searchText.isEmpty
            ? nil
            : NSPredicate(format: "title CONTAINS[c] %@ OR content CONTAINS[c] %@",
                          searchText, searchText)
        self._entries = FetchRequest(
            sortDescriptors: sort,
            predicate: pred,
            animation: .default
        )
    }

    var body: some View {
        List(entries) { entry in
            VStack(alignment: .leading) {
                Text(entry.title ?? "").font(.headline)
                Text(entry.content ?? "").font(.subheadline)
                    .lineLimit(2)
            }
        }
        .searchable(text: $searchText)
    }
}

يوميات مع بحث: عند إدخال النص في حقل searchable يتم إنشاء NSPredicate مع CONTAINS[c] (بحث غير حساس لحالة الأحرف) على حقلي title و content. Init يستقبل searchText وينشئ FetchRequest بالمسند المقابل. يقوم معدّل searchable بتحديث searchText تلقائياً، ويتفاعل @FetchRequest مع تغيير المسند ويعيد جلب البيانات.

لسيناريوهات أكثر تعقيداً مع مرشحات متعددة، قم بدمج NSCompoundPredicate مع منطق AND/OR. @FetchRequest يدعم مسندات بأي تعقيد، بما في ذلك الاستعلامات الفرعية المتداخلة (SUBQUERY) للتصفية حسب relationship. لكن ضع في اعتبارك: المسندات المعقدة تؤثر على الأداء — لمجموعات البيانات الكبيرة (أكثر من 10,000 سجل)، استخدم فهرسة Core Data وحدد الجلب عبر fetchLimit.

المسندات الديناميكية في وقت التشغيل

@FetchRequest يدعم المسندات الديناميكية عبر معامل predicate، الذي يمكن تغييره في وقت التشغيل. للقيام بذلك، يتم إنشاء @FetchRequest بمسند متغير وتمريره عبر Binding. عندما يغير المستخدم المرشح — مثلاً يختار فئة مهام — يتم تحديث المسند ويعاد تشغيل الجلب تلقائياً.

يتم تنفيذ المسندات الديناميكية عبر خاصية @State أو @Published، التي تمرر كـ Binding إلى مهيئ @FetchRequest. يتتبع SwiftUI التغييرات في هذا Binding ويعيد إنشاء NSFetchRequest بالمسند الجديد، محدثاً القائمة تلقائياً.

الأسئلة الشائعة

كيف تمرر managedObjectContext إلى @FetchRequest؟

أضف @Environment(\.managedObjectContext) var viewContext في View جذرية ومررها عبر معدّل .environment(\.managedObjectContext, context). جميع Views التابعة التي تستخدم @FetchRequest ستستقبل السياق تلقائياً من البيئة.

هل يمكن تحديث @FetchRequest دون إعادة تحميل View؟

نعم، قم بتعديل سياق Core Data: أضف أو احذف أو حدث كائن واستدع save(). @FetchRequest سيقوم تلقائياً بإجراء جلب جديد وتحديث واجهة المستخدم. لتحديث قسري دون save()، استخدم viewContext.refreshAllObjects().

ما الفرق بين @FetchRequest و @SectionedFetchRequest؟

@FetchRequest يُرجع مصفوفة مسطحة من الكائنات. @SectionedFetchRequest يجمع النتائج حسب key path محدد إلى أقسام، مشابهاً UITableView مع sectionNameKeyPath. استخدم @SectionedFetchRequest للقوائم المجمعة بعناوين أقسام.

كيف تستخدم @FetchRequest مع Relationships؟

للتصفية حسب relationship، استخدم تدوين النقطة: NSPredicate(format: «category.name == %@», «Work»). @FetchRequest يقوم تلقائياً بإجراء JOIN في SQLite. للوصول إلى relationship عبر كائن محدد، استخدم خصائص NSManagedObject المعتادة.

هل يدعم @FetchRequest الترقيم (pagination)؟

@FetchRequest لا يدعم الترقيم مباشرة، حيث إنه مصمم لعرض جميع السجلات مع تحديث تفاعلي. للترقيم، استخدم NSFetchRequest مع fetchLimit/fetchOffset وقم بتحميل البيانات يدوياً أثناء التمرير في القائمة.

الملخص

  • @FetchRequest — property wrapper للجلب التفاعلي التلقائي لـ Core Data في SwiftUI
  • الترتيب يُحدد بواسطة مصفوفة NSSortDescriptor، التصفية بواسطة NSPredicate
  • تحديث تلقائي لواجهة المستخدم عند أي تغييرات في البيانات بسياق Core Data
  • managedObjectContext يُمرر عبر @Environment في تسلسل Views
  • المسندات الديناميكية تُنفذ عبر init مخصص مع معامل المسند
  • عمليات CRUD تُنفذ عبر viewContext مع مزامنة تلقائية للقائمة
  • استخدم @FetchRequest لعرض Core Data، @SectionedFetchRequest للقوائم المجمعة

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا