.task { } — ما هو، معدِّل async وتحميل البيانات في View

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

.task { } هو معدِّل في SwiftUI تم تقديمه في iOS 15، يقوم بتشغيل عملية غير متزامنة عند ظهور View وإلغائها تلقائياً عند اختفائها. على عكس .onAppear الذي ينفذ كوداً متزامناً دون إمكانية الإلغاء، يعمل .task مع سياق async/await ويراعي دورة حياة View: عند اختفاء View، يستدعي SwiftUI الدالة cancel() على Task الذي تم إنشاؤه. هذا يمنع تسرب الذاكرة وتنفيذ العمليات بعد أن لم تعد View بحاجة للتحديث. وفقاً لجلسة Apple WWDC Session 10132 — Meet async/await in SwiftUI (2024)، فإن .task هو الطريقة المفضلة لتحميل البيانات في SwiftUI لأنه يعمل بأمان مع Structured Concurrency ويدير تلقائياً عمر العملية غير المتزامنة.

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

  • .task { } — معدِّل SwiftUI لتحميل البيانات غير المتزامن عند ظهور View، متاح منذ iOS 15.
  • الإلغاء التلقائي — عند اختفاء View، يلغي SwiftUI Task، مما يمنع تسرب الذاكرة.
  • سياق async/await — داخل .task، استدعاءات async متاحة دون الحاجة إلى DispatchQueue أو Combine.
  • .task(id:) — إصدار مع معرف يعيد تشغيل المهمة عند تغيير القيمة المحددة.
  • Structured Concurrency — .task يدعم Structured Concurrency وTaskGroup للعمليات المتوازية.

ما هو .task { } في SwiftUI

.task { } هو معدِّل View يقوم بإنشاء Task في سياق async عند ظهور View على الشاشة. يقوم SwiftUI بتنفيذ الإغلاق المُمرر في خلفية الخيط الرئيسي، تاركاً الخيط الرئيسي حراً لعمليات UI. عندما تختفي View، يلغي SwiftUI تلقائياً Task عبر آلية Structured Concurrency — وهذا يضمن أن العملية غير المتزامنة لن تستمر في التنفيذ بعد أن لم تعد نتيجتها مطلوبة.

وفقاً لـ Apple — Swift Programming Language (2025)، يستخدم .task مفهوم Structured Concurrency الذي تم تقديمه في Swift 5.5. كل .task ينشئ مهمة فرعية ضمن مهمة View الأصلية. إذا تم إلغاء المهمة الأصلية (اختفاء View)، يتم إلغاء جميع المهام الفرعية تلقائياً. هذا يبسط بشكل جذري إدارة دورة حياة العمليات غير المتزامنة مقارنة بالتخزين اليدوي لمراجع DispatchWorkItem أو AnyCancellable.

على عكس النهج التقليدي مع @State + الاستدعاء اليدوي في .onAppear، لا يتطلب .task تخزين مرجع إلى Task للإلغاء لاحقاً. يقوم SwiftUI بذلك تلقائياً، مما يقلل من الكود المتكرر ويزيل خطر نسيان إلغاء مهمة.

swift
struct ArticlesView: View {
    @State var articles: [Article] = []
    @State var error: Error?
    
    var body: some View {
        List(articles) { article in
            Text(article.title)
        }
        .task {
            do {
                articles = await APIClient().fetchArticles()
            } catch {
                self.error = error
            }
        }
    }
}

.task مقابل .onAppear: الفروق الرئيسية

العديد من المطورين معتادون على تحميل البيانات في .onAppear، ولكن مع ظهور async/await و .task، أصبح هذا النهج قديماً. .onAppear ينفذ الكود بشكل متزامن — للعمليات غير المتزامنة داخل .onAppear، تحتاج إلى لف الاستدعاء في Task { } والاحتفاظ يدوياً بمرجع للإلغاء المحتمل. .task يفعل ذلك تلقائياً.

الخاصية.task { }.onAppear
سياق asyncasync/await مدمجيتطلب غلاف Task { }
الإلغاء التلقائينعم، عند اختفاء Viewلا، يجب تنفيذه يدوياً
Structured Concurrencyيدعملا يدعم
إعادة التشغيلفقط عند تغيير idفي كل مرة تظهر فيها View
توصية Appleالنهج المفضلللعمليات المتزامنة

.onAppear لا يزال مفيداً للعمليات المتزامنة — على سبيل المثال، التسجيل أو الإعداد الأولي للـ UI. ولكن لتحميل البيانات غير المتزامن، طلبات الشبكة، قواعد البيانات أو نظام الملفات، استخدم .task. إنه أكثر أماناً ونظافة من الناحية المعمارية.

