@MainActor هو ممثل عام في لغة Swift يضمن تنفيذ الكود على الخيط الرئيسي. وفقًا لـ Apple Developer, 2024، يقوم @MainActor بأتمتة التبديل إلى الخيط الرئيسي عند العمل مع واجهة المستخدم، مما يريح المطور من الاستدعاء اليدوي لـ DispatchQueue.main.async. ظهر هذا الوسم في Swift 5.5 مع نظام async/await.
الخلاصة
@MainActor هو ممثل عام في Swift يجمع بين خصائص الممثلين وضمان التنفيذ على الخيط الرئيسي للتطبيق. إنه جزء من نظام التزامن في Swift الذي تم تقديمه في Swift 5.5 مع async/await والتزامن المنظم. يسمح الوسم للمطور بعدم التفكير في التبديل اليدوي للخيوط ويقلل من عدد أخطاء واجهة المستخدم.
الممثل في Swift هو نوع مرجعي يعزل حالته ويضمن أن خيطًا واحدًا فقط يمكنه تعديله. @MainActor هو ممثل عام خاص يكون منفذه هو الخيط الرئيسي. أي كود مُوسم بـ @MainActor يُنفذ على الخيط الرئيسي — حتى لو تم استدعاؤه من مهمة خلفية.
قبل @MainActor، كان المطورون يتحولون يدويًا إلى الخيط الرئيسي عبر DispatchQueue.main.async. كان هذا مصدرًا متكررًا للأخطاء: كان المطورون ينسون التبديل، مما يؤدي إلى أعطال بسبب تحديث واجهة المستخدم ليس على الخيط الرئيسي. @MainActor يحل هذه المشكلة على مستوى نظام الأنواع.
مصدر معظم الأخطاء في تطبيقات iOS هو عدم أمان واجهة المستخدم — تحديث الواجهة من خيط خلفي. قامت Apple بدمج @MainActor في Swift Concurrency لجعل التبديل إلى الخيط الرئيسي تلقائيًا وقابلًا للتحقق من قبل المترجم، مما يلغي فئة كاملة من أخطاء وقت التشغيل.
مبدأ العمل لـ @MainActor يعتمد على نظام تنفيذ Swift Concurrency. عندما يستدعي خيط دالة موسومة بـ @MainActor، يقوم المجدول بتعليقها على المنفذ الحالي واستئنافها على الخيط الرئيسي. يتتبع المترجم حدود الاستدعاء ويضمن الأمان.
تنفيذ @MainActor يتم بواسطة MainActor.shared — منفذ مرتبط بالخيط الرئيسي للتطبيق. عندما تكون دالة غير متزامنة موسومة بـ @MainActor، فإنها تستأنف دائمًا على هذا المنفذ، بغض النظر عن الخيط الذي بدأت عليه المهمة الأصلية.
import SwiftUI
class ViewModel: ObservableObject {
@Published var items: [String] = []
@MainActor
func loadData() async {
let result = await fetchRemoteData()
items = result // بأمان، يضمن MainActor الخيط الرئيسي
}
}
إذا كانت دالة موسومة بـ @MainActor وتستدعي دالة غير متزامنة أخرى، فإنها ترث سياق الممثل افتراضيًا. هذا يعني أن جميع الاستدعاءات المتداخلة تُنفذ أيضًا على الخيط الرئيسي، ما لم يُحدد خلاف ذلك. يتتبع المترجم هذا ويصدر خطأ عند محاولة تمرير إغلاق غير متسق.
مقارنة @MainActor و DispatchQueue.main تساعد في فهم لماذا تعتبر الآلية الجديدة أكثر أمانًا وملاءمة، على الرغم من أن كلاهما يحل نفس المهمة — تنفيذ الكود على الخيط الرئيسي.
@MainActor هو تحقق على مستوى المترجم. إذا حاولت استدعاء دالة @MainActor من سياق غير آمن، سيصدر المترجم تحذيرًا أو خطأ. DispatchQueue.main.async هو استدعاء في وقت التشغيل: سيتجمّع الكود لكنه قد يتعطل في وقت التشغيل عند محاولة تحديث واجهة المستخدم من خيط خلفي.
DispatchQueue.main.async يضيف كتلة إلى قائمة الانتظار قد يتم تنفيذها مع تأخير. @MainActor مع async/await يقوم بالتبديل المباشر للمنفذ دون إنشاء إغلاقات غير ضرورية. هذا يقلل من العبء ويجعل وقت التنفيذ أكثر قابلية للتنبؤ.
// النهج القديم
DispatchQueue.main.async {
self.updateUI()
}
// النهج الجديد مع @MainActor
@MainActor
func updateUI() {
// يُتنفذ على الخيط الرئيسي
self.label.text = "تم التحديث"
}
| المعيار | @MainActor | DispatchQueue.main |
|---|---|---|
| التحقق | مترجم | وقت التشغيل |
| الصيغة | وسم (تصريحي) | استدعاء (أمرّي) |
| العبء | منخفض (تبديل منفذ) | متوسط (إغلاق + قائمة) |
| قابلية الاختبار | عالية (يمكن استبدال MainActor.shared) | منخفضة (صعبة المحاكاة) |
في مشاريع iOS الفعلية، يُستخدم @MainActor في طبقات ViewModel وعروض SwiftUI ووحدات تحكم UIKit. يمكن تطبيق الوسم على الطرق الفردية وعلى النوع بأكمله.
بوضع علامة على فئة بـ @MainActor، تضمن أن جميع طرقها وخصائصها يمكن الوصول إليها فقط على الخيط الرئيسي. هذا مناسب بشكل خاص لعروض SwiftUI وفئات ObservableObject: ببساطة تضيف @MainActor قبل class، ويتم تحديث جميع خصائص @Published بأمان.
@MainActor
final class UserListViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
func fetchUsers() async {
isLoading = true
users = await api.getUsers()
isLoading = false
}
}
عند العمل مع كود UIKit قديم حيث كان التبديل بين الخيوط يدويًا، يمكنك استخدام MainActor.run للتبديل الصريح. هذا مناسب للانتقال التدريجي إلى Swift Concurrency دون إعادة كتابة قاعدة الكود بأكملها.
await MainActor.run {
self.tableView.reloadData()
}
على الرغم من كل مزاياها، فإن @MainActor لديه عدد من القيود التي من المهم أخذها في الاعتبار عند تصميم بنية التطبيق. فهم حدود قابلية التطبيق يساعد في تجنب الاستخدام غير الصحيح.
إذا كانت سلسلة الاستدعاءات بأكملها موسومة بـ @MainActor، فإن أي عمل ثقيل سيتم تنفيذه على الخيط الرئيسي، مما يسبب تجميد واجهة المستخدم. يُوصى بوسم طبقة واجهة المستخدم فقط بـ @MainActor، مع ترك منطق الأعمال وطلبات الشبكة في ممثلين خلفيين أو المنفذ العام.
واجهات البرمجة القديمة القائمة على الاستدعاءات (مثل URLSession بدون async/await) لا تدعم سياق الممثل. يتطلب التكامل غلافًا مع CheckedContinuation. أيضًا، @MainActor غير متوافق مع performSelector و target-action وأنماط UIKit غير المتزامنة الأخرى.
عند تصحيح تطبيقات @MainActor، يصعب إعادة إنتاج ظروف السباق لأن المترجم يمنع العديد منها في وقت البناء بدلاً من وقت التشغيل. ومع ذلك، قد يخلق هذا شعورًا زائفًا بالأمان: العمل غير الصحيح مع الكائنات القابلة للتغيير المشتركة (مثل NSCache أو المتغيرات العالمية المشتركة) لا يزال ممكنًا إذا لم تكن موسومة بـ @MainActor وتُستخدم دون مزامنة صريحة.
@MainActor يبسط بشكل كبير اختبار منطق واجهة المستخدم حيث يلغي الحاجة إلى التبديل اليدوي للخيوط في الاختبارات. ومع ذلك، هناك خصوصيات يجب مراعاتها عند كتابة اختبارات الوحدة واختبارات واجهة المستخدم.
في XCTest، تقوم بيئة الاختبار تلقائيًا بإعداد منفذ الخيط الرئيسي. عندما يتم تشغيل طريقة اختبار على الخيط الرئيسي، لا يتطلب استدعاء دوال @MainActor إعدادًا إضافيًا — يتم تنفيذها في نفس السياق. لاختبار السيناريوهات الخلفية، استخدم MainActor.run داخل Task مع أولوية ومنفذ صريحين، مع التحقق بشكل منفصل من أن الكود يعمل بشكل صحيح عند استدعائه من الخلفية.
أحد الأساليب الشائعة هو اختبار ViewModel مع @MainActor، حيث يتم التحقق من تحديث خصائص @Published بشكل صحيح بعد العمليات غير المتزامنة. بفضل توريث سياق الممثل، فإن استدعاء await داخل الاختبار يضمن التنفيذ على الخيط الرئيسي بدون ضمانات إضافية من DispatchQueue أو تبديل سياق يدوي، مما يبسط كتابة الاختبارات.
عند إعادة هيكلة كود موجود إلى Swift Concurrency، تحقق من عزل @MainActor من خلال المترجم: أي استدعاء لطرق متزامنة بدون @MainActor من سياق @MainActor يُوسم كخطأ. تُستخدم هذه الخاصية للترحيل التدريجي لمشروع إلى async/await: تضع علامة على طبقة ViewModel كـ @MainActor، ويقوم المترجم بتسليط الضوء على جميع الاستدعاءات غير الآمنة التي يجب نقلها إلى ممثلين خلفيين.
عند إنشاء محاكيات لتبعيات @MainActor، استخدم بروتوكولات بطرق غير متزامنة تعلن عن دوال غير متزامنة مع أنواع الإرجاع. هذا يسمح باستبدال خدمات الشبكة وقواعد البيانات والتبعيات الخارجية الأخرى دون كسر عزل الممثل. يتحقق المترجم من أن المحاكي ينفذ جميع متطلبات العزل، مما يمنع الوصول العرضي إلى كود @MainActor من خيوط اختبار خلفية.
عند اختبار كود @MainActor بشكل متزامن، استخدم XCTestExpectation لانتظار اكتمال العمليات غير المتزامنة. قم بتعيين التوقع في الاختبار ونفذ fulfillment داخل إغلاق يتم تنفيذه على الخيط الرئيسي. إذا توقف الاختبار إلى أجل غير مسمى — فمن المحتمل أن الاستدعاء على الخيط الرئيسي لا يحدث، وتحتاج إلى التحقق من عزل الممثل. لتصحيح سياق التنفيذ، من المفيد إضافة تحقق Thread.isMainThread داخل كود الاختبار.
الأسئلة الشائعة
لا، يكفي وضع علامة فقط على الطرق التي تحدث واجهة المستخدم. ومع ذلك، إذا كانت الفئة تحتوي على عدة طرق من هذا القبيل، فمن الأسهل إضافة @MainActor إلى الفئة بأكملها. هذا يضمن أن جميع أعضائها يُنفذون على الخيط الرئيسي ويبسط صيانة الكود.
@MainActor هو مثيل محدد لممثل عام مرتبط بالخيط الرئيسي. @globalActor هو بروتوكول لإنشاء ممثلين عامين خاصين بك. على سبيل المثال، يمكنك إنشاء @BackgroundActor لتنفيذ كود على خيط خلفي إذا كانت بنية المشروع تتطلب ذلك.
نعم، الدوال المتزامنة مع @MainActor تُنفذ أيضًا على الخيط الرئيسي. ومع ذلك، تتجلى القيمة الرئيسية لـ @MainActor مع async/await، عندما تستأنف دالة غير متزامنة تلقائيًا على الخيط الرئيسي دون تبديل يدوي عبر DispatchQueue.main.
Task.cancel() يعمل مع مهام @MainActor بنفس الطريقة كما هو الحال مع المهام العادية. يمكن لمهمة @MainActor التحقق من Task.isCancelled أو رمي CancellationError. عند الإلغاء، لا يتم حظر الخيط الرئيسي — تتوقف المهمة ببساطة عن التنفيذ عند أقرب نقطة تعليق.
يضمن المترجم الأمان: إذا استدعيت دالة @MainActor من سياق خلفي، سيشير المترجم إلى الخطأ. للاستدعاءات غير المتزامنة، يكفي وضع علامة await على الكود المستدعي، وسيقوم المنفذ نفسه بالتبديل إلى الخيط الرئيسي. للاستدعاءات المتزامنة، يلزم التبديل الصريح عبر MainActor.run.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا