SwiftUI @Observable vs @State : quand utiliser quoi en 2026
Comprendre les différences entre @Observable et @State en SwiftUI pour choisir le bon outil de gestion d'état selon le contexte de votre application iOS.

La gestion d'état constitue le pilier central de toute application SwiftUI performante. Depuis iOS 17, le macro @Observable révolutionne la façon de créer des modèles réactifs, tandis que @State reste incontournable pour l'état local des vues. Comprendre quand utiliser chaque outil permet d'éviter les re-renders inutiles et de construire des applications fluides.
Cet article explore les mécanismes internes de @Observable et @State, leurs différences fondamentales, et fournit des règles claires pour choisir le bon outil selon le contexte. Inclut les changements du macro @State d'iOS 27 (rétro-portés vers iOS 17).
Les fondamentaux de @State
@State représente la forme la plus simple de gestion d'état en SwiftUI. Ce property wrapper crée un stockage persistant pour une valeur qui appartient exclusivement à la vue qui la déclare.
struct CounterView: View {
// @State creates storage managed by SwiftUI
@State private var count = 0
var body: some View {
VStack(spacing: 20) {
// The view updates when count changes
Text("Counter: (count)")
.font(.largeTitle)
HStack(spacing: 16) {
Button("- 1") {
count -= 1
}
Button("+ 1") {
count += 1
}
}
.buttonStyle(.borderedProminent)
}
}
}Chaque modification de count déclenche un re-render de la vue. SwiftUI gère automatiquement le cycle de vie de cette valeur, la préservant lors des reconstructions du body.
Caractéristiques clés de @State
@State possède plusieurs propriétés distinctives qui définissent son utilisation optimale :
struct FormView: View {
// Simple local state with value types
@State private var username = ""
@State private var isEnabled = true
@State private var selectedIndex = 0
// Complex value types supported
@State private var configuration = FormConfiguration()
var body: some View {
Form {
TextField("Username", text: $username)
Toggle("Enabled", isOn: $isEnabled)
Picker("Option", selection: $selectedIndex) {
Text("Option A").tag(0)
Text("Option B").tag(1)
Text("Option C").tag(2)
}
}
}
}
// Structs work perfectly with @State
struct FormConfiguration: Equatable {
var theme: Theme = .light
var fontSize: CGFloat = 16
var showNotifications: Bool = true
}
enum Theme {
case light, dark, system
}L'élément crucial : @State fonctionne avec des types valeur (structs, enums, types primitifs). Pour les types référence (classes), d'autres outils sont nécessaires.
Le macro @Observable expliqué
Introduit avec iOS 17, @Observable transforme n'importe quelle classe en source de données réactive. Contrairement à l'ancien protocole ObservableObject, ce macro offre une granularité fine : seules les propriétés réellement lues par une vue déclenchent son re-render. La documentation du framework Observation fournit la référence complète de l'API.
import Observation
// @Observable transforms the class into a reactive source
@Observable
class UserModel {
var name: String = ""
var email: String = ""
var avatarURL: URL?
var preferences = UserPreferences()
// Computed properties work too
var isValid: Bool {
!name.isEmpty && email.contains("@")
}
}
struct UserPreferences {
var newsletter: Bool = false
var notifications: Bool = true
var theme: Theme = .system
}La magie opère à la compilation : le macro génère automatiquement le code de tracking nécessaire pour chaque propriété.
Observation granulaire en action
La différence majeure avec l'ancien ObservableObject réside dans la granularité du tracking :
@Observable
class ProfileModel {
var name: String = ""
var bio: String = ""
var followerCount: Int = 0
var posts: [Post] = []
}
struct ProfileHeaderView: View {
let model: ProfileModel
var body: some View {
VStack {
// This view only re-renders if name or bio change
Text(model.name)
.font(.title)
Text(model.bio)
.foregroundStyle(.secondary)
}
}
}
struct FollowerCountView: View {
let model: ProfileModel
var body: some View {
// This view only re-renders if followerCount changes
HStack {
Image(systemName: "person.2")
Text("(model.followerCount) followers")
}
}
}
struct ProfileScreen: View {
@State private var model = ProfileModel()
var body: some View {
VStack {
// Each subview tracks only its dependencies
ProfileHeaderView(model: model)
FollowerCountView(model: model)
Button("Simulate new follower") {
// Only re-renders FollowerCountView
model.followerCount += 1
}
}
}
}SwiftUI analyse le body de chaque vue pour déterminer quelles propriétés sont lues. Seules ces propriétés déclenchent un re-render lors de leur modification.
Comparaison directe @Observable vs @State
Le choix entre ces deux outils dépend de plusieurs facteurs. Voici une comparaison structurée :
// Scenario 1: Temporary UI state -> @State
struct ToggleExample: View {
@State private var isExpanded = false // @State appropriate
var body: some View {
VStack {
Button(isExpanded ? "Collapse" : "Expand") {
withAnimation {
isExpanded.toggle()
}
}
if isExpanded {
Text("Detailed content...")
}
}
}
}
// Scenario 2: Shared business data -> @Observable
@Observable
class CartModel { // @Observable appropriate
var items: [CartItem] = []
var promoCode: String?
var total: Decimal {
items.reduce(0) { $0 + $1.price * Decimal($1.quantity) }
}
var itemCount: Int {
items.reduce(0) { $0 + $1.quantity }
}
func addItem(_ item: CartItem) {
if let index = items.firstIndex(where: { $0.id == item.id }) {
items[index].quantity += 1
} else {
items.append(item)
}
}
func removeItem(_ item: CartItem) {
items.removeAll { $0.id == item.id }
}
}
struct CartItem: Identifiable, Equatable {
let id: UUID
let name: String
let price: Decimal
var quantity: Int
}Tableau récapitulatif des cas d'usage
| Critère | @State | @Observable |
|---|---|---|
| Type de données | Types valeur (struct, enum) | Classes |
| Scope | Local à une vue | Partageable entre vues |
| Complexité | État simple | Logique métier complexe |
| Cycle de vie | Géré par SwiftUI | Géré explicitement |
| Re-render | Vue entière | Granulaire par propriété |
Prêt à réussir tes entretiens iOS ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Mise à jour iOS 27 : @State en macro avec initialisation paresseuse
La WWDC 2026 a introduit un changement significatif pour @State : il est désormais implémenté comme un macro plutôt qu'un property wrapper. Ce changement résout directement un problème fréquent lors de l'utilisation de @State avec des classes @Observable.
Le problème avant iOS 27
Auparavant, les classes @Observable stockées dans @State étaient initialisées de manière anticipée. Cela pouvait provoquer la création multiple de la même instance avant même l'apparition de la vue :
// Before iOS 27: potential multiple initializations
struct StickerStoreView: View {
// The StickerStore instance could be created eagerly
// and recreated during parent view updates
@State private var store = StickerStore()
var body: some View {
// ...
}
}
@Observable
class StickerStore {
init() {
print("StickerStore initialized")
// This could print multiple times before the view appeared
}
}La correction : initialisation paresseuse
Avec iOS 27 (et rétro-porté vers iOS 17), les classes @Observable stockées dans @State sont désormais initialisées de manière paresseuse, exactement une fois durant le cycle de vie de la vue :
// iOS 27+: guaranteed single initialization
struct StickerStoreView: View {
@State private var store = StickerStore()
var body: some View {
// store is created exactly once, when the view first needs it
StoreContentView(store: store)
}
}
@Observable
class StickerStore {
var stickers: [Sticker] = []
init() {
print("StickerStore initialized")
// Now prints exactly once per view lifetime
}
}Lors de l'assignation de @State dans un initialiseur, supprimez la valeur par défaut pour éviter les erreurs de compilation :
// Correct pattern when using custom initializer
struct StickerPageView: View {
@State private var page: StickerPage // No default value
let title: String
init(title: String) {
self.page = StickerPage(title: title)
self.title = title
}
var body: some View {
// ...
}
}Cette correction élimine les allocations d'objets inutiles et garantit la persistance correcte des instances @Observable tout au long du cycle de vie de la vue. Pour les développeurs qui préparent des questions d'entretien iOS, comprendre ce changement démontre une connaissance des mécanismes internes actuels de SwiftUI.
Patterns d'utilisation avancés
Combiner @State et @Observable
Dans les applications réelles, ces outils coexistent harmonieusement. @State gère l'état UI local tandis que @Observable encapsule les données métier.
@Observable
class TodoListModel {
var todos: [Todo] = []
var filter: TodoFilter = .all
var filteredTodos: [Todo] {
switch filter {
case .all:
return todos
case .active:
return todos.filter { !$0.isCompleted }
case .completed:
return todos.filter { $0.isCompleted }
}
}
func addTodo(title: String) {
let todo = Todo(id: UUID(), title: title, isCompleted: false)
todos.append(todo)
}
func toggleTodo(_ todo: Todo) {
guard let index = todos.firstIndex(where: { $0.id == todo.id }) else { return }
todos[index].isCompleted.toggle()
}
}
struct Todo: Identifiable, Equatable {
let id: UUID
var title: String
var isCompleted: Bool
}
enum TodoFilter: CaseIterable {
case all, active, completed
}
struct TodoListView: View {
// Business data via @Observable (lazy init since iOS 27)
@State private var model = TodoListModel()
// Local UI state via @State
@State private var newTodoTitle = ""
@State private var isAddingTodo = false
@State private var selectedTodo: Todo?
var body: some View {
NavigationStack {
VStack {
// Filter with Picker
Picker("Filter", selection: $model.filter) {
ForEach(TodoFilter.allCases, id: .self) { filter in
Text(filter.label).tag(filter)
}
}
.pickerStyle(.segmented)
.padding()
// Todo list
List(model.filteredTodos, selection: $selectedTodo) { todo in
TodoRowView(todo: todo) {
model.toggleTodo(todo)
}
}
}
.navigationTitle("Tasks")
.toolbar {
Button {
isAddingTodo = true
} label: {
Image(systemName: "plus")
}
}
.sheet(isPresented: $isAddingTodo) {
AddTodoSheet(model: model)
}
}
}
}
struct TodoRowView: View {
let todo: Todo
let onToggle: () -> Void
var body: some View {
HStack {
Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundStyle(todo.isCompleted ? .green : .secondary)
.onTapGesture(perform: onToggle)
Text(todo.title)
.strikethrough(todo.isCompleted)
}
}
}
extension TodoFilter {
var label: String {
switch self {
case .all: return "All"
case .active: return "Active"
case .completed: return "Completed"
}
}
}@Observable avec injection de dépendances
Pour les applications plus complexes, l'injection via l'environnement SwiftUI permet un découplage efficace :
@Observable
class AuthenticationService {
var currentUser: User?
var isAuthenticated: Bool { currentUser != nil }
func login(email: String, password: String) async throws {
// Authentication logic
currentUser = User(id: UUID(), email: email, name: "User")
}
func logout() {
currentUser = nil
}
}
struct User: Identifiable, Equatable {
let id: UUID
let email: String
let name: String
}
// Extension to create an environment key
extension EnvironmentValues {
@Entry var authService: AuthenticationService = AuthenticationService()
}
// Configuration in the App
@main
struct MyApp: App {
@State private var authService = AuthenticationService()
var body: some Scene {
WindowGroup {
ContentView()
.environment(.authService, authService)
}
}
}
// Usage in views
struct ProfileView: View {
@Environment(.authService) private var authService
var body: some View {
if let user = authService.currentUser {
VStack {
Text("Hello, (user.name)")
Button("Sign Out") {
authService.logout()
}
}
} else {
Text("Not signed in")
}
}
}Performance et optimisation
Éviter les re-renders inutiles
Même avec la granularité de @Observable, certains patterns peuvent dégrader les performances :
// Bad pattern: reading the entire object
struct BadPatternView: View {
let model: ProfileModel
var body: some View {
// Reads model.name AND model.posts even if only name is displayed
let _ = model.posts.count // Creates unnecessary dependency
Text(model.name)
}
}
// Good pattern: targeted reading
struct GoodPatternView: View {
let model: ProfileModel
var body: some View {
// Tracks only name
Text(model.name)
}
}
// Extract into subviews to isolate dependencies
struct OptimizedProfileView: View {
let model: ProfileModel
var body: some View {
VStack {
// Each subview has its own dependencies
ProfileNameView(model: model)
ProfilePostsView(model: model)
ProfileStatsView(model: model)
}
}
}
struct ProfileNameView: View {
let model: ProfileModel
var body: some View {
Text(model.name)
.font(.title)
}
}
struct ProfilePostsView: View {
let model: ProfileModel
var body: some View {
ForEach(model.posts) { post in
PostRow(post: post)
}
}
}
struct ProfileStatsView: View {
let model: ProfileModel
var body: some View {
HStack {
StatBadge(value: model.followerCount, label: "Followers")
StatBadge(value: model.posts.count, label: "Posts")
}
}
}Les propriétés calculées de @Observable sont réévaluées à chaque accès. Pour des calculs complexes, envisagez de mettre en cache le résultat dans une propriété stockée.
Observation avancée avec Swift 6.4
Swift 6.4 a introduit SE-0506, ajoutant des API de notification de changement granulaires et continues au framework Observation. Ces outils avancés ciblent les développeurs de middleware et les implémentations de systèmes de widgets plutôt que le développement d'applications standard. La plupart des applications continuent d'utiliser le pattern standard du macro @Observable. Les notes de version de Swift 6.4 détaillent ces ajouts.
Migration depuis ObservableObject
Pour les projets existants utilisant ObservableObject, la migration vers @Observable simplifie le code :
// Old pattern with ObservableObject
class OldSettingsModel: ObservableObject {
@Published var darkMode: Bool = false
@Published var fontSize: CGFloat = 16
@Published var notifications: Bool = true
}
struct OldSettingsView: View {
@StateObject private var settings = OldSettingsModel()
// or @ObservedObject if injected
var body: some View {
Form {
Toggle("Dark Mode", isOn: $settings.darkMode)
Slider(value: $settings.fontSize, in: 12...24)
Toggle("Notifications", isOn: $settings.notifications)
}
}
}
// New pattern with @Observable
@Observable
class NewSettingsModel {
var darkMode: Bool = false
var fontSize: CGFloat = 16
var notifications: Bool = true
}
struct NewSettingsView: View {
@State private var settings = NewSettingsModel()
var body: some View {
Form {
Toggle("Dark Mode", isOn: $settings.darkMode)
Slider(value: $settings.fontSize, in: 12...24)
Toggle("Notifications", isOn: $settings.notifications)
}
}
}Les avantages de la migration :
- Plus besoin de
@Publishedsur chaque propriété @Stateremplace@StateObjectpour la création (avec initialisation paresseuse garantie)- Observation granulaire automatique
- Code plus lisible et maintenable
Pour les applications ciblant iOS 17+, la migration vers @Observable est recommandée. Le guide de migration Combine vs async/await couvre des patterns de modernisation connexes.
Règles de décision pratiques
Voici un guide décisionnel pour choisir le bon outil :
/*
RULE 1: Ephemeral UI state -> @State
- Animations, transitions
- Local form states
- Temporary selections
- Section expand/collapse
*/
struct AnimatedCard: View {
@State private var isFlipped = false // Local UI state
// ...
}
/*
RULE 2: Shared data across views -> @Observable
- Business data models
- Authentication state
- Shopping cart
- User preferences
*/
@Observable
class UserSession { // Shared across app
var user: User?
var preferences: Preferences
// ...
}
/*
RULE 3: Simple struct with binding -> @State
- Local configuration
- Isolated forms
*/
struct FormData {
var name: String = ""
var email: String = ""
}
struct FormView: View {
@State private var formData = FormData() // Struct with @State
// ...
}
/*
RULE 4: Complex business logic -> @Observable
- Validations
- Network calls
- Data transformations
*/
@Observable
class OrderProcessor { // Complex logic
var items: [OrderItem] = []
var status: OrderStatus = .draft
func validate() -> [ValidationError] { /* ... */ }
func submit() async throws { /* ... */ }
}Sources
- Documentation du framework Observation : référence officielle de l'API Apple
- Notes de version Swift 6.4 : tracking d'observation avancé SE-0506
- Guide SwiftUI WWDC 2026 : changements du macro @State et initialisation paresseuse
Choisir entre @Observable et @State en SwiftUI
Le choix entre @Observable et @State se résume à deux questions fondamentales : le type de données (valeur ou référence) et le scope de l'état (local ou partagé). @State excelle pour l'état UI simple et local, tandis que @Observable brille pour les modèles de données complexes nécessitant une observation granulaire.
Checklist de décision
- Utiliser
@Statepour les types valeur et l'état UI éphémère - Utiliser
@Observablepour les classes avec données métier - Préférer
@Observablequand l'état traverse plusieurs vues - Extraire en sous-vues pour optimiser les re-renders
- Éviter de lire des propriétés non nécessaires dans le body
- Migrer progressivement depuis
ObservableObject - Utiliser l'environnement pour l'injection de dépendances
- Tester les performances avec Instruments pour les cas complexes
Avec les améliorations du macro @State d'iOS 27 (rétro-portées vers iOS 17), combiner ces outils est plus fiable que jamais. La correction de l'initialisation paresseuse supprime une source courante de bugs lors du stockage de classes @Observable dans @State.
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Tu saurais repérer le bug en iOS ?
Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Écrit par
Anthony Fillion-MailletFondateur de SharpSkill
Développeur fullstack depuis plus de 10 ans. Il dirige SharpSkill et répond de tout ce qui y est publié.
Mis à jour le 5 octobre 2026
Tags
Partager
Articles similaires

SwiftUI Performance : optimiser LazyVStack et listes complexes
Techniques d'optimisation pour LazyVStack et listes SwiftUI. Réduire la consommation mémoire, améliorer le scrolling et éviter les pièges de performance courants.

SwiftUI Custom ViewModifiers : patterns réutilisables pour design system
Créer des ViewModifiers SwiftUI personnalisés pour un design system cohérent. Patterns, bonnes pratiques et exemples concrets pour styliser vos vues iOS efficacement.

CloudKit avec SwiftUI en 2026 : synchronisation de données cross-device
Guide complet pour implémenter la synchronisation CloudKit avec SwiftUI : CKSyncEngine, intégration SwiftData, gestion des conflits et bonnes pratiques iOS 2026.