TekinAli ⎇ main

Ekranlar arası veri akışı: UIKit'te injection'dan SwiftUI'da @Observable'a

UIKit view controller'ları arasında veriyi singleton kullanmadan ileri ve geri taşımak, SwiftUI'da @Observable, @State, @Environment ve @Bindable ile state paylaşmak, değer tabanlı navigasyon ve Xcode 27'de @State başlangıç değerlerinde neyin değiştiği.

Bir uygulamaya ikinci ekran eklendiği anda verinin bir ekrandan diğerine geçmesi gerekiyor: bir liste detay ekranına bir sipariş veriyor, bir editör sonucu geri gönderiyor, birkaç ekran aynı sepeti gösteriyor. Verinin sahibi belli değilse belirtiler de tahmin edilebilir: kimse set etmediği için crash eden implicitly unwrapped property'ler, üst ve alt ekran arasında retain cycle'lar, güncellenmeyen ya da modelini yanlış zamanda yeniden oluşturan SwiftUI view'ları.

Özünde bu bir sahiplik ve dependency injection meselesi. Bu yazıda önce UIKit için önerdiğim kalıpları, sonra iOS 17'den beri SwiftUI tarafını ObservableObject dönemine göre çok daha sade hale getiren Observation framework'ünü, en sonda da @State'in başlangıç değerini değerlendirme biçimindeki 2026 değişikliği dahil tuzakları anlatıyorum.

Üç tür veri akışı

Üç ayrı durumu birbirinden ayırmak işe yarıyor:

  1. İleri: A ekranında bir Order var, B ekranının onu göstermesi gerekiyor.
  2. Geri: B ekranı bir şeyi düzenliyor ve A'nın sonucu alması gerekiyor.
  3. Paylaşılan: Birkaç ekran aynı state'i gösteriyor ve değiştiriyor, örneğin bir alışveriş sepeti.

Üçünü de aynı araçla, genelde bir singleton ya da NotificationCenter ile çözmek cazip geliyor. İkisi de çalışır, ikisi de altı ay sonra kodun takibini zorlaştırır. Her durum için daha uygun bir kalıp var.

UIKit: initializer'lar, closure'lar ve delegate'ler

İleri: initializer üzerinden injection

En temiz seçenek, derleyicinin kontrol edebildiği seçenek: bir let property ve bir initializer.

final class OrderDetailViewController: UIViewController {
    private let order: Order

    init(order: Order) {
        self.order = order
        super.init(nibName: nil, bundle: nil)
    }

    // Storyboard versiyonu, instantiateViewController(identifier:creator:) ile kullanılır
    init?(coder: NSCoder, order: Order) {
        self.order = order
        super.init(coder: coder)
    }

    required init?(coder: NSCoder) {
        fatalError("init(order:) ya da init(coder:order:) kullanın")
    }
}

Ekran bir storyboard'daysa, sonradan doldurulan implicitly unwrapped bir var order: Order!'a artık ihtiyacınız yok. iOS 13'ten beri UIStoryboard sizin initializer'ınızı çağırabiliyor:

let storyboard = UIStoryboard(name: "Main", bundle: nil)
let detail = storyboard.instantiateViewController(identifier: "OrderDetail") { coder in
    OrderDetailViewController(coder: coder, order: order)
}
navigationController?.pushViewController(detail, animated: true)

prepare(for:sender:) hâlâ çalışıyor ve segue ağırlıklı eski kodlarda gayet yeterli. Sorunu şu: property'yi nesne oluştuktan sonra dolduruyor, bu yüzden hedef ekranın aslında hiç olmaması gereken "boş" bir durumu ele alması gerekiyor. Yeni kodda kullanmamanızı öneririm.

Geri: closure mı, delegate mi

"Kullanıcı yeni bir başlık kaydetti" gibi tek bir sonuç için en az törenli yol closure:

let editor = EditTitleViewController(initialTitle: order.title)
editor.onSave = { [weak self] newTitle in
    self?.rename(order, to: newTitle)
}
present(UINavigationController(rootViewController: editor), animated: true)

Alt ekran birbiriyle ilişkili birkaç olay bildiriyorsa (kaydedildi, iptal edildi, silindi, doğrulama başarısız oldu) delegate protokolü daha uygun. Sözleşmeyi tek bir yerde belgeliyor, weak var delegate de sahipliği açıkça gösteriyor:

protocol EditTitleViewControllerDelegate: AnyObject {
    func editTitleViewController(_ controller: EditTitleViewController, didSave title: String)
    func editTitleViewControllerDidCancel(_ controller: EditTitleViewController)
}

