İçeriğe geç / Skip to content / Zum Inhalt
Ahmet Balaman LogoAhmet Balaman

SwiftUI @State, @Binding ve @Observable: Veri Akışı Rehberi

Ahmet Balaman

6 dk okuma

SwiftSwiftUIStateBindingObservableiOS
SwiftUI @State, @Binding ve @Observable: Veri Akışı Rehberi

SwiftUI'da ekran, verinin bir fonksiyonudur: veri değişir, body yeniden hesaplanır, ekran güncellenir. Bu yüzden asıl soru "ekranı nasıl güncellerim" değil, "bu verinin sahibi kim" sorusudur. @State, @Binding, @Observable, @Bindable ve @Environment bu soruya verilen beş farklı cevaptır. Yanlış olanı seçtiğinizde derleyici çoğu zaman susar, ama ekran kendi kendine sıfırlanan alanlarla ya da güncellenmeyen yazılarla cevap verir.

Bu yazıda önce güncel yaklaşımı (Observation framework), sonra hâlâ pek çok eğitimde ve projede karşınıza çıkan ObservableObject ailesini, ikisi arasındaki birebir eşleşmeyi ve en sık yapılan hataları anlatıyorum. View yapısına yeni başlıyorsanız önce SwiftUI ile ilk uygulama yazısına göz atmak işinizi kolaylaştırır.

@State: View'ın Kendi Verisi

@State, yalnızca o view'ı ilgilendiren küçük değerler içindir: bir sayaç, bir anahtarın açık olup olmadığı, bir metin alanının taslağı. View bir struct olduğu ve her güncellemede yeniden oluşturulduğu için değeri struct'ın içinde saklayamazsınız; @State değeri SwiftUI'ın kendi deposuna taşır ve view yeniden oluşturulsa bile korur.

import SwiftUI

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack(spacing: 12) {
            Text("Sayaç: \(count)")
            Button("Artır") { count += 1 }
        }
    }
}

private yazmak bir alışkanlık olmalı. @State bir verinin sahibi olduğunuzu söyler; dışarıdan atanabilen bir @State neredeyse her zaman yanlış araç seçildiğini gösterir.

@Binding: Sahibi Başkası Olan Veriyi Değiştirmek

Alt view bir değeri hem okuyacak hem değiştirecek, ama değerin sahibi üst view ise @Binding kullanılır. Binding bir kopya değil, sahibindeki değere açılan iki yönlü bir bağlantıdır. Üst view'da $ öneki ile üretilir.

struct NotificationToggle: View {
    @Binding var isOn: Bool

    var body: some View {
        Toggle("Bildirimler", isOn: $isOn)
    }
}

struct SettingsView: View {
    @State private var notificationsEnabled = true

    var body: some View {
        Form {
            NotificationToggle(isOn: $notificationsEnabled)
            Text(notificationsEnabled ? "Açık" : "Kapalı")
        }
    }
}

Alt view değeri yalnızca gösterecekse binding'e gerek yok; düz bir let parametresi yeterli. Binding'i "yazma izni" olarak düşünün ve yalnızca gerekene verin.

@Observable: Birden Fazla Ekranın Paylaştığı Model

Veri tek bir değerden büyüdüğünde, örneğin bir sepet, bir oturum ya da bir liste olduğunda, onu bir sınıfa taşırsınız. Observation framework'ü ile bunun için sınıfın başına @Observable yazmak yeterli. iOS 17 ve sonrasında kullanılabilir.

import Observation

struct CartItem: Identifiable {
    let id = UUID()
    var name: String
    var price: Double
    var quantity = 1
}

@Observable
final class CartModel {
    var items: [CartItem] = []
    var note = ""

    var total: Double {
        items.reduce(0) { $0 + $1.price * Double($1.quantity) }
    }

    func add(_ item: CartItem) {
        items.append(item)
    }
}

Burada @Published yok. Makro, saklanan her özelliği izlenebilir hâle getiriyor ve SwiftUI bir view'ın body içinde gerçekten okuduğu özellikleri kaydediyor. note değiştiğinde yalnızca note okuyan view'lar yeniden çizilir. Modelin neden struct değil class olduğunu merak ediyorsanız struct ve class farkı yazısında ayrıntısı var: burada aynı nesneyi birden fazla ekranın paylaşması gerekiyor, yani referans semantiği istiyoruz.

Modeli oluşturan view onu @State ile tutar. Modeli dışarıdan alan view'da ise hiçbir sarmalayıcıya gerek yoktur:

struct CartScreen: View {
    @State private var cart = CartModel()

    var body: some View {
        List {
            ForEach(cart.items) { item in
                Text(item.name)
            }
            CartNoteField(cart: cart)
            CartTotalRow(cart: cart)
        }
    }
}

struct CartTotalRow: View {
    let cart: CartModel

    var body: some View {
        Text("Toplam: \(cart.total, format: .number.precision(.fractionLength(2)))")
    }
}

@Bindable: Modelin Özelliğine Binding Üretmek

