SwiftUI'da @Observable makrosu, iOS 17 ile tanıtılan Observation framework'ünün temel yapı taşıdır ve @ObservableObject + @Published ikilisinin yerini alarak model sınıflarınızı SwiftUI'ya çok daha az kod ve çok daha yüksek performansla bağlar. Bu rehberde makronun nasıl çalıştığını, @Bindable ile birlikte kullanımını, iPad, Mac Catalyst ve visionOS'ta davranışının nasıl farklılaştığını ve @ObservedObject'ten geçiş sırasında karşılaşacağınız tuzakları göstereceğim. Kod örneklerinin hepsi Xcode 16 ve iOS 17+ üzerinde denendi; bazıları geçen ay ürüne alınan bir müşteri projesinden birebir alındı.
@Observable makrosu, bir sınıfı otomatik olarak Observable protokolüne uyumlu hale getirir; @Published ve ObservableObject'e artık ihtiyaç yoktur.
SwiftUI, view'in yalnızca gerçekten okuduğu özellikleri izler; @ObservableObject'in aksine tüm nesne değişikliklerinde render yeniden çalışmaz.
@Bindable, iki yönlü bağlamayı ($model.name) sağlar; @Observable modellerde @ObservedObject yerine kullanılır.
Observation framework iOS 17+, macOS 14+, watchOS 10+, tvOS 17+ ve visionOS 1+ üzerinde çalışır; alt sürümlerde geriye dönük polyfill yoktur.
Migrasyonun anahtarı: sınıfları final class olarak tutmak ve computed property'lerde access/withMutation'a dikkat etmek.
Observation framework nedir?
Observation framework, Apple'ın 2023 WWDC'sinde iOS 17 ile birlikte tanıttığı, Swift 5.9'un makro sistemi üzerine kurulu bir gözlemleme (observation) altyapısıdır. Combine'ın ObservableObject protokolüne kıyasla daha sade bir API sunar; @Published gibi property wrapper'lara ihtiyaç duymaz ve Swift'in tip sistemine derin şekilde entegre olur.
Temelde şunu yapar: model sınıfınıza @Observable makrosunu koyduğunuzda, derleyici sınıfın her stored property'sine getter/setter enjekte eder ve ObservationRegistrar aracılığıyla erişimleri kaydeder. SwiftUI, view'in body çalışırken hangi property'leri okuduğunu otomatik tespit eder; sadece bunlar değişince re-render tetiklenir. Bu, büyük view hiyerarşilerinde neredeyse ücretsiz bir performans kazancı sağlar.
Framework yalnızca SwiftUI için değil. withObservationTracking(_:onChange:) API'siyle SwiftUI dışında da (örneğin bir UIKit view controller içinden bir modelin değişimini gözlemlemek için) kullanabilirsiniz. Bu, UIKit + SwiftUI karışık projeler için özellikle önemli, çünkü modeli iki dünya arasında tek bir gerçek kaynak olarak tutmayı mümkün kılıyor.
@Observable makrosu nasıl çalışır?
@Observable bir "attached macro"dur; class deklarasyonuna bağlanır ve derleme zamanında kod üretir. Xcode 16'da, makro ile işaretlenmiş bir sınıfa sağ tıklayıp Expand Macro seçerseniz, üretilen kodu görebilirsiniz.
import Observation
import SwiftUI
@Observable
final class CounterModel {
var count: Int = 0
var lastUpdated: Date = .now
func increment() {
count += 1
lastUpdated = .now
}
}
Makro genişletildiğinde, kabaca şuna benzer bir yapı ortaya çıkar:
final class CounterModel: Observable {
@ObservationIgnored
private let _$observationRegistrar = ObservationRegistrar()
private var _count: Int = 0
var count: Int {
get {
access(keyPath: \.count)
return _count
}
set {
withMutation(keyPath: \.count) {
_count = newValue
}
}
}
// ...aynı desen lastUpdated için tekrar eder
}
Her okumada access(keyPath:) çağrılıp property "izlenebilir" olarak kaydedilir; her yazımda withMutation(keyPath:) ile abonelere haber verilir. SwiftUI bu registrar'ı yakalar ve view'i sadece gerçekten okuduğu property'lerin değişimlerine bağlar. Bir view model.count'u okumadıysa, count değişse bile o view yeniden çizilmez; bu davranış @ObservableObject ile mümkün değildi.
@Observable ve @ObservedObject farkı nedir?
En sık sorulan soru bu ve cevabı üç boyutta ayırmak gerekiyor: gerekli protokol, property-level izleme ve SwiftUI entegrasyonu.
Özellik
@ObservableObject (eski)
@Observable (yeni)
Protokol
ObservableObject (Combine)
Observable (Observation)
Property işareti
@Published gerekir
Hiçbir işaret gerekmez
İzleme granülerliği
Nesne düzeyi (herhangi bir @Published değişince tüm view yeniden çizilir)
Property düzeyi (yalnızca okunan property değişince yeniden çizilir)
View'da wrapper
@ObservedObject, @StateObject, @EnvironmentObject
Doğrudan let/var, @State (sahiplenmek için), @Environment, @Bindable
Minimum sürüm
iOS 13+
iOS 17+, macOS 14+, watchOS 10+
UIKit entegrasyonu
Combine sink
withObservationTracking
Performans
Tüm view alt ağacını yeniden hesaplayabilir
Etkilenmeyen view'lar dokunulmaz kalır
Pratikte en büyük fark performansta. Bir liste ekranı düşünün: 500 satır, her satırın kendi verisi var. @ObservableObject ile modelin bir property'sindeki değişim, tüm liste view'inin diff'ini tetikler. @Observable ile ise sadece o satırı gösteren ForEach item'ı invalide olur. Ölçtüğüm birkaç projede, ağır listelerde re-render sayısı %40 ile %80 arasında düştü.
@Bindable ne için kullanılır?
@Bindable, @Observable modellerinizde iki yönlü bağlama (two-way binding) oluşturmanın modern yolu. Yani bir TextField'a $model.name geçirmek istediğinizde. @ObservableObject dünyasında bunu @ObservedObject veya @StateObject halletiyordu; @Observable'da model artık property wrapper gerektirmediği için ayrı bir mekanizmaya ihtiyaç doğdu.
import SwiftUI
struct ProfileEditView: View {
// Model üst view'den gelir; sahiplik burada değil.
@Bindable var user: UserModel
var body: some View {
Form {
TextField("Ad", text: $user.name)
TextField("E-posta", text: $user.email)
Toggle("Bildirimler", isOn: $user.notificationsEnabled)
}
}
}
@Bindable'ın kritik özelliği: sahiplik iddiasında bulunmaz. View, modeli sadece ödünç alır; yaşam döngüsünü başka bir view yönetir (çoğunlukla üstteki bir @State). Bu, @StateObject vs @ObservedObject arasında gidip gelen o eski karar akışını büyük ölçüde ortadan kaldırır.
@State, @Environment ve @Observable birlikte kullanımı
Sık kafa karıştıran bir konu: hangi wrapper'ı nerede kullanmalı? Observation framework, üç ana desene indirger:
Sahiplik: modeli oluşturan view'da @State private var model = ...
Bağımlılık enjeksiyonu: alt view'lara doğrudan let model: MyModel veya @Bindable var model: MyModel
Global paylaşım: .environment(model) ile aşağıya gönderin, alt view'da @Environment(MyModel.self) private var model
@main
struct MyApp: App {
@State private var session = AppSession()
var body: some Scene {
WindowGroup {
RootView()
.environment(session)
}
}
}
struct SettingsView: View {
@Environment(AppSession.self) private var session
var body: some View {
// İki yönlü bağlama gerekirse:
@Bindable var session = session
Toggle("Karanlık mod", isOn: $session.darkMode)
}
}
Dikkat: @Environment(AppSession.self) çağrısı, environment'ta böyle bir tip yoksa çalışma zamanında hata verir. Optional istiyorsanız @Environment(AppSession.self) private var session: AppSession? şeklinde tanımlayın. Bu, önizleme (Preview) senaryolarında güvenlik ağı gibi çalışır.
SwiftUI'nın veri akışı desenlerini derinlemesine incelemek isterseniz, SwiftUI NavigationStack ve router deseni rehberimizde Observation framework'ün navigation state ile nasıl birleştirildiğini örneklerle gösterdim.
@ObservableObject'ten @Observable'a geçiş
Mevcut bir kod tabanınız varsa, aşağıdaki 5 adımlı geçiş rutini büyük çoğunluğunda çalışır. Adım adım gidin, her adımda derleyin ve test edin.
Adım 1: Sınıf tanımını dönüştürün
// Önce
final class CartViewModel: ObservableObject {
@Published var items: [CartItem] = []
@Published var isLoading = false
}
// Sonra
import Observation
@Observable
final class CartViewModel {
var items: [CartItem] = []
var isLoading = false
}
Adım 2: View'lardaki wrapper'ları güncelleyin
@ObservedObject → doğrudan let veya @Bindable var. @StateObject → @State. @EnvironmentObject → @Environment(Type.self).
// Önce
struct CartView: View {
@StateObject private var vm = CartViewModel()
var body: some View { ... }
}
// Sonra
struct CartView: View {
@State private var vm = CartViewModel()
var body: some View { ... }
}
Adım 3: environment enjeksiyonunu güncelleyin
// Önce
ContentView().environmentObject(session)
// Sonra
ContentView().environment(session)
Adım 4: Combine yayınlarını yeniden düşünün
@Published verdiği $property Combine Publisher'ları artık yok. Eğer bir yerde bir property'nin değişimini reactive olarak dinliyorsanız, iki seçeneğiniz var: (1) withObservationTracking ile manuel dinleyin, veya (2) o property'yi hedef sınıfta korumak için @ObservationIgnored ile bir CurrentValueSubject yaratın.
func observeCount(_ model: CounterModel) {
withObservationTracking {
_ = model.count
} onChange: {
print("Count değişti, yeniden çağır!")
Task { @MainActor in
self.observeCount(model) // Bir sonraki değişim için yeniden kaydol
}
}
}
withObservationTrackingtek seferliktir. onChange bir kez tetiklenir, sonra yeniden kaydolmanız gerekir. SwiftUI'nın view yeniden çizilirken bunu otomatik yapmasının nedeni de bu.
Adım 5: Concurrency uyarılarını çözün
Swift 6 strict concurrency açıksa, @Observable sınıfınızı bir aktöre bağlamanız gerekebilir. Çoğu SwiftUI modeli için @MainActor doğru cevaptır:
@Observable
@MainActor
final class CartViewModel {
var items: [CartItem] = []
// ...
}
@Observable'ın en büyük vaadi property düzeyinde izleme, ama bunu yanlış kullanmak da mümkün. Aşağıda ölçtüğüm ve gördüğüm en yaygın performans desenleri var.
Fazla iç içe geçmiş computed property'ler
Bir computed property'nin dönüş değeri hangi stored property'lere bağlıysa, view onların hepsini gözlemler. Bu iyidir, ama koleksiyon üzerinde reduce, filter gibi ağır işlemler yapıyorsanız, her stored property değişiminde bu değer yeniden hesaplanır. Ölçtüğünüzde darboğaz burasıysa, sonucu bir stored property'ye cache'leyin ve mutasyon anında güncelleyin. (Ben son bir projede tam bu tuzağa düştüm, alışveriş sepeti toplamı her keystroke'ta 12000 kez hesaplanıyordu.)
ForEach ve Identifiable
Property-level izleme, ForEach içinde her satırın yalnızca kendi item'ını gözlemlemesi anlamına gelir. Ama bunun için satır view'inin doğrudan modeli parametre olarak alması ve stabil bir id'ye sahip olması şart. ForEach(model.items) { item in RowView(item: item) } deseni doğrudur; ForEach(0..<model.items.count, id: \.self) yanlıştır çünkü index'ler kayabilir.
Instruments ile ölçün
Xcode 16'da SwiftUI Instruments template'i, hangi view'in ne sıklıkta ve hangi nedenden dolayı re-render edildiğini gösterir. @ObservableObject'ten geçtikten sonra Body Updates sayısındaki düşüşü buradan doğrulamanızı şiddetle tavsiye ederim. Gözle "hızlandı" hissini takip etmek yanıltıcı olabiliyor.
iPad, Mac Catalyst ve visionOS farkları
Observation framework, API seviyesinde tüm platformlarda aynıdır. Ancak SwiftUI'nın izlemeyi ne zaman devreye soktuğu ufak farklılıklar gösteriyor:
iPad (iPadOS 17+): Multitasking (Split View, Slide Over) sırasında SwiftUI, arka planda kalan sahne için re-render'ları erteler. @Observable modelleriniz güncellenmeye devam eder, sadece view yeniden çizilmez. Bu davranış Combine'a göre değişmedi.
Mac Catalyst: AppKit köprüsü nedeniyle bazı view'ler NSHostingView içinde koşar. Property-level izleme çalışır, ama bir NSViewController içinden UIKit-style bir SwiftUI görünümünü barındırıyorsanız, host controller değişmedikçe UI güncellemesi anlık olmayabilir. invalidateIntrinsicContentSize ile zorlamanız gerekebilir.
visionOS 1+: RealityView içinde tutulan @Observable modeller özel bir vaka. RealityKit entity'leri SwiftUI'nın izleme mekanizmasına otomatik bağlanmaz; el ile update closure'unu tetiklemeniz gerekir. Bunu @Observable içinde tutulan bir "version" sayacıyla yapmak yaygın bir workaround.
watchOS 10+: TabView + karmaşık @Observable modelleri, dial navigation ile sayfa değiştirirken beklenmedik view invalidasyonlarına yol açabilir. Watch performans bütçesi çok dar; property-level granularity burada özellikle değerlidir.
Kod tabanına @Observable getirdikten sonra sık gördüğüm 6 tuzak var. Hepsini gerçek projelerde debug ederken öğrendim.
1) Struct'a @Observable koymak derlenmez
@Observable yalnızca sınıflara uygulanır. Bir struct'a koymayı denerseniz derleme hatası alırsınız. Modelinizin referans semantiğine ihtiyacı yoksa, klasik @State private var value = MyStruct() deseni değişmeden çalışmaya devam eder.
2) @Bindable'ı yanlış yerde @State ile karıştırmak
Bir view hem sahip olup hem bind etmek isterse, @State deyip iç kısımda @Bindable var model = model shadowing'ini kullanın. Bu Xcode'un komple desteklediği bir desen ve WWDC örneklerinde de kullanılır.
3) Optional environment'ta crash
@Environment(MyModel.self) private var model: environment'ta yoksa çalışma zamanı hatasıyla crash eder. Testler ve Preview'lar için @Environment(MyModel.self) private var model: MyModel? tercih edin.
4) @ObservationIgnored'ı unutmak
Modelde bir URLSession, cache veya cancellation token tutuyorsanız, bunlar izlemeye dahil olmamalı. @ObservationIgnored private var cancellables = Set<AnyCancellable>() demeyi unutmayın; aksi halde her değişimde SwiftUI gereksiz yere bilgilendirilir.
5) SwiftData ile karışıklık
SwiftData'nın @Model makrosu içeriden @Observable kullanır. Yani @Model işaretli sınıflarınız zaten Observable. İkisini birlikte koymayın, çakışırlar. Bu konuda daha derin bir inceleme için SwiftData @Model ve @Query kalıcılık rehberimize göz atın.
6) Preview'da environment eksik
Bir view @Environment(SessionModel.self) bekliyorsa, Preview'da .environment(SessionModel()) vermelisiniz. Yoksa crash gelir. Bunu bir #Preview içinde her seferinde tekrarlamamak için ortak bir PreviewData yardımcısı yazmak işe yarıyor.
Sıkça Sorulan Sorular
@Observable hangi iOS sürümünde çalışır?
@Observable makrosu ve Observation framework iOS 17, iPadOS 17, macOS 14, watchOS 10, tvOS 17 ve visionOS 1'den itibaren kullanılabilir. Alt sürümler için resmi bir backport yoktur; iOS 16 ve altını desteklemeniz gerekiyorsa @ObservableObject + @Published dünyasında kalmalısınız.
@Observable ile @StateObject birlikte kullanılabilir mi?
Hayır, birlikte kullanılmaz. @StateObject yalnızca ObservableObject protokolüne uyan tipler içindir. @Observable modelini sahiplenmek için @State kullanın: @State private var model = MyModel(). Bu, @StateObject'in @Observable karşılığıdır.
@Observable performansı @ObservableObject'e göre gerçekten daha mı iyi?
Evet, özellikle büyük view hiyerarşilerinde. @ObservableObject nesne düzeyinde bir objectWillChange yayınlar; bir property değişince tüm bağımlı view'lar yeniden çizilir. @Observable ise property düzeyinde izler; sadece okunan property değişince view invalide olur. Instruments ile ölçtüğüm listelerde re-render sayısı %40 ile %80 arasında düşüyor.
@Observable class'ı UIKit'te nasıl gözlemlerim?
withObservationTracking(_:onChange:) API'siyle. Bu fonksiyon tek seferlik bir gözlem kaydeder; onChange tetiklendikten sonra yeniden çağırmanız gerekir. Sürekli bir stream için, onChange içinde kendinizi yeniden kaydolan bir fonksiyona sarın ve Task { @MainActor in } içinde çağırın.
@Bindable ne zaman gerekli değildir?
Eğer view yalnızca modelin değerlerini okuyorsa ve Binding gerektiren bir kontrol (TextField, Toggle, Slider vb.) kullanmıyorsa @Bindable'a ihtiyaç yoktur. Sıradan bir let model: MyModel yeterlidir; SwiftUI izlemeyi otomatik olarak yapar.
SwiftUI NavigationStack ve NavigationPath ile tip güvenli navigasyon: Router deseni, derin bağlantı işleme, cold-launch durum geri yükleme ve VoiceOver erişilebilirliği için 2026 rehberi.
Swift Testing, XCTest'in yerini alan modern framework. @Test makrosu, #expect kontrolleri, paralel çalıştırma ve @Suite ile Swift 6 ve Xcode 16'da birim testlerin nasıl yazıldığını, XCTest'ten kademeli geçiş örnekleriyle görün.