İki yaklaşımın da tek bir kuralı var: alt ekran üst ekranı hayatta tutmamalı. Delegate'i weak yapın, saklanan closure'larda [weak self] kullanın.

SwiftUI'da Observation (iOS 17+)

Observation framework'ü ve @Observable macro'su iOS 17 ile geldi ve bu sahiplik sorularının çoğunu çok daha basit hale getirdi. Geçiş yaparken şu eşleştirme iyi bir başlangıç noktası:

Eskiden (iOS 13–16) Şimdi (iOS 17+)
class Model: ObservableObject + @Published var @Observable class Model, düz var ile
@StateObject var model = Model() @State var model = Model()
@ObservedObject var model: Model let model: Model ya da @Bindable var model: Model
.environmentObject(model) + @EnvironmentObject .environment(model) + @Environment(Model.self)

Küçük ama eksiksiz bir örnek: en üstte sahiplenilen ve environment üzerinden paylaşılan bir sepet.

import SwiftUI
import Observation

@Observable
final class CartItem: Identifiable {
    let id = UUID()
    var name: String
    var quantity: Int
    var note = ""

    init(name: String, quantity: Int = 1) {
        self.name = name
        self.quantity = quantity
    }
}

@Observable
final class Cart {
    var items: [CartItem] = []
    var totalQuantity: Int { items.reduce(0) { $0 + $1.quantity } }
}

@main
struct ShopApp: App {
    @State private var cart = Cart()          // sahibi burası

    var body: some Scene {
        WindowGroup {
            CartScreen()
                .environment(cart)            // iOS 17+
        }
    }
}

struct CartScreen: View {
    @Environment(Cart.self) private var cart  // iOS 17+

    var body: some View {
        NavigationStack {
            List(cart.items) { item in
                NavigationLink(value: item.id) {
                    HStack {
                        Text(item.name)
                        Spacer()
                        Text("x\(item.quantity)")
                    }
                }
            }
            .navigationDestination(for: UUID.self) { id in   // iOS 16+
                if let item = cart.items.first(where: { $0.id == id }) {
                    CartItemEditor(item: item)
                }
            }
            .toolbar { Text("\(cart.totalQuantity) ürün") }
        }
    }
}

struct CartItemEditor: View {
    @Bindable var item: CartItem               // iOS 17+, $item.quantity'yi sağlar

    var body: some View {
        Form {
            Stepper("Adet: \(item.quantity)", value: $item.quantity, in: 1...99)
            TextField("Not", text: $item.note)
        }
    }
}

Altını çizmek istediğim birkaç nokta:

  • Sadece okuyan alt view'lar wrapper istemez. Bir view @Observable bir nesneyi yalnızca okuyorsa düz bir let yeterli. SwiftUI, body'nin gerçekten okuduğu property'leri takip ediyor.
  • @Bindable sadece binding içindir. Stepper ya da TextField için $item.quantity gerektiğinde kullanın. Environment'tan gelen bir nesne için body içinde yerel olarak da oluşturabilirsiniz: @Bindable var cart = cart.
  • Değerlerle navigasyon yapın. NavigationLink(value:) ile navigationDestination(for:) birlikte kullanıldığında hedef ekran, Hashable bir değerden ihtiyaç anında oluşturuluyor. Bir ID geçirip paylaşılan modelde aramak, tek bir doğruluk kaynağı (single source of truth) bırakıyor.

Ayrıntılı takip ve iç içe nesneler

ObservableObject ile bir üst nesne yalnızca kendi @Published property'sine yeni bir değer atandığında değişiklik yayınlar. items nesnelerden oluşan bir array ise ve siz items[3].quantity'yi değiştirirseniz, array hâlâ aynı referansları tuttuğu için üst nesne hiçbir şey yayınlamaz ve liste güncellenmez. Klasik "array güncellenmiyor" problemi tam olarak bu.

Observation değişiklikleri her nesnede property bazında takip ediyor. item.quantity'yi okuyan satır o item'ın o property'sine bağımlı hale geliyor, değişince de satır güncelleniyor. İç içe değişiklikleri elle iletmenize artık gerek yok.

Tuzaklar

@State başlangıç değeri ve Xcode 27

Bu konu dikkat istiyor, çünkü davranış 2026'da değişti.

Xcode 26'ya kadar Apple'ın State dokümantasyonu, property'nin SwiftUI view'u her oluşturduğunda varsayılan değerini de oluşturduğunu söylüyor. SwiftUI storage olarak yalnızca ilk örneği tutuyor, ama üst view struct'ı yeniden oluşturduğu her seferde Model() yeniden değerlendiriliyor ve bu sık olabiliyor. @StateObject'te bu sorun hiç yoktu, çünkü başlangıç değerini autoclosure olarak alıyor. Bu yüzden @State private var model = ExpensiveModel() sinsi bir performans ve yan etki tuzağıydı.

Xcode 27 ile @State artık property wrapper değil, bir Swift macro'su. Apple'ın dokümantasyonu ve TN3211 teknik notu, başlangıç değerinin artık lazy olarak, view ilk oluşturulduğunda yalnızca bir kez değerlendirildiğini söylüyor. Davranış, derlediğiniz Xcode sürümüne bağlı. Yani hâlâ Xcode 26 kullanan bir ekip eski davranışla çalışıyor. TN3211 ayrıca kaynak uyumsuzluklarını da listeliyor. Bunlardan biri şu: bir @State property'nin inline varsayılan değeri varken init içinde de atama yaparsanız inline değer kazanıyor ve sizin atamanız yok sayılıyor.

İki toolchain'de de işe yarayan kurallarım:

  • @State başlangıç değerlerini ucuz ve yan etkisiz tutun.
  • Ağ çağrısı, dosya erişimi ya da veritabanı kurulumu gibi pahalı işleri initializer'larda değil, .task (iOS 15+) içinde yapın.
  • Model bir parametreye bağlıysa onu .task(id:) içinde oluşturun. Böylece parametre değiştiğinde gerçekten yeni bir model oluşur:
@MainActor
@Observable
final class ProfileModel {
    let userID: Int
    var name = ""
    init(userID: Int) { self.userID = userID }
    func load() async { /* profili getir */ }
}

struct ProfileScreen: View {
    let userID: Int
    @State private var model: ProfileModel?

    var body: some View {
        Group {
            if let model { ProfileContent(model: model) } else { ProgressView() }
        }
        .task(id: userID) {
            let model = ProfileModel(userID: userID)
            self.model = model
            await model.load()
        }
    }
}

Parametreye bağlı modellerde kilit nokta bu: state, view kimliği (identity) başına bir kez oluşturulur. Sonradan farklı bir parametre geçirmek onu değiştirmez. Kimliği değiştirmeniz (.id(userID) ile) ya da yukarıdaki gibi modeli kendiniz yeniden oluşturmanız gerekir.

Diğer tuzaklar

  • Sahibi olmadığınız nesne için @State. Üst view bir model geçiriyorsa, alt view onu @State ile sarmamalı. Bu, eski @StateObject ve @ObservedObject karışıklığının yeni isimlerle tekrarı.
  • Sahibi olmayan environment. Optional olmayan @Environment(Cart.self), bir üst view'un .environment(cart) çağırdığını varsayar. Çağıran yoksa bunu derleme zamanında değil, çalışma zamanında öğrenirsiniz. Bu genelde preview'larda başınıza gelir. Nesnenin olmaması meşru bir durumsa property'yi Cart? olarak tanımlayın. Apple, optional versiyonun nil döndürdüğünü belgeliyor.
  • İki dünyayı karıştırmak. @State içinde saklanan bir ObservableObject, view'u yalnızca referans değiştiğinde günceller, @Published property'leri değiştiğinde değil. Bir modeli taşıyacaksanız tamamen taşıyın.

Özet

  1. UIKit ileri yön: initializer injection. Storyboard için instantiateViewController(identifier:creator:) (iOS 13+).
  2. UIKit geri yön: tek sonuç için closure, çok olay için weak delegate.
  3. SwiftUI, iOS 17+: sahibinde @Observable + @State, paylaşmak için .environment / @Environment(Type.self), sadece binding gereken yerde @Bindable.
  4. NavigationStack ve navigationDestination(for:) (iOS 16+) ile değerlerle navigasyon yapın.
  5. @State başlangıç değerlerini ucuz tutun, ağır kurulumu .task ya da .task(id:) içine taşıyın ve Xcode 27'nin bu değerin ne zaman değerlendirildiğini değiştirdiğini unutmayın.

Veriyi ekranlar arasında taşımak büyük ölçüde verinin sahibinin kim olduğu meselesi. O netleştiğinde doğru API genelde kendiliğinden belli oluyor.

tekinali.db · salt okunur · ● bağlı

SQLite 3.53.3 · 8 tablo, 2 view.

Soldaki bir tabloya tıkla, aşağıdan bir sorgu seç ya da kendin yaz. Yardım: \?

↑↓ seç · Enter aç · Esc kapat