TextField ya da Toggle bir Binding ister. @Observable bir modelin özelliğinden $ ile binding üretebilmek için modeli @Bindable ile işaretlersiniz:

struct CartNoteField: View {
    @Bindable var cart: CartModel

    var body: some View {
        TextField("Sipariş notu", text: $cart.note)
    }
}

@Environment: Modeli Ağaçtan Aşağı Taşımak

Modeli beş katman boyunca parametre olarak geçirmek yerine ortama koyabilirsiniz. Kökte .environment(_:) ile verilir, ihtiyaç duyan view tipiyle okur:

@main
struct ShopApp: App {
    @State private var cart = CartModel()

    var body: some Scene {
        WindowGroup {
            CartBadge()
                .environment(cart)
        }
    }
}

struct CartBadge: View {
    @Environment(CartModel.self) private var cart

    var body: some View {
        Text("Sepette \(cart.items.count) ürün")
    }
}

Ortamdan okunan bir modele binding gerekiyorsa body içinde yerel bir @Bindable tanımlanır:

struct CheckoutNote: View {
    @Environment(CartModel.self) private var cart

    var body: some View {
        @Bindable var cart = cart
        TextField("Sipariş notu", text: $cart.note)
    }
}

Eski Yaklaşım: ObservableObject Ailesi

iOS 17 öncesini destekleyen projelerde ve eski eğitimlerde Combine tabanlı yapı karşınıza çıkar. Mantık aynıdır, isimler farklıdır:

final class LegacyCartModel: ObservableObject {
    @Published var items: [CartItem] = []
    @Published var note = ""
}

struct LegacyCartScreen: View {
    @StateObject private var cart = LegacyCartModel()   // sahibi

    var body: some View {
        LegacyCartSummary(cart: cart)
            .environmentObject(cart)
    }
}

struct LegacyCartSummary: View {
    @ObservedObject var cart: LegacyCartModel            // sahibi değil

    var body: some View {
        Text("Sepette \(cart.items.count) ürün")
    }
}

struct LegacyCartBadge: View {
    @EnvironmentObject private var cart: LegacyCartModel

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

İki önemli fark var. Birincisi, ObservableObject tarafında herhangi bir @Published özellik değiştiğinde nesneyi gözleyen bütün view'lar geçersiz sayılır; Observation ise özellik bazında izler. İkincisi, eski tarafta sahiplik ile gözlem ayrı sarmalayıcılardır (@StateObject ve @ObservedObject) ve bunları karıştırmak aşağıda anlatacağım klasik hatayı üretir.

Durum Observation (iOS 17+) Eski yaklaşım
View'a özel basit değer @State @State
Alt view üstün değerini değiştirecek @Binding @Binding
View modeli oluşturuyor ve sahibi @State + @Observable sınıf @StateObject
View modeli dışarıdan alıyor, okuyor düz let / var @ObservedObject
Dışarıdan gelen modele $ ile binding @Bindable @ObservedObject
Uygulama genelinde paylaşılan model .environment(_:) + @Environment(Tip.self) .environmentObject(_:) + @EnvironmentObject
Modelde izlenen özellik ek işaret gerekmez @Published

Ne Zaman Kullanılır, Ne Zaman Kullanılmaz?

Yeni bir projede ve minimum hedefiniz iOS 17 ya da üstüyse Observation ile başlayın; daha az kod, daha az gereksiz yeniden çizim. Daha eski bir sürümü desteklemek zorundaysanız ObservableObject hâlâ tam olarak çalışır ve ikisi aynı projede yan yana durabilir, yani geçişi ekran ekran yapabilirsiniz.

Her şeyi modele taşımak da bir hatadır. Bir sheet'in açık olup olmadığı, bir alanın odak durumu ya da bir animasyon bayrağı view'a aittir ve @State olarak kalmalıdır. Tersi de geçerli: aynı veriyi iki ekran gösteriyorsa ikisinde ayrı @State tutmak yerine tek bir sahibe taşıyın. @Environment'ı ise gerçekten yaygın kullanılan modeller için ayırın; her şeyi ortama koymak bağımlılıkları görünmez kılar ve ortama konmamış bir modeli okumaya çalışan view çalışma anında çöker.

Sık Yapılan Hatalar

1. @ObservedObject ile nesne oluşturmak

struct BadProfileView: View {
    @ObservedObject var model = LegacyCartModel()   // yanlış
    var body: some View { Text(model.note) }
}

Belirti: Üst view her yeniden çizildiğinde model sıfırdan oluşur; girilen veri kaybolur, ağ isteği tekrar atılır. @ObservedObject nesneyi saklamaz, yalnızca gözler. Çözüm: Nesneyi oluşturan view'da @StateObject, Observation kullanıyorsanız @State.

2. Üstten gelen değeri @State'e kopyalamak

struct NameEditorBad: View {
    @State private var name: String

    init(name: String) {
        _name = State(initialValue: name)
    }

