SwiftUI-Animation-Masterclass, iOS 17+ ve macOS 14+ üzerinde SwiftUI için hareket sunan bir Swift
paketi: fiziğe dayalı spring, decay ve sekme animasyonları, parmağın hızını koruyan sürükleme
hareketleri, particle efektleri, değer değişince oynayan efektler ve geçişler. 2.0.0 sürümü
15 Ekim 2026'da yayınlandı. Bu yazı paketin çözmeye çalıştığı sorunu, arkasındaki tasarım
kararlarını, onu şekillendiren bir hatayı ve hâlâ var olan sınırları anlatıyor.
İçindekiler
- Sorun
- Tasarım kararları ve alternatifler
- Matematik value type'larda, modifier'lar ince
- Kapalı formda çözülen spring
- Particle'lar için tek Canvas ve seed'li simülasyon
- Her efektin içinde Reduce Motion
- Bilerek dar tutulan platformlar
- Geçmişten öğrenilen: hiçbir şey yapmayan bir modifier
- Kullanım
- Sınırlar
Sorun
SwiftUI'ın hazır animasyonları bir soruyu iyi yanıtlıyor: "Bu değişikliği şu eğriyle canlandır."
Ama hareketin kendisini dışarı açmıyor. Sık karşılaşılan birkaç etkileşim tam olarak buna
ihtiyaç duyuyor:
- Bir flick nereye düşecek? Aşağıdan açılan bir panel, animasyon başlamadan önce hangi
durağa gideceğini bırakma konumuna _ve_ bırakma hızına bakarak seçmek zorunda.
- Parmağın hızı spring'e nasıl aktarılır? Hızlı bir sürüklemeden sonra spring durağan
başlarsa view gözle görülür biçimde durup yeniden hareket ediyor.
- Bir efekt nasıl test edilir? Bir sallanmayı ya da sekmeyi görmenin tek yolu uygulamayı
çalıştırıp izlemekse, onun hakkında bir test yazmak zor.
Paket, tek bir ekranda iyi bir efekt isteyen ama bunun için bir framework benimsemek istemeyen
uygulama geliştiricileri için. Sunduğu her şey bir view modifier, bir Animation, bir
Transition ya da düz bir value type; hiçbir bağımlılığı yok.
Tasarım kararları ve alternatifler
Matematik value type'larda, modifier'lar ince
DampedSpring, Decay, GravityBounce, RubberBand, SnapPoints, ParticleSystem ve
ChangeEffect.state(at:) hareketi hiçbir view kodu olmadan hesaplıyor. Modifier'lar yalnızca
environment'ı okuyor, gesture ya da animasyon durumunu tutuyor ve sonucu uyguluyor. Alternatif,
mantığı her ViewModifier'ın içine koymak, yazması daha kısa ama ekran olmadan birim testi
yapılacak bir şey bırakmıyor.
Kapalı formda çözülen spring
DampedSpring, sönümlü yay denklemini kare kare adımlamak yerine analitik çözümünü kullanıyor.
Konum zamanın bir fonksiyonu oluyor; böylece her kare, ister bir test ister bir GIF render'ı için
olsun, tek başına hesaplanabiliyor. Bedeli, az sönümlü, kritik sönümlü ve aşırı sönümlü durumları
ayrı ayrı ele almak. Bu işi doğrulamak için testler implementasyondan kopyalanmış sayılarla
karşılaştırma yapmıyor: PhysicsTests aynı yayı sayısal olarak integre edip iki sonucu
karşılaştırıyor.
Akla ilk gelen alternatif SwiftUI'ın kendi .spring(...)'i ve paket yine oraya varıyor:
DampedSpring.animation(initialVelocity:) sıradan bir Animation döndürüyor,
DampedSpring.relativeVelocity(_:from:to:) da saniyede nokta cinsinden gesture hızını
SwiftUI'ın beklediği göreli hıza çeviriyor. Ancak tek başına .spring(...) kullanmak, yerleşme
süresi gibi değerlere erişim vermiyor.
Particle'lar için tek Canvas ve seed'li simülasyon
ParticleSystem kendi zamanlayıcısı olmayan bir struct; update(by:in:) çağrıldığında ilerliyor.
ParticleView onu bir TimelineView içinden sürüyor ve bütün particle'ları tek bir Canvas'a
çiziyor; canlı particle kalmayınca saat duruyor. Rastgelelik küçük bir SplitMix64 üreticisinden
geliyor, yani bir seed particle'ları tamamen belirliyor. Testlerin ve README animasyonlarının her
çalıştırmada aynı kareleri üretmesini sağlayan bu. Pakette cihaz üzerinde yapılmış bir performans
ölçümü yok; tek Canvas bir tasarım tercihi, bir benchmark sonucu değil.
Her efektin içinde Reduce Motion
Her efekt accessibilityReduceMotion değerini environment'tan kendisi okuyor: particle'lar
çizilmiyor, değişim efektleri oynamıyor, geçişler yumuşak bir solmaya dönüyor ve sürüklemeler kısa
bir ease-out ile yerine oturuyor. Alternatif, ayarı her çağrı noktasında kontrol etmeyi
geliştiriciye bırakmak, ama bunu unutmak kolay. "Respecting Reduce Motion" adlı DocC makalesi her
efektin ne yaptığını listeliyor ve bunun nerede bittiğini de söylüyor: withAnimation ile kendi
başlattığınız animasyonlar sizin yerinize değiştirilmiyor.
Bilerek dar tutulan platformlar
Paket Swift 6.2 araçlarını, iOS 17 ve macOS 14'ü gerektiriyor ve Swift 6 dil modunda derleniyor.
visualEffect, keyframeAnimator ve iOS 17'nin Transition protokolü bu sürümlere ihtiyaç
duyuyor. CI'da derlenip test edilmeyen platformlar manifest'te yer almıyor.
Geçmişten öğrenilen: hiçbir şey yapmayan bir modifier
Repo Ağustos 2025'te açıldı. 1.0.0 sürümü (Şubat 2026) derlenemiyordu: manifest, repoda olmayan
Sources/Components ve Sources/Utilities klasörlerini gösteriyordu. Haziran 2026'da kodun büyük
kısmı kaldırıldı (15850b4, 5.991 satır silindi); geriye bir demo view ve yeni bir public API,
scrollDriven(multiplier:), kaldı.
Yeniden kurulum sırasında bu modifier'ı okumak sorunu ortaya çıkardı. d62293e itibarıyla
gövdesi şöyleydi:
swift
1public func body(content: Content) -> some View {2 // High-performance GeometryReader implementation3 content4 .overlay(5 GeometryReader { proxy in6 Color.clear.preference(key: ScrollOffsetKey.self, value: proxy.frame(in: .global).minY)7 }8 )9}multiplier hiç okunmuyordu, ölçülen frame de kimsenin izlemediği bir preference'a yazılıyordu.
API derleniyordu, dokümantasyonu vardı ve hiçbir etkisi yoktu. e08cc0a commit'i iki imzayı da
korudu ve modifier'ın adının söylediğini yapmasını sağladı:
swift
1public func body(content: Content) -> some View {2 let multiplier = reduceMotion ? 0 : multiplier3 content.visualEffect { effect, proxy in4 effect.offset(y: ScrollParallax.offset(minY: proxy.frame(in: .scrollView).minY, multiplier: multiplier))5 }6}Matematik ScrollParallax.offset(minY:multiplier:) adlı saf bir fonksiyona taşındı; fonksiyon
-minY · multiplier döndürüyor, sonlu olmayan girdide 0 veriyor ve ScrollTests bunu
doğruluyor. CHANGELOG bu değişikliği "Changed" başlığı altında listeliyor, çünkü mevcut
kullanıcılar artık view'larının hareket ettiğini görüyor.
Aynı inceleme fizik tiplerinde daha sessiz bir hata sınıfı da buldu. DampedSpring, Decay ve
GravityBounce sabitlerini initializer'da doğruluyordu, ama sabitler public var olduğu için
sonradan mass = 0 atamak kontrolü atlıyor ve her metot NaN döndürüyordu. 6a44ac5 aynı
precondition'ı her özelliğin didSet'ine ekledi. Ardından f0698d8 ikinci bir açığı kapattı:
.infinity değeri > 0 kontrolünden geçiyor ve sonsuz bir stiffness ∞/∞ üretiyordu. Kontroller
artık sonlu değer istiyor; commit mesajı, eklenen 11 exit-test beklentisinin düzeltme olmadan
hepsinin başarısız olduğunu not ediyor.
Son ayrıntı testlerin kendisiyle ilgili. Bir test drop.gravity = 2000 atayıp ardından
drop.gravity == 2000 kontrol ediyordu; bu yalnızca saklanan bir özelliğin değeri geri verdiğini
kanıtlar. 921732a bunu, 100 birimlik bir düşüşün ilk kez √(2·100/2000) saniye sonra yere
değdiğini kontrol eden bir testle değiştirdi; böylece yeni gravity değeri harekete hiç ulaşmazsa
test başarısız oluyor.
Üç durumda da ders aynı: derlenen bir public API, çalışan bir API ile aynı şey değil; bir test de
ancak davranış bozulduğunda başarısız oluyorsa işe yarıyor.
Kullanım
Paketi Swift Package Manager ile ekleyin:
swift
1dependencies: [2 .package(url: "https://github.com/muhittincamdali/SwiftUI-Animation-Masterclass.git", from: "2.0.0")3],4targets: [5 .target(name: "YourApp", dependencies: [6 .product(name: "SwiftUIAnimationMasterclass", package: "SwiftUI-Animation-Masterclass")7 ])8]Sonra herhangi bir view'a bir modifier ekleyin. Bu beğeni düğmesi her dokunuşta zıplıyor ve bir
avuç kalp fırlatıyor:
swift
1import SwiftUI2import SwiftUIAnimationMasterclass3 4struct LikeButton: View {5 @State private var likes = 06 7 var body: some View {8 Button("Like", systemImage: "heart.fill") { likes += 1 }9 .buttonStyle(.borderedProminent)10 .changeEffect(.jump(), value: likes)11 .particleBurst(.hearts, trigger: likes)12 }13}Sorun bölümündeki aşağıdan açılan panel tek bir modifier. Yavaş bir bırakış en yakın durağa
dönüyor, bir flick ise bir sonrakine devam ediyor:
swift
1struct PullUpPanel: View {2 @State private var detent = 13 4 var body: some View {5 RoundedRectangle(cornerRadius: 24)6 .fill(.blue.gradient)7 .frame(height: 400)8 .overlay(alignment: .top) {9 Capsule().fill(.white.opacity(0.6)).frame(width: 40, height: 5).padding(10)10 }11 .snapDrag(.vertical, offsets: [0, 260], selection: $detent)12 }13}İki örnek de örnek uygulamanın ve test target'ının derlediği ReadmeSamples.swift dosyasından
geliyor; bu yüzden README, bir derleme kırılmadan API'den uzaklaşamıyor. Bütün efektleri bir arada
görmek için AnimationShowcase()'i bir #Preview içine koyabilir ya da MotionGallery örnek
uygulamasını bir iPhone simülatöründe veya Mac'te çalıştırabilirsiniz.
Sınırlar
- Cihazda performans ölçümü yok. Particle sayısı 10.000 ile, simülasyon adımı 1/60 saniye ile
sınırlı; ama kare hızı ve CPU kullanımı gerçek donanımda ölçülmedi.
- Render kontrol ediliyor, snapshot testi yok. Testler view'ları
ImageRendererile çizip
kapsama ve piksel kontrolü yapıyor; referans görüntü karşılaştırması yok ve scrollDriven'ın
visualEffect bağlantısını bir UI testi doğrulamıyor.
- Çökme kontrolleri yalnızca macOS'ta. Geçersiz sabitlerin süreci durdurduğunu kanıtlayan
exit test'ler, Swift Testing'in yalnızca macOS'ta bulunan bir özelliğini kullanıyor.
- Precondition'lar release derlemesinde de çökertiyor. Sıfır kütle gibi geçersiz bir
yapılandırma uygulamayı yazıldığı yerde durduruyor; gesture hızı gibi sistemden gelen değerler
ise sınırlanıyor.
- Tek skaler hız. SwiftUI'ın spring'i tek bir başlangıç hızı alıyor; bu yüzden iki boyutlu bir
sürükleme hızını hareket yönüne izdüşürüyor.
- Dar platform aralığı. iOS 17+, macOS 14+ ve Xcode 26 ya da üstü.
2.0.0'ın test süitinde 13 suite içinde 73 Swift Testing testi var; normal bir swift test
çalıştırmasında 70'i koşuyor, README görsellerini üreten üçü ise bir çıktı klasörü verilmedikçe
atlanıyor. README'deki yol haritasında bir path boyunca ilerleyen animasyon ve aynı Transition
protokolü üzerine kurulu yeni geçişler yer alıyor.
Kaynak kod: github.com/muhittincamdali/SwiftUI-Animation-Masterclass

