@StateObject είναι ένα Property Wrapper στο SwiftUI για τη δημιουργία και ιδιοκτησία μιας παρουσίας ObservableObject απευθείας στο view. Το SwiftUI εγγυάται ότι το αντικείμενο αρχικοποιείται μία φορά κατά τη διάρκεια του κύκλου ζωής της προβολής και δεν αναδημιουργείται σε επαναλαμβανόμενες αποδόσεις. Σύμφωνα με το Apple Developer Documentation (2025), το @StateObject συνιστάται για ριζικά views που δημιουργούν πηγή δεδομένων. @StateObject είναι η σωστή επιλογή για την ιδιοκτησία ObservableObject στην ιεραρχία SwiftUI.
Κύρια σημεία
@StateObject είναι ένα Property Wrapper που εμφανίστηκε στο SwiftUI 2.0 (iOS 14) και συνδυάζει τις δυνατότητες των @ObservedObject και @State. Όπως το @ObservedObject, εγγράφεται σε αλλαγές ObservableObject. Όπως το @State, εγγυάται ότι τα δεδομένα επιβιώνουν από επαναλαμβανόμενες αρχικοποιήσεις της δομής view. Το @StateObject δημιουργεί το αντικείμενο μία φορά κατά την πρώτη εμφάνιση του view στην οθόνη και το αποθηκεύει στο heap του SwiftUI.
Πριν από την εμφάνιση του @StateObject, οι προγραμματιστές χρησιμοποιούσαν το @ObservedObject για όλα τα ObservableObject, συμπεριλαμβανομένων αυτών που δημιουργούνταν σε views. Αυτό οδηγούσε σε συχνή απώλεια δεδομένων κατά την ενημέρωση του γονικού view, όταν η δομή view αναδημιουργούνταν, παίρνοντας μαζί της και την παρουσία @ObservedObject. Το @StateObject έλυσε αυτό το πρόβλημα προσθέτοντας εγγύηση σταθερότητας.
Ο βασικός κανόνας: το @StateObject εφαρμόζεται στο view που δημιουργεί το αντικείμενο στον προεπιλεγμένο αρχικοποιητή (let model = ViewModel()). Τα θυγατρικά views που λαμβάνουν αυτό το αντικείμενο χρησιμοποιούν @ObservedObject. Αυτός ο διαχωρισμός εγγυάται μια ενιαία πηγή αλήθειας σε ολόκληρη την ιεραρχία.
Το SwiftUI διαχειρίζεται τον κύκλο ζωής του @StateObject μέσω ενός διαχειριστή αποθήκευσης, ανάλογου με το @State. Κατά την πρώτη εμφάνιση του view, το SwiftUI δεσμεύει μνήμη για το αντικείμενο και το αποθηκεύει σε μια μόνιμη περιοχή. Σε επαναλαμβανόμενες αποδόσεις (κλήση body), το αντικείμενο δεν αναδημιουργείται — χρησιμοποιείται η υπάρχουσα παρουσία. Το αντικείμενο ζει όσο το view βρίσκεται στην ιεραρχία.
Όταν το view αφαιρείται από την ιεραρχία, το SwiftUI καταστρέφει το @StateObject, καλώντας το deinit. Κατά την εκ νέου προσθήκη του view στην ιεραρχία, δημιουργείται μια νέα παρουσία. Αυτό είναι σημαντικό να ληφθεί υπόψη κατά τον σχεδιασμό: αν χρειάζεται να διατηρήσετε δεδομένα μεταξύ αφαιρέσεων view, χρησιμοποιήστε ένα επίπεδο υπηρεσίας (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 είναι στην οθόνη. Το Timer ξεκινά στο onAppear και σταματά στο deinit. Αν είχε χρησιμοποιηθεί @ObservedObject, σε κάθε απόδοση του TimerView θα δημιουργούνταν ένα νέο TimerViewModel με δευτερόλεπτα = 0 και το χρονόμετρο δεν θα λειτουργούσε ποτέ σωστά. Το @StateObject εγγυάται ότι το viewModel είναι μοναδικό και σταθερό.
Η επιλογή μεταξύ @StateObject και @ObservedObject εξαρτάται από το ποιος κατέχει το αντικείμενο. Αν το view δημιουργεί το αντικείμενο — @StateObject. Αν το view λαμβάνει ένα έτοιμο αντικείμενο — @ObservedObject. Αυτός ο κανόνας είναι τόσο σημαντικός που το Xcode εκδίδει προειδοποίηση κατά τη χρήση @StateObject σε θυγατρικό view που λαμβάνει το αντικείμενο μέσω αρχικοποιητή.
| Κατάσταση | Συνιστώμενος wrapper |
|---|---|
| View δημιουργεί μοντέλο μέσω ViewModel() | @StateObject |
| View λαμβάνει μοντέλο από γονέα | @ObservedObject |
| Μοντέλο χρησιμοποιείται σε ένα view | @StateObject |
| Μοντέλο μεταδίδεται μέσω Environment | @EnvironmentObject |
| Μοντέλο χρειάζεται για προεπισκόπηση | @ObservedObject + mock |
Στην πράξη, στην αρχή του έργου συχνά χρησιμοποιείται @StateObject στο ριζικό view και @ObservedObject σε όλα τα θυγατρικά views. Καθώς η εφαρμογή μεγαλώνει, μέρος του @StateObject μπορεί να αντικατασταθεί από @EnvironmentObject για απλοποίηση της ιεραρχίας. Ωστόσο, το @StateObject παραμένει η καλύτερη επιλογή για αρθρωτές οθόνες με δική τους λογική.
Το πρώτο μοτίβο — MVVM με @StateObject. Το ViewModel ως ObservableObject δημιουργείται στο view μέσω @StateObject. Το ViewModel περιέχει ιδιότητες @Published και επιχειρηματική λογική. Το view εγγράφεται σε αλλαγές και ενημερώνει τη διεπαφή. Αυτή η προσέγγιση παρέχει ελέγξιμη απομόνωση: το ViewModel μπορεί να ελεγχθεί χωρίς UI δημιουργώντας μια παρουσία απευθείας.
Το δεύτερο μοτίβο — @StateObject με εξαρτήσεις. Αν το ViewModel απαιτεί υπηρεσίες, χρησιμοποιήστε αρχικοποίηση με παραμέτρους. Για παράδειγμα, @StateObject var viewModel = UserViewModel(api: APIClient.shared). Ωστόσο, να είστε προσεκτικοί: οι παράμετροι υπολογίζονται σε κάθε απόδοση body, αλλά το αντικείμενο δημιουργείται μόνο μία φορά. Το SwiftUI αγνοεί τις επόμενες αρχικοποιήσεις του @StateObject.
Το τρίτο μοτίβο — ένθετα @StateObject. Στο SwiftUI μπορείτε να έχετε πολλαπλά @StateObject σε ένα view, αλλά αυτό σπάνια δικαιολογείται. Συνήθως ένα @StateObject είναι υπεύθυνο για ολόκληρο το σύνολο δεδομένων του view. Αν η λογική γίνεται πολύ περίπλοκη, χωρίστε την σε μια σύνθεση υπηρεσιών @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. Οποιοδήποτε θυγατρικό view μπορεί να έχει πρόσβαση σε αυτά μέσω @EnvironmentObject χωρίς να τα μεταδίδει μέσω της αλυσίδας αρχικοποιητών.
@StateObject υποστηρίζει αρχικοποίηση με οποιεσδήποτε παραμέτρους, αλλά με ένα σημαντικό χαρακτηριστικό: ο αρχικοποιητής καλείται μόνο μία φορά. Σε επαναλαμβανόμενες αποδόσεις body, η νέα τιμή των παραμέτρων αγνοείται. Αυτό σημαίνει ότι αν μεταδώσετε @State var id: Int = 5 στο @StateObject var vm = ViewModel(id: id), όταν αλλάξει το id, το ViewModel δεν θα λάβει τη νέα τιμή.
Για να λύσετε αυτό το πρόβλημα, χρησιμοποιήστε onReceive ή onAppear για συγχρονισμό. Εγγραφείτε σε αλλαγές παραμέτρων μέσα στο ViewModel μέσω Combine ή μεταδώστε παραμέτρους μέσω της μεθόδου .onChange(of:) στο επίπεδο view. Εναλλακτικά — χρησιμοποιήστε @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:), η οποία φορτώνει δεδομένα για το μεταδιδόμενο ID. Αυτό εγγυάται ότι το ViewModel δημιουργήθηκε από τον μηχανισμό @StateObject, αλλά τα δεδομένα φορτώνονται σε κάθε εμφάνιση του view με το τρέχον ID.
Το κύριο λάθος — χρήση @StateObject σε θυγατρικά views που λαμβάνουν το αντικείμενο από τον γονέα. Αν το ParentView δημιουργεί @StateObject model, και το ChildView δηλώνει @StateObject var model: ModelType (με προεπιλεγμένη παράμετρο), τότε το ChildView θα δημιουργήσει τη δική του ανεξάρτητη παρουσία. Τα αντικείμενα γονέα και παιδιού δεν θα συνδέονται και οι αλλαγές στο ένα δεν θα αντικατοπτρίζονται στο άλλο.
Το δεύτερο λάθος — τοποθέτηση @StateObject σε List ή ForEach. Κάθε στοιχείο της λίστας δημιουργεί το δικό του @StateObject, οδηγώντας σε πολλαπλές ανεξάρτητες παρουσίες. Για λίστες, είναι σωστό να μεταδίδεται ένα ObservableObject σε όλα τα στοιχεία μέσω @ObservedObject ή να χρησιμοποιούνται δομές Identifiable με @State μέσα στο List.
Το τρίτο πρόβλημα — έλλειψη καθαρισμού στο deinit. Το @StateObject ζει ολόκληρο τον κύκλο ζωής του view. Αν το αντικείμενο δημιουργεί χρονόμετρα, εγγραφές Combine ή αιτήματα δικτύου, το deinit πρέπει να τα ακυρώνει. Διαφορετικά, διαρροές μνήμης και συνέχιση της εργασίας παρασκηνίου μετά το κλείσιμο της οθόνης είναι αναπόφευκτες. Χρησιμοποιείτε πάντα Combine Cancellable store ή ακύρωση χρονομέτρων στο 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 views ενσωματωμένα σε UIKit μέσω UIHostingController. Ο κύκλος ζωής του αντικειμένου συνδέεται με το SwiftUI view, όχι με το UIViewController. Αν το SwiftUI view αντικατασταθεί, το @StateObject καταστρέφεται.
Πολλά μικρά @StateObject με διαχωρισμένες ευθύνες. Αυτό βελτιώνει την ελεγξιμότητα, την επαναχρησιμοποίηση και την απόδοση — όταν ένα αντικείμενο αλλάζει, μόνο τα εγγεγραμμένα μέρη της διεπαφής επανασχεδιάζονται, όχι ολόκληρο το view.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης