Tüm yazılara dön
·4 dk okuma

SwiftUI'da durum sahipliği: görünümleri sade, veri akışını öngörülebilir tutmak

SwiftUI ile yazılan bir ekran küçükken çok şirin durur. Birkaç @State, bir liste, bir buton. Sonra ekran büyür, durum üç farklı yere dağılır, bir değer değiştiğinde yanlış görünüm yeniden çizilir ve kimse verinin asıl sahibinin kim olduğunu söyleyemez. Bu sorunların çoğu SwiftUI'ın kendisinden değil, durum sahipliğinin belirsizliğinden doğar.

Bu yazıda tek bir soruya odaklanıyorum: bu durum kime ait? Sorunun cevabı hangi property wrapper'ı kullanacağınızı, durumun nerede yaşayacağını ve hangi görünümlerin güncelleneceğini belirler.

Tek doğruluk kaynağı ilkesi

SwiftUI'da görünüm, durumun bir fonksiyonudur. Durum değişir, görünüm yeniden hesaplanır. Bu model ancak her değerin tek bir sahibi olduğunda iyi çalışır. Aynı bilgiyi iki yerde tutarsanız, ikisini senkron tutmak sizin işiniz olur ve hatalar tam burada başlar.

Pratik bir kural: bir değeri yalnızca onu değiştirmesi gereken en alttaki ortak üst görünümde veya modelde tutun. Alt görünümler ya değeri salt okunur alır ya da bir Binding ile değiştirme yetkisi alır. Kopyalamayın, yansıtmayın.

@State yalnızca yerel ve geçici durum içindir

@State, bir görünümün kendi sahip olduğu basit değerler için uygundur: bir metin alanının içeriği, bir açılır panelin açık olup olmadığı, seçili sekme gibi. Bu değerler o görünümün yaşam döngüsüne bağlıdır ve başka ekranların bilmesi gerekmez.

struct NoteEditor: View {
    @State private var draft = ""
    @State private var isConfirmingDiscard = false

    var body: some View {
        TextEditor(text: $draft)
            .toolbar {
                Button("Vazgeç") { isConfirmingDiscard = true }
            }
    }
}

@State değerlerini private yapmak iyi bir alışkanlıktır. Dışarıdan değiştirilmesi gereken bir değer varsa, o değer zaten bu görünümün durumu değildir; üst görünüme taşınmalıdır.

@Binding ile yetkiyi açıkça devretmek

Alt görünümün bir değeri değiştirmesi gerekiyorsa @Binding kullanın. Binding bir kopya değil, sahibine giden bir referanstır. Böylece veri hâlâ tek yerde durur, alt görünüm yalnızca erişim hakkı alır.

struct FavoriteButton: View {
    @Binding var isFavorite: Bool

    var body: some View {
        Button {
            isFavorite.toggle()
        } label: {
            Image(systemName: isFavorite ? "star.fill" : "star")
        }
    }
}

Bir alt görünümün yalnızca okuması yeterliyse binding vermeyin, düz değer verin. Daha az yetki, daha az sürpriz demektir. Ayrıca önizlemelerde ve testlerde düz değerle çalışan bir görünüm çok daha kolay kurulur.

Paylaşılan durum için gözlemlenebilir model

Birden fazla ekranın paylaştığı veya iş mantığı içeren durum için bir model sınıfı gerekir. Modern SwiftUI'da bunun için @Observable makrosunu kullanabilirsiniz. Bu yaklaşımın önemli bir faydası, görünümün yalnızca gövdesinde gerçekten okuduğu özelliklere bağlanmasıdır. Yani modelin başka bir özelliği değiştiğinde ilgisiz görünümler yeniden hesaplanmaz.

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

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

    func add(_ product: Product) {
        // iş kuralları burada, görünümde değil
    }
}

Burada ikinci bir ilke var: iş kurallarını görünümün içine koymayın. Toplam hesaplama, kupon doğrulama, ağ çağrısı gibi işler modelde yaşasın. Görünüm yalnızca durumu gösterir ve kullanıcı niyetini modele iletir. Bu ayrım testi de kolaylaştırır; modeli arayüz olmadan sınayabilirsiniz.

Modeli oluşturan görünümde @State ile saklayın, alt görünümlere parametre olarak geçirin. Eski ObservableObject tabanlı kodunuz varsa @StateObject ve @ObservedObject ayrımını koruyun: nesneyi yaratan sahip @StateObject, yalnızca kullanan @ObservedObject olmalıdır. Sahibi yanlış seçmek, nesnenin görünüm her yeniden oluştuğunda sıfırlanmasına yol açabilir.

Ortam nesneleri: dikkatli kullanın

@Environment ile bir modeli görünüm ağacının derinlerine kadar taşımak pratiktir, fakat bağımlılığı görünmez kılar. Bir görünüm hangi modele ihtiyaç duyduğunu imzasında söylemez; model ortamda yoksa uygulama çalışma zamanında çöker.

Bu yüzden ortamı gerçekten uygulama genelinde olan şeyler için ayırın: oturum bilgisi, tema, yerel ayar, ayarlar. Bir ekrana özgü model ise parametre olarak açıkça geçirilsin. Kural şu: bir bağımlılık görünümün temel girdisiyse imzada görünsün, çevresel bir bağlamsa ortamda yaşasın.

Yeniden çizimi ölçün, tahmin etmeyin

Durumu doğru yere koymanın bir amacı da gereksiz iş yapmamaktır. Büyük bir görünüm gövdesinde her şeyi tek yerde okursanız, küçük bir değişiklik koca ekranı yeniden hesaplatır. Çözüm genellikle görünümü mantıklı parçalara bölmektir; her parça yalnızca ihtiyaç duyduğu veriyi alır.

Şüphelendiğiniz yerleri Instruments içindeki SwiftUI araçlarıyla ölçün. let _ = Self._printChanges() gibi hata ayıklama yardımcıları, bir görünümün neden yeniden hesaplandığını görmenize yardım eder. Önce ölçün, sonra bölün; her görünümü kendiliğinden parçalamak okunabilirliği de bozabilir.

Kontrol listesi

Yeni bir ekran yazarken şu soruları sırayla sorun:

  • Bu değer yalnızca bu görünüme mi ait? O zaman @State ve private.
  • Bir alt görünüm değiştirecek mi? O zaman @Binding; yalnızca okuyacaksa düz değer.
  • Birden fazla ekran paylaşıyor veya iş kuralı içeriyor mu? O zaman modelde, @Observable ile.
  • Bağımlılık gerçekten uygulama geneli mi? O zaman ortamda; değilse parametre olarak.
  • Aynı bilgi iki yerde mi tutuluyor? Birini kaldırın.

Kapanış

SwiftUI'da çoğu karmaşıklık, durumun sahipsiz kalmasından çıkar. Her değere tek bir sahip atadığınızda hangi wrapper'ı seçeceğiniz, kodun nerede duracağı ve neyin yeniden çizileceği büyük ölçüde kendiliğinden netleşir. Önce sahipliği düşünün, wrapper'ı sonra seçin; görünümleriniz sade, veri akışınız öngörülebilir kalır.