๐ŸŒ KO

๐Ÿ‘๏ธ @Observable State Management

The new Observation framework in iOS 17. Modern state management that completely replaces ObservableObject.

โญ Difficulty: โญ โฑ๏ธ Est. Time: 1h ๐Ÿ“‚ App Frameworks

โœจ @Observable is?

Swift macro-based state management introduced in iOS 17+. Replaces @Published, @StateObject, @ObservedObject with a simpler, more performant approach.

๐Ÿ†š Before & After Comparison

Before (ObservableObject).swift
// iOS 16 ์ดํ•˜: ObservableObject
class CounterViewModel: ObservableObject {
    @Published var count = 0
    @Published var message = ""

    func increment() {
        count += 1
        message = "Count: \(count)"
    }
}

struct ContentView: View {
    @StateObject private var viewModel = CounterViewModel()

    var body: some View {
        Text("\(viewModel.count)")
    }
}
After (@Observable).swift
// iOS 17+: @Observable
import Observation

@Observable
class CounterViewModel {
    var count = 0
    var message = ""

    func increment() {
        count += 1
        message = "Count: \(count)"
    }
}

struct ContentView: View {
    var viewModel = CounterViewModel()

    var body: some View {
        Text("\(viewModel.count)")
    }
}

๐ŸŽฏ Basic Usage

TodoViewModel.swift
import Observation
import Foundation

@Observable
class TodoViewModel {
    var todos: [Todo] = []
    var isLoading = false
    var errorMessage: String?

    func addTodo(_ title: String) {
        let todo = Todo(title: title)
        todos.append(todo)
    }

    func loadTodos() async {
        isLoading = true
        defer { isLoading = false }

        do {
            // API ํ˜ธ์ถœ
            try await Task.sleep(for: .seconds(1))
            todos = [Todo(title: "์ƒ˜ํ”Œ ํ•  ์ผ")]
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

struct Todo: Identifiable {
    let id = UUID()
    var title: String
}

๐Ÿ“ฑ SwiftUI Integration

ContentView.swift
import SwiftUI

struct ContentView: View {
    var viewModel = TodoViewModel()

    var body: some View {
        NavigationStack {
            List(viewModel.todos) { todo in
                Text(todo.title)
            }
            .overlay {
                if viewModel.isLoading {
                    ProgressView("๋กœ๋”ฉ ์ค‘...")
                }
            }
            .navigationTitle("ํ•  ์ผ")
            .toolbar {
                Button("Add") {
                    viewModel.addTodo("์ƒˆ ํ•  ์ผ")
                }
            }
            .task {
                await viewModel.loadTodos()
            }
        }
    }
}

๐Ÿ”„ @State vs Regular Properties

With @Observable, you don't need @State. Regular properties are automatically observed.

StateComparison.swift
struct ContentView: View {
    // โŒ ๋ถˆํ•„์š” (iOS 17+)
    // @State private var viewModel = TodoViewModel()

    // โœ… ์ด๋ ‡๊ฒŒ๋งŒ ํ•ด๋„ Automatically ์—…๋ฐ์ดํŠธ
    var viewModel = TodoViewModel()

    var body: some View {
        Text("\(viewModel.count)")
    }
}

๐ŸŽฏ @ObservationIgnored

ํŠน์ • ํ”„๋กœํผํ‹ฐ๋ฅผ ๊ด€์ฐฐ ๋Œ€์ƒ์—์„œ ์ œ์™ธ is possible.

IgnoredProperty.swift
import Observation

@Observable
class ViewModel {
    var count = 0  // ๊ด€์ฐฐ๋จ (UI ์—…๋ฐ์ดํŠธ)

    @ObservationIgnored
    var internalCache: [String: Any] = [:]  // ๊ด€์ฐฐ ์•ˆ ๋จ (์„ฑ๋Šฅ ์ตœ์ ํ™”)

    func increment() {
        count += 1
        internalCache["lastIncrement"] = Date()  // UI ์—…๋ฐ์ดํŠธ ์•ˆ ํ•จ
    }
}

๐Ÿ”— Using in Environment

EnvironmentUsage.swift
import SwiftUI

@Observable
class AppState {
    var isLoggedIn = false
    var username = ""
}

@main
struct MyApp: App {
    @State private var appState = AppState()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(appState)
        }
    }
}

struct ContentView: View {
    @Environment(AppState.self) private var appState

