SwiftUI @Observable vs @State: Kapan Menggunakan yang Mana di 2026

Kuasai perbedaan antara @Observable dan @State di SwiftUI untuk memilih alat manajemen state yang tepat untuk aplikasi iOS.

Perbandingan @Observable dan @State di SwiftUI untuk developer iOS

Manajemen state adalah pondasi utama dari setiap aplikasi SwiftUI yang performan. Sejak iOS 17, makro @Observable telah merevolusi cara model reaktif dibuat, sementara @State tetap penting untuk state lokal pada view. Memahami kapan menggunakan setiap alat akan menghindari render ulang yang tidak perlu dan memungkinkan pembuatan aplikasi yang lancar serta responsif.

Yang dibahas artikel ini

Artikel ini menjelajahi mekanisme internal @Observable dan @State, perbedaan fundamentalnya, serta menyediakan panduan jelas untuk memilih alat yang tepat sesuai konteks. Termasuk perubahan makro @State di iOS 27 (backport ke iOS 17).

Dasar @State

@State mewakili bentuk paling sederhana dari manajemen state di SwiftUI. Property wrapper ini menciptakan penyimpanan persisten untuk sebuah nilai yang sepenuhnya dimiliki oleh view yang mendeklarasikannya.

CounterView.swiftswift
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)
        }
    }
}

Setiap perubahan pada count memicu render ulang view. SwiftUI secara otomatis mengelola siklus hidup nilai ini, mempertahankannya antara rekonstruksi body.

Karakteristik utama @State

@State memiliki beberapa sifat khas yang menentukan penggunaannya yang optimal:

StateCharacteristics.swiftswift
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
}

Elemen krusialnya: @State bekerja dengan tipe nilai (struct, enum, tipe primitif). Untuk tipe referensi (kelas), diperlukan alat lain.

Makro @Observable dijelaskan

Diperkenalkan dengan iOS 17, @Observable mengubah kelas apa pun menjadi sumber data reaktif. Berbeda dengan protokol ObservableObject lama, makro ini menawarkan observasi granular: hanya properti yang benar-benar dibaca oleh view yang memicu render ulangnya. Dokumentasi framework Observation menyediakan referensi API yang lengkap.

UserModel.swiftswift
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
}

Keajaibannya terjadi pada waktu kompilasi: makro secara otomatis menghasilkan kode pelacakan yang diperlukan untuk setiap properti.

Observasi granular dalam aksi

Perbedaan utama dari ObservableObject lama terletak pada granularitas pelacakan:

GranularObservation.swiftswift
@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
            }
        }
    }
}
Pelacakan otomatis

SwiftUI menganalisis body setiap view untuk menentukan properti mana yang dibaca. Hanya properti tersebut yang memicu render ulang ketika dimodifikasi.

Perbandingan langsung: @Observable vs @State

Pemilihan antara kedua alat ini bergantung pada beberapa faktor. Berikut perbandingan terstruktur:

ComparisonExample.swiftswift
// 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
}

Tabel ringkasan kasus penggunaan

Kriteria@State@Observable
Tipe dataTipe nilai (struct, enum)Kelas
CakupanLokal pada satu viewDapat dibagi antar view
KompleksitasState sederhanaLogika bisnis kompleks
Siklus hidupDikelola SwiftUIDikelola secara eksplisit
Render ulangSeluruh viewGranular per properti

Siap menguasai wawancara iOS Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Pembaruan iOS 27: @State sebagai makro dengan inisialisasi lazy

WWDC 2026 memperkenalkan perubahan signifikan pada @State: sekarang diimplementasikan sebagai makro, bukan property wrapper. Perubahan ini langsung mengatasi masalah umum saat menggunakan @State dengan kelas @Observable.

Masalah sebelum iOS 27

Sebelumnya, kelas @Observable yang disimpan di @State diinisialisasi secara eager. Hal ini bisa menyebabkan instance yang sama dibuat berkali-kali sebelum view tampil:

BeforeIOS27.swiftswift
// 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
    }
}

Solusinya: inisialisasi lazy

Dengan iOS 27 (dan backport ke iOS 17), kelas @Observable yang disimpan di @State sekarang diinisialisasi secara lazy, tepat sekali untuk seumur hidup view:

AfterIOS27.swiftswift
// 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
    }
}
Perubahan pola initializer

Saat menetapkan @State di initializer, hapus nilai default untuk menghindari error kompilasi:

InitializerPattern.swiftswift
// 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 {
        // ...
    }
}

