iOS 17’de gelen Observation framework’ünü 2025’in başında bir projede baştan beri kullandım. ObservableObject ve @Published yerine @Observable tercih etmenin ne getirdiğini, neyi kaybettiğimi paylaşayım.
Motivasyon: gereksiz re-render
ObservableObject ile bir property değiştiğinde objectWillChange publisher tetikleniyor ve view’ın tamamı re-render oluyor. @Published 10 property’li bir ViewModel’da sadece tek property’yi kullanan view bile güncelleniyor:
class OldVM: ObservableObject {
@Published var query = ""
@Published var results: [Item] = []
@Published var isLoading = false
// ... 7 more
}
struct SearchBar: View {
@ObservedObject var vm: OldVM
var body: some View {
TextField("search", text: $vm.query)
// isLoading değişince de re-render
}
}Observation ile property bazlı tracking yapılıyor. SwiftUI hangi property okunuyorsa sadece ona abone oluyor:
@Observable class NewVM {
var query = ""
var results: [Item] = []
var isLoading = false
}
struct SearchBar: View {
let vm: NewVM // @ObservedObject yok
var body: some View {
TextField("search", text: Bindable(vm).query)
}
}Ölçtüğüm fark
Kompleks bir tablolu ekranda 50 satır var, her satırın kendi view modeli yok; tek büyük VM listeyi yönetiyor. @Published ile sadece bir satırın checkbox’ını tıkladığında 50 satır da re-render oluyordu. @Observable ile sadece ilgili satır güncelleniyor. Instruments’ta frame drop %60 azaldı.
Bindable kullanımı
@Published’in $vm.property binding syntax’ı @Observable’da @Bindable property wrapper ile geliyor:
struct EditView: View {
@Bindable var user: User
var body: some View {
TextField("name", text: $user.name)
}
}Parametre olarak alıyorsan function scope’unda ayrı Bindable yaratman lazım:
struct Cell: View {
let user: User
var body: some View {
@Bindable var bindable = user
TextField("name", text: $bindable.name)
}
}Environment ile geçiş
Eski kodda @EnvironmentObject kullanılıyordu. Observation’da daha temiz bir API var; @Environment üzerinden observable object geçiriyorsun:
@Observable class AppState { var user: User? }
@main struct MyApp: App {
@State var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView().environment(appState)
}
}
}
struct ContentView: View {
@Environment(AppState.self) var appState
var body: some View { Text(appState.user?.name ?? "") }
}Combine’la entegrasyon
@Published publisher üretiyordu; Combine pipeline kuruyordun. @Observable bunu vermiyor. Observable bir property’yi publisher’a çevirmek için manuel iş yapman lazım:
import Observation
import Combine
let subject = PassthroughSubject<String, Never>()
withObservationTracking {
_ = vm.query
} onChange: {
subject.send(vm.query)
}Bu pattern rebind gerektiriyor (her change’den sonra yeniden withObservationTracking çağırmalısın). İşi biraz daha el yordamı.
Bıraktığım alışkanlık: @StateObject vs @State
ObservableObject için @StateObject gerekiyordu (view lifecycle boyunca yaşayacak). @Observable için artık @State yeterli:
struct Screen: View {
@State private var vm = MyVM()
// eskiden @StateObject private var vm = MyVM()
}Minimum iOS 17 şartı
Observation framework iOS 17+. Daha eski destekliyorsan ObservableObject’te kalman gerekiyor veya conditional compilation ile iki kod yolu tutman lazım. Kendi tercihim: proje minimum deployment’ını iOS 17’ye çekebiliyorsam çekiyorum. iOS 16 kullanıcı oranı 2026 ortasından itibaren küçük.
Pitfall: computed property tracking’i
Observation computed property’yi tek başına track etmiyor. Computed içindeki stored property’leri takip ediyor:
@Observable class VM {
var items: [Item] = []
var activeCount: Int { items.filter { $0.isActive }.count }
}View vm.activeCount okuduğunda Observation items‘a abone oluyor. Bu genelde doğru davranış ama tracking’i debug ederken karışıklık yaratabiliyor.
Son notlar
Yeni projede @Observable varsayılanım. Mevcut projede kademeli olarak taşıyorum; aynı ekranda ObservableObject ve @Observable yan yana yaşıyor. Geçiş sırasında bug’a denk gelmedim; API yüzeyi uyumlu. Kaybettiğim tek şey Combine publisher’ları; ona da ihtiyacım zaten düşüyordu çünkü async/await’e geçtim. Net kazanç.