Soru geçmişi
Swift 5.9'dan önce, reaktif programlama SwiftUI'de ObservableObject protokolü ile Combine çerçevesinin birleşimini kullanıyordu. Geliştiriciler, yayıncılar sentezlemek için özellikleri @Published ile manuel olarak işaretliyordu ya da mutasyonlar hakkında görünümlere bildirimde bulunmak için objectWillChange.send() çağrısı yapıyordu. Bu desen, kaba granaul güncellemeler sorunuyla karşı karşıyaydı; herhangi bir özellik değişikliği, tam görünüm gövdesinin yeniden değerlendirilmesine neden oluyordu ve referans anlamlarını zorunlu kıldığı için, karmaşık görünüm modelleri için yapıların kullanılmasını engelliyordu. Observation çerçevesi, açık yayıncı bildirimleri olmaksızın ince-granül otomatik reaktivite sağlamak için tanıtıldı.
Sorun
Temel zorluk, açık bir şablon olmaksızın özellik erişimini ve mutasyonunu tespit etmekti, mantık güvenliği ve yüksek performans sağlanıyordu. Geleneksel çözümler, ya stringly-typed ve kırılgan olan manuel anahtar-değer gözlemeyi (KVO) ya da alan modelini karıştıran sarıcı türlerini gerektiriyordu. Sistem, bağımlılıkları dinamik olarak kaydetmek için okuma ve yazma işlemlerini kesmek zorundaydı, ancak ObservableObject'in referans sayımlı kimlik yayılımının mimari kısıtlamaları olmadan ve yöntem değiştirme sırasında çalışma zamanı aşırı yükü olmadan.
Çözüm
Swift, akran, üye ve erişimci makro yeteneklerini birleştiren @Observable makrosunu kullanır. Derleme sırasında, makro, işaretlenmiş sınıfı özel bir ObservationRegistrar örneği ekleyerek dönüştürür. Ardından, her saklanan özelliği, okumaları _$observationRegistrar.track(self, keyPath: ...) ve yazmaları willSet/didSet bildirimleriyle saran hesaplanan erişimciler olarak yeniden yazar. Bu genişletme, otomatik olarak Observable protokolüne uyum sağlamakta ve gerekli observationRegistrar hesaplanan özelliğini uygulamaktadır. SwiftUI, görünüm gövdesi değerlendirmesi sırasında bu kaydedici ile entegre olur ve yalnızca gerçekten erişilen özellikler için kendini gözlemci olarak kaydeder, böylece manuel Combine ayarına gerek kalmadan ince güncellemeleri elde eder.
@Observable class AyarlarGörünümModeli { var karanlıkModAçık = false var bildirimSayısı = 0 } // Kavramsal derleyici genişletmesi: class AyarlarGörünümModeli { private let _$observationRegistrar = ObservationRegistrar() var karanlıkModAçık: Bool { get { _$observationRegistrar.track(self, keyPath: \.karanlıkModAçık) return _karanlıkModAçık } set { _$observationRegistrar.willSet(self, keyPath: \.karanlıkModAçık) let eskiDeğer = _karanlıkModAçık _karanlıkModAçık = yeniDeğer _$observationRegistrar.didSet(self, keyPath: \.karanlıkModAçık, eskiDeğer: eskiDeğer) } } private var _karanlıkModAçık = false // ... bildirimSayısı için benzer desen }
Canlı hisse fiyatları, kullanıcı portföy toplamları ve haber akışları gösteren gerçek zamanlı bir finansal gösterge SwiftUI uygulaması tasarlıyorsunuz. Görünüm modeli, Boolean UI bayraklarından karmaşık grafik veri yapıları arasında otuz ayrı özelliğe sahip.
Başlangıçta, ekip her özellik için @Published sargılı ObservableObject kullanarak bunu uyguladı. Bu, ciddi performans düşüşüne neden oldu: tek bir hisse fiyatı güncellendiğinde, tüm gösterge yeniden hesaplandı çünkü ObservableObject, "bir şey değişti" bildiriminde bulunarak tam nesnede güncellemeler yapıyordu ve granülarite eksikliği vardı. Kod ayrıca oldukça uzundu; her seferinde tekrar eden @Published var beyanları ve aboneliklerde bellek sızıntılarını önlemek için manuel AnyCancellable depolaması gerektiriyordu.
Ekip, performans ve şablon sorunlarını çözmek için üç mimari yaklaşımı değerlendirdi.
İlk yaklaşım, manuel Combine optimizasyonuna dayanıyordu. Kritik özellikler için ayrı PassthroughSubject örnekleri oluşturacaklar ve belirli güncellemeleri .onReceive kullanarak abone olacaklardı. Avantajı, hangi UI bileşenlerinin yenileneceği üzerinde hassas bir kontrol sağlamaktı. Ancak dezavantajı, otuz konunun otuz aboneliği gerektirmesi ve Set<AnyCancellable> ile hataya açıktı, bu da kod tabanını sürdürülemez hale getirdi.
İkinci yaklaşım, SwiftUI'nin @State'ini değer tipi görünüm modelleri ile kullanmayı öneriyordu. Görünüm modelini değiştirilemez bir değer olarak ele alacak ve her mutasyonda onu değiştireceklerdi. Avantajı, doğal değer anlamları ve gereksiz güncellemeleri önleyen otomatik eşitlik kontrolleriydi. Dezavantajı, referans kimliğinin kaybıydı; her mutasyon yeni bir örnek oluşturdu, bu da ScrollView pozisyonunun geri yüklenmesini kırdı ve kimlik kaybı nedeniyle karmaşık nesne koordinasyonunu imkansız hale getirdi.
Üçüncü yaklaşım, @Observable makrosunu benimsemeyi seçti. Sınıfı @Observable ile işaretleyerek ve tüm @Published özelliklerini kaldırarak, derleyici otomatik olarak özellikleri ObservationRegistrar kullanacak şekilde dönüştürdü. Avantajı, sözdiziminin temiz kalması ve basit var beyanlarının yanı sıra, SwiftUI'nin otomatik olarak her görünümün gövdesinde hangi özelliklerin erişildiğini takip etmesiydi; sadece belirli alt görünümlerin güncellenmesi sağlandı. Dezavantajı, Swift 5.9'a geçiş yapma gereği ve ekibi makro hata ayıklama tekniklerinde eğitme ihtiyacıydı.
Ekip, 200 satırlık Combine abonelik kodunu kaldırmanın yanı sıra granülarite sorununu çözme avantajından dolayı üçüncü yaklaşımı seçti. Yüksek frekanslı fiyat güncellemeleri sırasında CPU kullanımında %40'lık bir azalma gözlemlediler. Sonuç, bireysel hisse fiyatı etiketlerinin, sabit haber akışı bölümü için düzen yeniden hesaplanmadan bağımsız olarak güncellendiği yanıt veren bir göstergeydi.
Neden Observation çerçevesi gözlemlenen türün bir sınıf (referans anlamları) olmasını gerektiriyor ve @Observable'un bir yapıya uygulanması durumunda hangi derleme hatası oluşuyor?
@Observable makrosu, bir saklanan özellik olarak bir ObservationRegistrar enjekte ederek ve Observable protokolünü uygulayarak genişler. Yapılar, kopyalama üzerine yazma anlamı olan değer türlerdir; her mutasyon kavramsal olarak yeni bir örnek oluşturur ve farklı bir kimlik ile sonuçlanır. ObservationRegistrar iç durumu sürdürülmelidir - gözlemci listeleri ve izleme kapsamları - gözlem grafiğini sürdürmek için mutasyonlar arasında kalmalıdır. Eğer bir yapıya uygulanırsa, mutasyonlar kaydedici durumunu yanlış bir şekilde kopyalar ve gözlemciler ile örnek arasındaki bağı bozabilir. Derleyici, makronun gereken saklanan özelliği bir değer türüne ekleyemeyeceğini belirten bir hata üreterek bunu önler; daha spesifik olarak, sonuç türü, gerekli referans istikrarı eksik olduğu için Observable'a uyum gösteremez.
Observation çerçevesi, iç içe gözlemlenebilir nesneleri nasıl ele alır ve neden @Published ile olduğu gibi bireysel özellikler için bir projeksiyon değeri yoktur?**
Bir @Observable sınıfında, kendisi de bir @Observable sınıfı olan bir özellik varsa, çerçeve, erişimi özellik düzeyinde takip eder; iç içe nesneleri otomatik olarak gözlemlemez. outer.inner.name erişimi, dış nesnenin inner özelliğine bir bağımlılık kaydeder. Eğer inner örneği tamamen değiştirilirse, gözlemciler bildirilir. Ancak, inner.name değişiklikleri, dış nesnenin gözlemcilerine bildirilmez, dış nesne içindekini açık bir şekilde takip etmedikçe. Combine'dan farklı olarak, Observation için bireysel özellikler için bir projeksiyon değeri kavramı yoktur, çünkü çerçeve, yayıncı akışları yerine kaydedici aracılığıyla doğrudan özellik takibi kullanır. SwiftUI'de Observation için $ sözdizimi, tam örneğe uygulandığında @Bindable ile kullanılır (örneğin, @Bindable var viewModel: AyarlarGörünümModeli $viewModel.karanlıkModAçık sağlar), bireysel özellik beyanlarında değil.
Observation çerçevesi, hangi belirli iş parçacığı güvenliği garantileri sunar ve Swift eşzamanlılık aktörleri ile nasıl etkileşime girer?**
ObservationRegistrar kendiliğinden iş parçacığı güvenli değildir; gözlemlenen nesneye seri erişim varsayar. Bir @Observable sınıfı bir aktöre (örneğin @MainActor gibi) izole edildiğinde, tüm mutasyonlar ve gözlemler otomatik olarak o aktörün bağlamında gerçekleşir, veri yarışlarını önler. Çerçeve, gözlemcinin izole etme alanına saygılı olacak şekilde gözlem geri çağırmalarının geçerli olduğunu garanti eder. Kritik bir uygulama detayı, izleme mekanizmasının, görünüm gövdesi yürütülmesi sırasında geçerli gözlem kapsamını sürdürmek için TaskLocal depolamasını kullanmasıdır. Bu, gözlem kaydının, yalnızca kaydedildiği belirli asenkron işlem sırasında aktif olmasını sağlar.