    var body: some View {
        TextField("Ad", text: $name)
    }
}

Belirti: Üst view'daki isim değişir ama alan eski değeri göstermeye devam eder; alanda yapılan değişiklik de üste ulaşmaz. @State başlangıç değerini yalnızca view ilk kez ekrana geldiğinde kullanır. Çözüm: Veri üstünse @Binding var name: String kullanın ve $name geçirin.

3. State'i ömrü kısa olan view'da tutmak

Bir if dalının içindeki view koşul yanlış olduğunda ağaçtan çıkar ve @State'i de onunla birlikte silinir. Belirti: Detay bölümünü kapatıp açtığınızda yazdığınız not boş gelir. Aynı şey .id(...) değeri değişen view'larda da olur. Çözüm: Korunması gereken değeri, ekranda kalıcı olan üst view'a taşıyın ve aşağıya binding olarak verin.

4. @Bindable'ı unutmak

struct CartNoteFieldBad: View {
    let cart: CartModel
    var body: some View {
        TextField("Not", text: $cart.note)   // hata
    }
}

Belirti: Derleyici cannot find '$cart' in scope hatası verir. Çözüm: let cart yerine @Bindable var cart. Bir de küçük not: @State private var model = Model() ifadesinde Model() view struct'ı her oluşturulduğunda çalışır, SwiftUI yalnızca ilkini saklar. Bu yüzden modelin init'inde ağır iş yapmayın; yüklemeyi .task içine alın. Bunun nasıl yapılacağını async/await rehberinde gösteriyorum.

Mini Senaryo: Görev Listesi

Beş aracın hepsini tek ekranda görelim. Görevler uygulama genelinde paylaşılan bir TodoStore içinde; liste ekranı onu ortamdan okuyor, ekleme sheet'i ise kendi taslağını @State ile tutuyor.

struct TodoItem: Identifiable {
    let id = UUID()
    var title: String
    var isDone = false
}

@Observable
final class TodoStore {
    var items: [TodoItem] = []

    var remainingCount: Int {
        items.filter { !$0.isDone }.count
    }

    func add(title: String) {
        let trimmed = title.trimmingCharacters(in: .whitespaces)
        guard !trimmed.isEmpty else { return }
        items.append(TodoItem(title: trimmed))
    }
}

struct TodoListScreen: View {
    @Environment(TodoStore.self) private var store
    @State private var isAdding = false

    var body: some View {
        @Bindable var store = store

        NavigationStack {
            List($store.items) { $item in
                Toggle(item.title, isOn: $item.isDone)
            }
            .navigationTitle("Kalan: \(store.remainingCount)")
            .toolbar {
                Button("Ekle") { isAdding = true }
            }
            .sheet(isPresented: $isAdding) {
                AddTodoSheet(isPresented: $isAdding)
            }
        }
    }
}

struct AddTodoSheet: View {
    @Environment(TodoStore.self) private var store
    @Binding var isPresented: Bool
    @State private var draft = ""

    var body: some View {
        Form {
            TextField("Görev", text: $draft)
            Button("Kaydet") {
                store.add(title: draft)
                isPresented = false
            }
            .disabled(draft.isEmpty)
        }
    }
}

Kim neyin sahibi? isAdding liste ekranının, draft sheet'in, görevler ise TodoStore'un. Sheet, isPresented binding'i sayesinde kendini kapatabiliyor ama o değerin sahibi değil. List($store.items) her satıra o öğenin binding'ini veriyor; Toggle değiştiğinde remainingCount okuyan başlık da kendiliğinden güncelleniyor. add içindeki guard kullanımı size yabancıysa optionals ve guard let yazısına bakabilirsiniz. Uygulamanın kökünde .environment(TodoStore()) yerine, @State ile tutulan tek bir örneği ortama vermeyi unutmayın.

Sık Sorulan Sorular

@State ile @Binding arasındaki fark nedir?

@State verinin sahibidir ve değeri SwiftUI'ın deposunda saklar. @Binding hiçbir şey saklamaz; başka bir view'ın sahip olduğu değere okuma ve yazma erişimi verir.

@Observable varken ObservableObject öğrenmeme gerek var mı?

Var, çünkü mevcut projelerin ve eğitimlerin büyük kısmı hâlâ onu kullanıyor ve iOS 17 öncesini destekleyen uygulamalarda tek seçenek o. Yeni kodda Observation tercih edin, eskisini okuyup anlayabilecek kadar bilin.

@StateObject yerine @State kullanabilir miyim?

Yalnızca model @Observable makrosuyla işaretliyse. ObservableObject protokolüne uyan bir sınıfı @State ile tutarsanız özellik değişiklikleri view'ı güncellemez; o sınıflar için @StateObject gerekir.

View'ım neden güncellenmiyor?

En sık üç sebep: model @Observable ya da @Published ile işaretli değil, view değeri bir kopyadan okuyor (üstten gelen değer @State'e kopyalanmış), ya da değişiklik body içinde hiç okunmayan bir özellikte oluyor.

Yorumlar