Perbaikan ini menghilangkan alokasi objek yang tidak perlu dan memastikan instance @Observable bertahan dengan benar sepanjang siklus hidup view. Bagi developer yang mempersiapkan pertanyaan wawancara iOS, memahami perubahan ini menunjukkan pengetahuan tentang internal SwiftUI terkini.

Pola penggunaan lanjutan

Menggabungkan @State dan @Observable

Dalam aplikasi nyata, kedua alat ini hidup berdampingan dengan harmonis. @State menangani state UI lokal sementara @Observable membungkus data bisnis.

CombinedPatterns.swiftswift
@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 dengan dependency injection

Untuk aplikasi yang lebih kompleks, injeksi melalui environment SwiftUI memungkinkan pemisahan yang efektif:

DependencyInjection.swiftswift
@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")
        }
    }
}

Performa dan optimasi

Menghindari render ulang yang tidak perlu

Meskipun dengan granularitas @Observable, beberapa pola tetap dapat menurunkan performa:

PerformanceOptimization.swiftswift
// 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")
        }
    }
}
Properti computed yang mahal

Properti computed pada @Observable dievaluasi ulang setiap kali diakses. Untuk perhitungan kompleks, sebaiknya cache hasilnya dalam properti tersimpan.

Observasi lanjutan dengan Swift 6.4

Swift 6.4 memperkenalkan SE-0506, menambahkan API notifikasi perubahan yang granular dan kontinu ke framework Observation. Alat-alat lanjutan ini ditargetkan untuk pengembang middleware dan implementasi sistem widget, bukan untuk pengembangan aplikasi tipikal. Sebagian besar aplikasi tetap menggunakan pola makro @Observable standar. Catatan rilis Swift 6.4 merinci penambahan ini.

Migrasi dari ObservableObject

Untuk proyek yang sudah ada dan menggunakan ObservableObject, migrasi ke @Observable menyederhanakan kode:

MigrationExample.swiftswift
// 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)
        }
    }
}

Keuntungan migrasi:

  • Tidak lagi membutuhkan @Published pada setiap properti
  • @State menggantikan @StateObject untuk pembuatan (dengan jaminan init lazy)
  • Observasi granular otomatis
  • Kode yang lebih mudah dibaca dan dipelihara

Untuk aplikasi yang menargetkan iOS 17+, migrasi ke @Observable direkomendasikan. Panduan migrasi Combine vs async/await membahas pola modernisasi terkait.

Aturan praktis pengambilan keputusan

Panduan keputusan untuk memilih alat yang tepat:

DecisionGuide.swiftswift
/*
 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 { /* ... */ }
}

Sumber

Memilih antara @Observable dan @State di SwiftUI

Pemilihan antara @Observable dan @State bermuara pada dua pertanyaan fundamental: tipe data (nilai atau referensi) dan cakupan state (lokal atau berbagi). @State unggul untuk state UI yang sederhana dan lokal, sedangkan @Observable bersinar untuk model data kompleks yang memerlukan observasi granular.

Daftar periksa keputusan

  • Gunakan @State untuk tipe nilai dan state UI sementara
  • Gunakan @Observable untuk kelas dengan data bisnis
  • Lebih utamakan @Observable ketika state melintasi beberapa view
  • Pisahkan ke subview untuk mengoptimalkan render ulang
  • Hindari membaca properti yang tidak perlu di body
  • Migrasikan secara bertahap dari ObservableObject
  • Gunakan environment untuk dependency injection
  • Uji performa dengan Instruments untuk kasus kompleks

Dengan peningkatan makro @State di iOS 27 (backport ke iOS 17), menggabungkan kedua alat ini menjadi lebih andal dari sebelumnya. Perbaikan inisialisasi lazy menghilangkan sumber bug umum saat menyimpan kelas @Observable di @State.

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tantangan harian

Bisakah kamu menemukan bug di iOS?

Satu potongan kode nyata, satu bug tersembunyi, satu percobaan per hari. Tanpa akun untuk mencoba.

Anthony Fillion-Maillet

Ditulis oleh

Anthony Fillion-Maillet

Pendiri SharpSkill

Developer fullstack selama lebih dari 10 tahun. Ia menjalankan SharpSkill dan bertanggung jawab atas semua yang diterbitkan di sini.

Diperbarui 5 Oktober 2026

Tag

#swiftui
#ios
#observable
#state-management
#swift

Bagikan

Artikel terkait