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 Anda.

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.
Artikel ini menjelajahi mekanisme internal @Observable dan @State, perbedaan fundamentalnya, serta menyediakan panduan jelas untuk memilih alat yang tepat sesuai konteks.
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.
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:
struct FormView: View {
// ✅ Simple local state - 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.
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:
@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 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:
// 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 data | Tipe nilai (struct, enum) | Kelas | | Cakupan | Lokal pada satu view | Dapat dibagi antar view | | Kompleksitas | State sederhana | Logika bisnis kompleks | | Siklus hidup | Dikelola SwiftUI | Dikelola secara eksplisit | | Render ulang | Seluruh view | Granular per properti |
Siap menguasai wawancara iOS Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
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.
@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
@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:
@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:
// ❌ 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 pada @Observable dievaluasi ulang setiap kali diakses. Untuk perhitungan kompleks, sebaiknya cache hasilnya dalam properti tersimpan.
Pembaruan batch dengan withObservationTracking
Untuk skenario lanjutan, withObservationTracking memungkinkan deteksi perubahan tanpa membuat binding:
import Observation
@Observable
class DataSyncModel {
var lastSyncDate: Date?
var pendingChanges: Int = 0
var isSyncing: Bool = false
}
class SyncCoordinator {
let model: DataSyncModel
init(model: DataSyncModel) {
self.model = model
startObserving()
}
private func startObserving() {
// Observe changes without UI
withObservationTracking {
// Access that creates dependencies
_ = model.pendingChanges
_ = model.isSyncing
} onChange: {
// Called when an observed property changes
Task { @MainActor in
self.handleModelChange()
}
}
}
private func handleModelChange() {
if model.pendingChanges > 0 && !model.isSyncing {
// Trigger synchronization
Task {
await syncChanges()
}
}
// Re-establish observation
startObserving()
}
private func syncChanges() async {
model.isSyncing = true
// Sync logic...
model.isSyncing = false
model.pendingChanges = 0
model.lastSyncDate = Date()
}
}Migrasi dari ObservableObject
Untuk proyek yang sudah ada dan menggunakan ObservableObject, migrasi ke @Observable menyederhanakan kode:
// ❌ 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
@Publishedpada setiap properti @Statemenggantikan@StateObjectuntuk pembuatan- Observasi granular otomatis
- Kode yang lebih mudah dibaca dan dipelihara
Aturan praktis pengambilan keputusan
Panduan keputusan untuk memilih alat yang tepat:
/*
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 { /* ... */ }
}Kesimpulan
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
@Stateuntuk tipe nilai dan state UI sementara - ✅ Gunakan
@Observableuntuk kelas dengan data bisnis - ✅ Lebih utamakan
@Observableketika 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
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Tag
Bagikan
Artikel terkait

Performa SwiftUI: Mengoptimalkan LazyVStack dan Daftar Kompleks
Teknik optimasi untuk LazyVStack dan daftar SwiftUI. Mengurangi konsumsi memori, meningkatkan performa scroll, dan menghindari kesalahan umum.

ViewModifier kustom di SwiftUI: pola yang dapat digunakan kembali untuk Design System
Bangun ViewModifier kustom di SwiftUI untuk design system yang konsisten. Pola, praktik terbaik, dan contoh praktis untuk menstilisasi view iOS secara efisien.

SwiftUI: Membangun Antarmuka Modern untuk iOS
Panduan lengkap membangun antarmuka modern dengan SwiftUI: sintaks deklaratif, komponen, animasi, dan praktik terbaik untuk iOS 18.