    var body: some View {
        if appState.isLoggedIn {
            Text("์•ˆ๋…•ํ•˜์„ธ์š”, \(appState.username)")
        } else {
            LoginView()
        }
    }
}

๐Ÿงช Computed Property

์—ฐ์‚ฐ ํ”„๋กœํผํ‹ฐ๋„ Automatically ๊ด€์ฐฐ.

ComputedProperty.swift
@Observable
class CartViewModel {
    var items: [CartItem] = []

    // ์—ฐ์‚ฐ ํ”„๋กœํผํ‹ฐ๋„ Automatically ๊ด€์ฐฐ๋จ
    var totalPrice: Double {
        items.reduce(0) { $0 + $1.price }
    }

    var itemCount: Int {
        items.count
    }

    func addItem(_ item: CartItem) {
        items.append(item)
        // totalPrice์™€ itemCount๊ฐ€ Automatically ์—…๋ฐ์ดํŠธ๋จ
    }
}

struct CartView: View {
    var viewModel = CartViewModel()

    var body: some View {
        VStack {
            Text("์ด \(viewModel.itemCount)๊ฐœ")
            Text("ํ•ฉ๊ณ„: \(viewModel.totalPrice)์›")
        }
    }
}

โšก Performance Optimization

@Observable only observes properties actually in use.

Performance.swift
@Observable
class ViewModel {
    var name = ""
    var age = 0
    var email = ""
}

struct NameView: View {
    var viewModel: ViewModel

    var body: some View {
        Text(viewModel.name)  // name๋งŒ ๊ด€์ฐฐ, age/email ๋ณ€๊ฒฝ ์‹œ ์—…๋ฐ์ดํŠธ ์•ˆ ํ•จ
    }
}

// ObservableObject๋Š” @Published๊ฐ€ ๋ถ™์€ all ํ”„๋กœํผํ‹ฐ ๋ณ€๊ฒฝ ์‹œ ์—…๋ฐ์ดํŠธ
// @Observable์€ ์‹ค์ œ๋กœ body์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํ”„๋กœํผํ‹ฐ๋งŒ ๊ด€์ฐฐ (๋” ํšจ์œจ์ !)

๐Ÿ”„ withObservationTracking

SwiftUI ๋ฐ–์—์„œ๋„ ๋ณ€๊ฒฝ ๊ฐ์ง€๊ฐ€ is possible.

ManualTracking.swift
import Observation

@Observable
class Counter {
    var value = 0
}

let counter = Counter()

// ๋ณ€๊ฒฝ ๊ฐ์ง€
withObservationTracking {
    print("Current value: \(counter.value)")
} onChange: {
    print("๊ฐ’์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!")
}

counter.value = 10  // onChange ์‹คํ–‰๋จ

๐Ÿ“Š Migration Guide

Migration.swift
// BEFORE (iOS 13-16)
class OldViewModel: ObservableObject {
    @Published var name = ""
    @Published var age = 0
}

struct OldView: View {
    @StateObject var vm = OldViewModel()
    // or @ObservedObject var vm: OldViewModel

    var body: some View {
        Text(vm.name)
    }
}

// AFTER (iOS 17+)
@Observable
class NewViewModel {
    var name = ""
    var age = 0
}

struct NewView: View {
    var vm = NewViewModel()

    var body: some View {
        Text(vm.name)
    }
}

// ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์š”์•ฝ:
// 1. ObservableObject โ†’ @Observable
// 2. @Published ์‚ญ์ œ
// 3. @StateObject/@ObservedObject โ†’ ์ผ๋ฐ˜ ํ”„๋กœํผํ‹ฐ

๐Ÿ’ก @Observable์˜ ์žฅ์ 
โœ… ์ฝ”๋“œ๊ฐ€ 80% ๊ฐ„๊ฒฐํ•ด์ง
โœ… ์‚ฌ์šฉํ•˜๋Š” ํ”„๋กœํผํ‹ฐ๋งŒ ๊ด€์ฐฐ (์„ฑ๋Šฅ ํ–ฅ์ƒ)
โœ… No need for @Published or @StateObject
โœ… ํƒ€์ž… ์•ˆ์ „์„ฑ ํ–ฅ์ƒ
โœ… ์ปดํŒŒ์ผ ํƒ€์ž„์— ์—๋Ÿฌ ๊ฒ€์ถœ

๐Ÿ“ฆ Learning Resources

๐Ÿ’ป
GitHub Project
๐ŸŽ
Apple Official Docs
๐ŸŽฅ
WWDC23 Session

๐Ÿ“Ž Apple Official Resources

๐Ÿ“˜ Documentation ๐ŸŽฌ WWDC Sessions