.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 { } هو معدِّل 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 بذلك تلقائياً، مما يقلل من الكود المتكرر ويزيل خطر نسيان إلغاء مهمة.
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
}
}
}
}
العديد من المطورين معتادون على تحميل البيانات في .onAppear، ولكن مع ظهور async/await و .task، أصبح هذا النهج قديماً. .onAppear ينفذ الكود بشكل متزامن — للعمليات غير المتزامنة داخل .onAppear، تحتاج إلى لف الاستدعاء في Task { } والاحتفاظ يدوياً بمرجع للإلغاء المحتمل. .task يفعل ذلك تلقائياً.
| الخاصية | .task { } | .onAppear |
|---|---|---|
| سياق async | async/await مدمج | يتطلب غلاف Task { } |
| الإلغاء التلقائي | نعم، عند اختفاء View | لا، يجب تنفيذه يدوياً |
| Structured Concurrency | يدعم | لا يدعم |
| إعادة التشغيل | فقط عند تغيير id | في كل مرة تظهر فيها View |
| توصية Apple | النهج المفضل | للعمليات المتزامنة |
.onAppear لا يزال مفيداً للعمليات المتزامنة — على سبيل المثال، التسجيل أو الإعداد الأولي للـ UI. ولكن لتحميل البيانات غير المتزامن، طلبات الشبكة، قواعد البيانات أو نظام الملفات، استخدم .task. إنه أكثر أماناً ونظافة من الناحية المعمارية.
// ❌ نهج قديم: 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:) معلمة إضافية — معرف. عندما تتغير قيمة المعرف، يلغي SwiftUI المهمة الحالية ويبدأ مهمة جديدة بالمعرف الجديد. هذا مثالي للشاشات حيث تعتمد البيانات على معلمة محددة — على سبيل المثال، قائمة المقالات حسب الفئة أو تفاصيل المنتج حسب المعرف.
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 يدوياً.
على الرغم من أن .task يلغي المهمة تلقائياً عند اختفاء View، يجب على العملية غير المتزامنة نفسها التحقق بشكل تعاوني من الإلغاء. يستخدم Swift نموذج الإلغاء التعاوني — Task.cancel() لا يوقف التنفيذ قسراً، بل يضع فقط علامة isCancelled. يجب على الكود داخل المهمة التحقق من هذه العلامة بشكل دوري.
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 مناسب للعديد من السيناريوهات: من تحميل JSON البسيط إلى العمليات المتوازية المعقدة مع TaskGroup. دعنا نلقي نظرة على ثلاثة حالات استخدام نموذجية.
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
}
}
}
}
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
}
}
}
الخطأ الأكثر شيوعاً هو تغيير خصائص UI داخل .task دون التبديل إلى الخيط الرئيسي. على الرغم من أن SwiftUI يعيد التحديثات تلقائياً إلى الخيط الرئيسي عند تعديل @State في سياق async، فإن التلاعب المباشر بعناصر UIKit داخل .task قد يسبب تعطلاً.
// ❌ خطأ: بدون معالجة أخطاء
.task {
let data = await fetchData() // يتعطل عند حدوث خطأ!
items = data
}
// ✅ صحيح: do/catch
.task {
do {
items = await fetchData()
} catch {
errorMessage = error.localizedDescription
}
}
الأسئلة الشائعة
.task يلغي المهمة تلقائياً عند اختفاء View ويدعم Structured Concurrency. Task { } في .onAppear يتطلب الاحتفاظ يدوياً بمرجع للمهمة واستدعاء cancel() في .onDisappear. .task أيضاً أسهل في القراءة — فهو يشير صراحةً إلى أن تحميل البيانات هو جزء من دورة حياة View.
نعم، للاشتراك في AsyncSequence أو AsyncStream، استخدم for await value in publisher.values داخل .task. هذا يعمل مع كل من async/await و Combine عبر امتداد Publisher.values. عند اختفاء View، سينتهي التكرار تلقائياً وسيتم إلغاء الاشتراك.
نعم، إذا كانت View داخل TabView يتم إعادة إنشائها عند كل تبديل. بدءاً من iOS 18، يمكن لـ TabView الاحتفاظ بـ Views في الذاكرة — في هذه الحالة لا يتم إعادة تشغيل .task. استخدم .task(id:) مع معرف التبويب إذا كنت بحاجة لإعادة تحميل البيانات عند كل تبديل تبويب.
سيقوم SwiftUI بإلغاء المهمة عند اختفاء View. إذا كان طلب URLSession داخل المهمة، فسيتم إلغاؤه أيضاً. إذا كانت المهمة لا تدعم الإلغاء التعاوني (على سبيل المثال، لا تتحقق من isCancelled)، فستستمر في التنفيذ، لكن نتيجتها لن تُطبق على الحالة لأن View لم تعد موجودة.
نعم، يمكنك إضافة عدة معدِّلات .task على View واحدة. كل منها ينشئ مهمة مستقلة. هذا مفيد لفصل مصادر البيانات المختلفة: .task لتحميل الملف الشخصي، وآخر للاشتراك في WebSocket، وثالث لمراقبة الموقع الجغرافي.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.