swift
// ❌ نهج قديم: Task في .onAppear بدون إلغاء
var loadTask: Task<Void, Never>?
func body() { var body: some View { Text("") }
    .onAppear {
        loadTask = Task { await loadData() }
    }
    .onDisappear { loadTask?.cancel() }

// ✅ نهج حديث: .task يدير الإلغاء
func body() { var body: some View { Text("") }
    .task { await loadData() }

.task(id:) — إعادة التشغيل عند تغيير البيانات

يقبل المعدِّل .task(id:) معلمة إضافية — معرف. عندما تتغير قيمة المعرف، يلغي SwiftUI المهمة الحالية ويبدأ مهمة جديدة بالمعرف الجديد. هذا مثالي للشاشات حيث تعتمد البيانات على معلمة محددة — على سبيل المثال، قائمة المقالات حسب الفئة أو تفاصيل المنتج حسب المعرف.

swift
struct CategoryView: View {
    let categoryId: Int
    @State var items: [Item] = []
    
    var body: some View {
        List(items) { item in
            Text(item.name)
        }
        .task(id: categoryId) {
            await loadItems(for: categoryId)
        }
    }
    
    func loadItems(for id: Int) async {
        do {
            items = await APIClient().fetchItems(categoryId: id)
        } catch {
            // معالجة الخطأ
        }
    }
}

عندما يتغير categoryId، يلغي SwiftUI الطلب السابق ويبدأ طلباً جديداً. هذا مهم بشكل خاص عند التبديل السريع بين الفئات — الطلبات القديمة لن تتنافس مع الجديدة على تحديث الحالة. بدون .task(id:)، سيكون عليك تتبع التغييرات يدوياً عبر .onChange وإدارة Task يدوياً.

إلغاء المهام والتحقق من isCancelled

على الرغم من أن .task يلغي المهمة تلقائياً عند اختفاء View، يجب على العملية غير المتزامنة نفسها التحقق بشكل تعاوني من الإلغاء. يستخدم Swift نموذج الإلغاء التعاوني — Task.cancel() لا يوقف التنفيذ قسراً، بل يضع فقط علامة isCancelled. يجب على الكود داخل المهمة التحقق من هذه العلامة بشكل دوري.

swift
struct LoadingView: View {
    @State var progress: Double = 0
    
    var body: some View {
        ProgressView(value: progress)
            .task {
                for i in 0..<100 {
                    // التحقق من الإلغاء
                    try Task.checkCancellation()
                    
                    await Task.sleep(nanoseconds: 50_000_000)
                    progress = Double(i + 1) / 100.0
                }
            }
    }
}

Task.checkCancellation() يطرح CancellationError إذا تم إلغاء المهمة. هذه هي أبسط طريقة للتحقق — تعمل في أي سياق async. البديل هو التحقق من Task.isCancelled يدوياً قبل العمليات المكلفة. بالنسبة لـ URLSession، يتم إلغاء طلبات الشبكة تلقائياً عند إلغاء المهمة، حيث أن URLSession يدعم Structured Concurrency بشكل أصلي.

أمثلة عملية لـ .task

.task مناسب للعديد من السيناريوهات: من تحميل JSON البسيط إلى العمليات المتوازية المعقدة مع TaskGroup. دعنا نلقي نظرة على ثلاثة حالات استخدام نموذجية.

التحميل مع معالجة الأخطاء

swift
struct ProfileView: View {
    @State var profile: Profile?
    @State var isLoading = true
    
    var body: some View {
        Group {
            if isLoading {
                ProgressView()
            } else if let profile {
                Text(profile.name)
            } else {
                Text("فشل التحميل")
            }
        }
        .task {
            defer { isLoading = false }
            do {
                profile = await APIClient().fetchProfile()
            } catch {
                // الملف الشخصي يظل nil
            }
        }
    }
}

التحميل المتوازي مع TaskGroup

swift
struct DashboardView: View {
    @State var stats: DashboardStats?
    
    var body: some View {
        Text("لوحة التحكم")
            .task {
                stats = await Task {
                    await withThrowingTaskGroup { group in
                        group.addTask { await API().fetchUsers() }
                        group.addTask { await API().fetchOrders() }
                        group.addTask { await API().fetchRevenue() }
                        
                        return DashboardStats(
                            users: try await group.next(),
                            orders: try await group.next(),
                            revenue: try await group.next()
                        )
                    }
                }.value
            }
    }
}

الأخطاء الشائعة مع .task

الخطأ الأكثر شيوعاً هو تغيير خصائص UI داخل .task دون التبديل إلى الخيط الرئيسي. على الرغم من أن SwiftUI يعيد التحديثات تلقائياً إلى الخيط الرئيسي عند تعديل @State في سياق async، فإن التلاعب المباشر بعناصر UIKit داخل .task قد يسبب تعطلاً.

  • نسيان try/catch — .task لا يعالج الأخطاء تلقائياً. جميع الدوال التي تطرح أخطاء يجب أن تكون مغلفة في do/catch، وإلا سيتعطل التطبيق.
  • سباق البيانات — إذا تم تشغيل عدة .task(id:) بمعرفات مختلفة وقامت بتحديث نفس الحالة، يمكن أن تحدث سباقات بيانات. استخدم خصائص منفصلة لمصادر البيانات المختلفة.
  • العمليات المتزامنة الطويلة — .task لا يجعل الكود المتزامن غير متزامن. إذا كان هناك عمل متزامن ثقيل داخل .task، لفه في Task.detached أو انقله إلى دالة async منفصلة.
  • تجاهل CancellationError — عند التحقق من Task.checkCancellation()، يجب نقل CancellationError إلى الأعلى، وليس كبته. كبت الإلغاء يمكن أن يؤدي إلى تسرب الذاكرة.
swift
// ❌ خطأ: بدون معالجة أخطاء
.task {
    let data = await fetchData() // يتعطل عند حدوث خطأ!
    items = data
}

// ✅ صحيح: do/catch
.task {
    do {
        items = await fetchData()
    } catch {
        errorMessage = error.localizedDescription
    }
}

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

ما الفرق بين .task واستخدام Task { } في .onAppear؟

.task يلغي المهمة تلقائياً عند اختفاء View ويدعم Structured Concurrency. Task { } في .onAppear يتطلب الاحتفاظ يدوياً بمرجع للمهمة واستدعاء cancel() في .onDisappear. .task أيضاً أسهل في القراءة — فهو يشير صراحةً إلى أن تحميل البيانات هو جزء من دورة حياة View.

هل يمكن استخدام .task للاشتراك في publisher؟

نعم، للاشتراك في AsyncSequence أو AsyncStream، استخدم for await value in publisher.values داخل .task. هذا يعمل مع كل من async/await و Combine عبر امتداد Publisher.values. عند اختفاء View، سينتهي التكرار تلقائياً وسيتم إلغاء الاشتراك.

كيف يعمل .task مع TabView — هل يتم إعادة تشغيل المهمة عند تبديل التبويبات؟

نعم، إذا كانت View داخل TabView يتم إعادة إنشائها عند كل تبديل. بدءاً من iOS 18، يمكن لـ TabView الاحتفاظ بـ Views في الذاكرة — في هذه الحالة لا يتم إعادة تشغيل .task. استخدم .task(id:) مع معرف التبويب إذا كنت بحاجة لإعادة تحميل البيانات عند كل تبديل تبويب.

ماذا يحدث إذا اختفت View مع .task قبل اكتمال الطلب؟

سيقوم SwiftUI بإلغاء المهمة عند اختفاء View. إذا كان طلب URLSession داخل المهمة، فسيتم إلغاؤه أيضاً. إذا كانت المهمة لا تدعم الإلغاء التعاوني (على سبيل المثال، لا تتحقق من isCancelled)، فستستمر في التنفيذ، لكن نتيجتها لن تُطبق على الحالة لأن View لم تعد موجودة.

هل يمكن استخدام عدة معدِّلات .task على View واحدة؟

نعم، يمكنك إضافة عدة معدِّلات .task على View واحدة. كل منها ينشئ مهمة مستقلة. هذا مفيد لفصل مصادر البيانات المختلفة: .task لتحميل الملف الشخصي، وآخر للاشتراك في WebSocket، وثالث لمراقبة الموقع الجغرافي.

الخلاصة

  • .task { } — معدِّل SwiftUI للعمليات غير المتزامنة مع إلغاء تلقائي عند اختفاء View.
  • دعم async/await — داخل .task، سياق async كامل متاح دون الحاجة إلى غلاف Task.
  • .task(id:) — يعيد تشغيل المهمة عند تغيير المعرف، ليحل محل .onChange اليدوي.
  • الإلغاء التعاوني — استخدم Task.checkCancellation() للتحقق من الإلغاء داخل المهمة.
  • Structured Concurrency — .task يدعم TaskGroup والعمليات المتوازية مع إلغاء المهام الفرعية.
  • بديل لـ .onAppear — لتحميل البيانات غير المتزامن، استخدم .task بدلاً من نمط .onAppear + Task + .onDisappear.
  • معالجة الأخطاء — جميع استدعاءات async داخل .task يجب أن تكون مغلفة في do/catch لمنع الأعطال.

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

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

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

اقرأ أيضًا