@StateObject هو Property Wrapper في SwiftUI لإنشاء وامتلاك مثيل ObservableObject مباشرة في view. يضمن SwiftUI تهيئة الكائن مرة واحدة طوال دورة حياة العرض وعدم إعادة إنشائه في عمليات إعادة التصيير اللاحقة. وفقاً لـ وثائق مطوري Apple (2025)، يُوصى باستخدام @StateObject للعروض الجذرية التي تنشئ مصدر بيانات. @StateObject هو الخيار الصحيح لامتلاك ObservableObject في تسلسل SwiftUI.
الخلاصة
@StateObject هو Property Wrapper ظهر في SwiftUI 2.0 (iOS 14) يجمع بين إمكانيات @ObservedObject و @State. مثل @ObservedObject، يشترك في تغييرات ObservableObject. ومثل @State، يضمن بقاء البيانات عبر عمليات تهيئة متكررة لهيكل العرض. ينشئ @StateObject الكائن مرة واحدة عند أول ظهور للعرض على الشاشة ويخزنه في كومة SwiftUI.
قبل ظهور @StateObject، كان المطورون يستخدمون @ObservedObject لجميع ObservableObjects، بما في ذلك تلك المنشأة في العروض. أدى ذلك إلى فقدان متكرر للبيانات عند تحديث العرض الأب، مما تسبب في إعادة إنشاء هيكل العرض وأخذ مثيل @ObservedObject معه. حل @StateObject هذه المشكلة بإضافة ضمان الاستقرار.
القاعدة الأساسية: يُستخدم @StateObject في العرض الذي ينشئ الكائن في المُهيئ الافتراضي (let model = ViewModel()). العروض الفرعية التي تستلم هذا الكائن تستخدم @ObservedObject. يضمن هذا الفصل مصدر حقيقة واحد في جميع أنحاء التسلسل الهرمي.
يدير SwiftUI دورة حياة @StateObject من خلال مدير تخزين مشابه لـ @State. عند أول ظهور للعرض، يخصص SwiftUI ذاكرة للكائن ويخزنه في منطقة ثابتة. في عمليات إعادة التصيير اللاحقة (استدعاءات body)، لا يُعاد إنشاء الكائن—يُستخدم المثيل الموجود. يعيش الكائن طالما أن العرض موجود في التسلسل الهرمي.
عند إزالة العرض من التسلسل الهرمي، يقوم SwiftUI بتدمير @StateObject، باستدعاء deinit. عند إعادة إضافة العرض إلى التسلسل الهرمي، يُنشأ مثيل جديد. من المهم أخذ هذا في الاعتبار عند التصميم: إذا كنت بحاجة إلى الحفاظ على البيانات بين عمليات إزالة العرض، استخدم طبقة خدمة (singleton أو DI) أو @AppStorage للاستمرارية.
class TimerViewModel: ObservableObject {
@Published var seconds: Int = 0
private var timer: Timer?
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.seconds += 1
}
}
deinit {
timer?.invalidate()
}
}
struct TimerView: View {
@StateObject var viewModel = TimerViewModel()
var body: some View {
Text("\(viewModel.seconds)s")
.onAppear { viewModel.start() }
}
}
في المثال، يتم إنشاء TimerViewModel عبر @StateObject ويعيش طالما أن TimerView على الشاشة. يبدأ المؤقت في onAppear ويتوقف في deinit. إذا تم استخدام @ObservedObject، فإن كل إعادة تصيير لـ TimerView ستنشئ TimerViewModel جديداً بثوانٍ = 0، ولن يعمل المؤقت بشكل صحيح أبداً. يضمن @StateObject أن viewModel فريد ومستقر.
يعتمد الاختيار بين @StateObject و @ObservedObject على من يملك الكائن. إذا كان العرض ينشئ الكائن—@StateObject. إذا كان العرض يستلم كائناً جاهزاً—@ObservedObject. هذه القاعدة مهمة جداً لدرجة أن Xcode يُصدر تحذيراً عند استخدام @StateObject في عرض فرعي يستلم الكائن عبر مُهيئ.
| الموقف | الـ Wrapper الموصى به |
|---|---|
| العرض ينشئ نموذجاً عبر ViewModel() | @StateObject |
| العرض يستلم النموذج من الأب | @ObservedObject |
| النموذج يُستخدم في عرض واحد | @StateObject |
| النموذج يُمرر عبر Environment | @EnvironmentObject |
| النموذج مطلوب للمعاينات | @ObservedObject + mock |
عملياً، في بداية المشروع، غالباً ما يُستخدم @StateObject في العرض الجذري و @ObservedObject في جميع العروض الفرعية. مع نمو التطبيق، يمكن استبدال بعض مثيلات @StateObject بـ @EnvironmentObject لتبسيط التسلسل الهرمي. ومع ذلك، يبقى @StateObject الخيار الأفضل للشاشات المعيارية ذات المنطق الخاص بها.
النمط الأول—MVVM مع @StateObject. يُنشأ ViewModel كـ ObservableObject في العرض عبر @StateObject. يحتوي ViewModel على خصائص @Published ومنطق الأعمال. يشترك العرض في التغييرات ويُحدّث الواجهة. يوفر هذا النهج عزلاً قابلاً للاختبار: يمكن اختبار ViewModel بدون واجهة مستخدم عن طريق إنشاء مثيل مباشرة.
النمط الثاني—@StateObject مع التبعيات. إذا كان ViewModel يتطلب خدمات، استخدم التهيئة مع المعلمات. على سبيل المثال، @StateObject var viewModel = UserViewModel(api: APIClient.shared). لكن كن حذراً: تُحسب المعلمات في كل إعادة تصيير لـ body، لكن يُنشأ الكائن مرة واحدة فقط. يتجاهل SwiftUI عمليات التهيئة اللاحقة لـ @StateObject.
النمط الثالث—@StateObject متداخلة. في SwiftUI، يمكن أن يكون لديك عدة @StateObject في عرض واحد، لكن هذا نادراً ما يكون مبرراً. عادةً ما يتولى @StateObject واحد مجموعة البيانات بأكملها للعرض. إذا أصبح المنطق معقداً جداً، قسّمه إلى تكوين من خدمات @ObservedObject داخل @StateObject واحد.
struct AppView: View {
@StateObject var router = NavigationRouter()
@StateObject var auth = AuthViewModel()
var body: some View {
ContentView()
.environmentObject(router)
.environmentObject(auth)
}
}
في المثال، ينشئ AppView اثنين من @StateObject: NavigationRouter لإدارة التنقل و AuthViewModel للمصادقة. يتم حقن كلا الكائنين في Environment عبر environmentObject. يمكن لأي عرض فرعي الوصول إليهما عبر @EnvironmentObject دون المرور عبر سلسلة المُهيئات.
@StateObject يدعم التهيئة مع أي معلمات، لكن مع ملاحظة مهمة: يُستدعى المُهيئ مرة واحدة فقط. في عمليات إعادة تصيير body اللاحقة، يتم تجاهل القيم الجديدة في المعلمات. هذا يعني أنه إذا مررت @State var id: Int = 5 إلى @StateObject var vm = ViewModel(id: id)، فعند تغيير id، لن يستلم ViewModel القيمة الجديدة.
لحل هذه المشكلة، استخدم onReceive أو onAppear للمزامنة. اشترك في تغييرات المعلمة داخل ViewModel عبر Combine أو مرر المعلمات من خلال طريقة .onChange(of:) على مستوى العرض. بديل آخر هو استخدام @ObservedObject بدلاً من @StateObject إذا كان يجب أن يستجيب الكائن ديناميكياً للتغييرات الخارجية.
struct DetailView: View {
let itemId: Int
@StateObject var viewModel = DetailViewModel()
var body: some View {
Text(viewModel.title)
.onAppear { viewModel.load(id: itemId) }
}
}
النهج الصحيح: DetailView يستلم itemId كخاصية let (تُمرر عبر مُهيئ الهيكل)، ويُنشئ @StateObject DetailViewModel بدون معلمات. في onAppear، يُستدعى الأسلوب load(id:) لتحميل البيانات للمعرف المُمرر. يضمن هذا أن ViewModel يُنشأ بواسطة آلية @StateObject، ولكن البيانات تُحمّل في كل ظهور للعرض بالمعرف الحالي.
الخطأ الرئيسي—استخدام @StateObject في العروض الفرعية التي تستلم الكائن من الأب. إذا أنشأ ParentView @StateObject model، وأعلن ChildView @StateObject var model: ModelType (بمعامل افتراضي)، فسينشئ ChildView مثيلاً مستقلاً خاصاً به. لن يكون الكائنان الأب والابن مرتبطين، ولن تنعكس التغييرات في أحدهما على الآخر.
الخطأ الثاني—وضع @StateObject في List أو ForEach. كل عنصر في القائمة ينشئ @StateObject خاصاً به، مما يؤدي إلى مثيلات مستقلة متعددة. بالنسبة للقوائم، النهج الصحيح هو تمرير ObservableObject واحد لجميع العناصر عبر @ObservedObject أو استخدام هياكل Identifiable مع @State داخل List.
المشكلة الثالثة—عدم وجود تنظيف في deinit. يعيش @StateObject طوال دورة حياة العرض بالكامل. إذا أنشأ الكائن مؤقتات أو اشتراكات Combine أو طلبات شبكة، يجب على deinit إلغاءها. وإلا، فإن تسرب الذاكرة واستمرار العمل في الخلفية بعد إغلاق الشاشة أمر لا مفر منه. استخدم دائماً مخزن Cancellable من Combine أو ألغِ المؤقتات في deinit.
الأسئلة الشائعة
@StateObject أُضيف في SwiftUI 2.0 في WWDC 2020 مع iOS 14 و macOS 11 و watchOS 7 و tvOS 14. قبل ذلك، كان @ObservedObject هو الطريقة الوحيدة للعمل مع ObservableObject، مما أدى غالباً إلى أخطاء فقدان البيانات.
لا، @StateObject لا يدعم الأنواع الاختيارية (Optional). يجب تهيئة الكائن عند الإعلان. إذا كنت بحاجة إلى كائن اختياري، استخدم @ObservedObject أو @EnvironmentObject مع نوع اختياري.
أضف print(#function) إلى المُهيئ و deinit الخاص بـ ObservableObject. إذا لم يتم استدعاء init في عمليات إعادة التصيير—فإن @StateObject يعمل بشكل صحيح. إذا تم استدعاء init في كل مرة—استبدل @ObservedObject بـ @StateObject.
نعم، يعمل @StateObject في عروض SwiftUI المضمنة في UIKit عبر UIHostingController. دورة حياة الكائن مرتبطة بعرض SwiftUI، وليس بـ UIViewController. إذا تم استبدال عرض SwiftUI، يُتلف @StateObject.
عدة @StateObjects صغيرة بمسؤوليات منفصلة. هذا يحسن قابلية الاختبار وإعادة الاستخدام والأداء—عندما يتغير كائن واحد، تُعاد رسم الأجزاء المشتركة فقط من الواجهة، وليس العرض بأكمله.
ملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.