SwiftUI @Observable Rehberi 2026: Observation Framework, @Bindable ve Performans

@Observable makrosunu, Observation framework'ünü, @Bindable ile iki yönlü bağlamayı ve @ObservedObject'ten geçişi kod örnekleriyle anlatıyorum.

SwiftUI @Observable Rehberi (2026)

Güncelleme: 18 Temmuz 2026

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:

  1. Sahiplik: modeli oluşturan view'da @State private var model = ...
  2. Bağımlılık enjeksiyonu: alt view'lara doğrudan let model: MyModel veya @Bindable var model: MyModel
  3. 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
        }
    }
}

withObservationTracking tek 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] = []
    // ...
}

Bu konudaki ayrıntılar için Swift 6 strict concurrency ve Sendable rehberimize bakabilirsiniz; özellikle @MainActor izolasyonu ve nonisolated kaçış kapıları oradaki desene birebir oturuyor.

Performans ve render optimizasyonu

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

Bu davranışlar Apple'ın Discover Observation in SwiftUI ve Migrate your app to Swift 6 WWDC oturumlarında da işleniyor; platform farklarını orada da doğrulayabilirsiniz.

Tuzaklar ve yaygın hatalar

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.

Hiroshi Sato
Yazar Hakkında Hiroshi Sato

Apple Platforms specialist building for iOS, macOS, visionOS, and the occasional watchOS app nobody asked for.