32.Gün - SwiftUI Animasyonlar: Implict, Explicit, Binding
Table of Contents
Bu bölümde yeni bir projeye başlayacağız ve bu proje ile birlikte SwiftUI’de animasyonları inceleyeceğiz. Animasyonlar sayesinde kullanıcı ara yüzünün daha iyi görünmesini sağlamakla beraber daha iyi bir kullanıcı deneyimi elde edeceğiz.
SwiftUI Implict Animation #
SwiftUI’de en basit animasyon türü implict animation’dır. View’lara önceden “birisi sizi canlandırmak isterse, işte nasıl yanıt vermeniz gerektiği” deriz. SwiftUI daha sonra meydana gelen tüm değişikliklerin talep ettiğimiz animasyonu takip etmesini sağlayacaktır.
Bir örnekle başlayalım. Aşağıdaki kod dairesel action’u bulunmayan basit bir butonu gösterir.
Button("Tap Me") {
// do nothing
}
.padding(50)
.background(.red)
.foregroundStyle(.white)
.clipShape(.circle)
İstediğimiz şey, bu butonun her dokunulduğunda büyümesidir ve bunu scaleEffect()
adlı yeni bir modifier ile yapabiliriz. Buna 0’dan büyük bir değer verdiğinizde buton o boyutta çizilecektir. 1.0 değeri %100’e yani butonun normal boyutuna eşdeğerdir.
Butona her dokunulduğunda scaleEffect değerini değiştirmek istediğimiz için, bir Double
saklayacak bir @State
property kullanmamız gerekir. Aşağıdaki property’yi ekleyelim;
@State private var animationAmount = 1.0
Şimdi bu modifier’ı ekleyerek butonun efekti kullanmasını sağlayabiliriz.
.scaleEffect(animationAmount)
Son olarak, butona dokunulduğunda animasyon miktarını 1 arttırmak istiyoruz, bu nedenle butonun eylemi için bunu kullanalım;
animationAmount += 1
Bu kodu çalıştırırsanız, butona tekrar tekrar dokunarak büyütebileceğinizi göreceksiniz. Giderek artan yüksek çözünülüklerde yeniden çizilmeyecektir, bu nedenle buton büyüdükçe biraz bulanıklaştığını göreceksiniz.
İnsan gözü harekete karşı son derece duyarlıdır. Nesnelerin hareket ettiğini ya da görünümlerinin değiştiğini algılamakta son derece iyiyizdir, bu da animasyonu hem çok önemli hem de çok hoş yapan şeydir. Bu nedenle, SwiftUI’den değişikliklerimiz için implict animation oluşturmasını isteyebiliriz, böylece butona bir animation()
modifier ekleyerek tüm ölçeklendirme sorunsuz bir şekilde gerçekleşmesini sağlayabiliriz;
.animation(.default, value: animationAmount)
animationAmount
değeri her değiştiğinde SwiftUI’den varsayılan bir animasyon uygulamasını ister bu sayede butona her dokunduğunuzda animasyonu göreceksiniz.
Bu implict animation view’ın değişen tüm özellikleri üzerinde etkili olur, yani view’a daha fazla animasyon modifier eklersek hepsi birlikte değişecektir. Örneğin, butona ikinci bir modifier ekleyebiliriz, blur()
özel bir yarıçapa sahip bir Gauss bulanıklığı eklememizi sağlar. Aşağıdakini animation()
modifier’ından önce ekleyin.
.blur(radius: (animationAmount - 1) * 3)
(animationAmount - 1) * 3
, bulanıklık yarıçapının 0’dan başlayacağı (bulanıklık yok), ancak butona dokunduğumuzda 3 noktaya, 6 noktaya, 9 noktaya ve ötesine geçeceği anlamına gelir.
Uygulamayı tekrar çalıştırırsanız, artık sorunsuz bir şekilde ölçeklendiğini ve bulanıklaştığını göreceksiniz.
Mesele şu ki, animasyonun her bir karesinin neye benzemesi gerektiğini hiçbir yerde söylemedik ve SwiftUI’nin animasyonu ne zaman başlatıp bitirmesi gerektiğini bile söylemedik. Bu SwiftUI’deki view’lerin kendi state’lerinin fonksiyonu olması gibidir.
SwiftUI’da Animasyonları Özelleştirme #
Bir view’a animation()
modifier eklediğimizde SwiftUI, izlediğimiz değer değiştiğinde varsayılan sistem animasyonu neyse onu kullanarak bu view’da meydana gelen tüm değişiklikleri otomatik olarak canlandıracaktır. Pratikte, bu çok yumuşak bir spring animasyondur, yani iOS animasyonu yavaş başlatacak, arından hedef değeri hafifçe aşana kadar hızlanmasını sağlayacak, ardından son durumuna ulaşana kadar geriye doğru giderek sona erecektir.
Modifier’a farklı değerler girerek kullanılan animasyon türünü kontrol edebiliriz. Örneğin, animasyonun başlangıçtan bitişe kadar sabit bir hızda hareket etmesini sağlamak için .linear
değerini kullanabiliriz.
.animation(.linear, value: animationAmount)
İpucu : Implict animasyonların her zaman belirli bir değeri izlemesi gerekir, aksi takdirde animasyonlar her küçük değişiklik için tetiklenir. Örneğin cihazı dikeyden yataya döndürmek bile animasyonu tetikler ve bu da garip görünürdü.
iOS varsayılan olarak spring animasyonlarını seçer çünkü bunlar gerçek dünyada alışkın olduğumuz şeyleri taklit eder. Bunlar son derece özelleştirilebilir. Spring ile kabaca ne kadar sürede tamamlanması gerektiğini ve ayrıca yayın ne kadar zıplaması gerektiğini kontrol edebiliriz.
Örneğin, bu butonun hızlı bir şekilde ölçeklenmesini ve ardından çok fazla zıplamasını sağlar;
.animation(.spring(duration: 1, bounce: 0.9), value: animationAmount)
Daha hassas kontrol için, animasyonu saniye sayısı olarak belirtilen bir süre ile özelleştirebiliriz. Böylece, aşağıdaki gibi iki saniye süren bir kolay giriş-çıkış animasyonu elde edebiliriz;
struct ContentView: View {
@State private var animationAmount = 1.0
var body: some View {
Button("Tap Me") {
animationAmount += 1
}
.padding(50)
.background(.red)
.foregroundStyle(.white)
.clipShape(.circle)
.scaleEffect(animationAmount)
.animation(.easeInOut(duration: 2), value: animationAmount)
}
}
.easeInOut(duration: 2)
dediğimizde, aslında kendi modifier’ları olan bir animasyon yapısının instance’ını oluşturuyoruz. Böylece, bunun gibi bir gecikme eklemek için modifier’ları doğrudan animasyona ekleyebiliriz.
.animation(
.easeInOut(duration: 2)
.delay(1),
value: animationAmount
)
Bunu yaptıktan sonra, butona dokunduğunuzda iki saniyelik bir animasyon gerçekleştirmeden önce bir saniye bekleyeceksiniz.
Ayrıca animasyonun belirli sayıda tekrar etmesini isteyebilir ve hatta autoreverses
değerini true olarak ayarlayarak ileri ve geri zıplamasını sağlayabiliriz. Bu, son boyutuna ulaşmadan önce yukarı ve aşağı zıplayacak bir saniyelik bir animasyon oluşturur;
.animation(
.easeInOut(duration: 1)
.repeatCount(3, autoreverses: true),
value: animationAmount
)
Tekrarlama sayısını 2 olarak ayarlamış olsaydık, buton önce yukarı sonra aşağı ölçeklenir, ardından hemen daha büyük ölçeğe geri atlardı. Bunun nedeni, hangi animasyonları uygularsak uygulayalım, sonuçta düğmenin programımızın durumuyla eşeleşmesi gerektiğidir.Yani animasyon bittiğinde buton animationAmount
’ta ayarlanan değere sahip olmalıdır.
Sürekli animasyonlar için şu şekilde kullanılabilen bir repeatForever()
modifier vardır;
.animation(
.easeInOut(duration: 1)
.repeatForever(autoreverses: true),
value: animationAmount
)
Bu repeatForever()
animasyonlarını onAppear()
ile birlikte kullanarak hemen başlayan ve view’ın ömrü boyunca devam eden animasyonlar oluşturabiliriz.
Bunu göstermek için, animasyonu (.scaleEffect(animationAmount)
)butonun kendisinden kaldıracağız ve bunun yerine butonun etrafından bir tür titreşimli daire oluşturmak için bir overlay kullanacağız. Overlay, kapladığımız view’la aynı boyutta ve konumda yeni view’lar oluşturmamızı sağlayan overlay()
modifier kullanılarak oluşturulur.
Bu sebeple, ilk olarak bu overlay()
modifier’ını animation()
modifier’ından önce butona ekleyelim.
.overlay(
Circle()
.stroke(.red)
.scaleEffect(animationAmount)
.opacity(2 - animationAmount)
)
Bu, 2 - animationAmount
opaklık değeri kullanarak butonumuz üzerinde konturlu kırmızı bir daire oluşturur, böylece animationAmount
1 olduğunda opaklık 1 (opak) ve animationAmount
2 olduğunda opaklık 0 (şeffaf) olur.
Ardından, butondan scaleEffect()
’i bir önceki adımda kaldırmıştık, buton closure içindeki animationAmount += 1
kısmınıda kaldırın. Ayrıca animation
modifier’ını da overlay
içine taşıyın;
.overlay(
Circle()
.stroke(.red)
.scaleEffect(animationAmount)
.opacity(2 - animationAmount)
.animation(
.easeOut(duration: 1)
.repeatForever(autoreverses: false),
value: animationAmount
)
)
Yukarıdaki kodda autoreverses
’ı false olarak değiştirdiğimize dikkat edin.
Son olarak, butona animationAmount
değerini 2 olarak ayarlayacak bir onAppear()
modifier ekleyin.
.onAppear {
animationAmount = 2
}
Overlay circle’ın autoreversing olmadan reperat forever animasyonunu kullandığı için, overlay circle ‘ın sürekli olarak büyüdüğünü ve solduğunu göreceksiniz.
Bitmiş kodunuz aşağıdaki gibi görünmelidir.
Button("Tap Me") {
// animationAmount += 1
}
.padding(50)
.background(.red)
.foregroundStyle(.white)
.clipShape(.circle)
.overlay(
Circle()
.stroke(.red)
.scaleEffect(animationAmount)
.opacity(2 - animationAmount)
.animation(
.easeInOut(duration: 1)
.repeatForever(autoreverses: false),
value: animationAmount
)
)
.onAppear {
animationAmount = 2
}
Animating Bindings #
animation()
modifier herhangi bir SwiftUI binding’e uygulanabilir, bu da veri güncellemeleri ile tetiklenen animasyonlara sahip olmamızı sağlar.
Bunu en iyi kod ile anlayabiliriz. Bir VStack
, bir Stepper
ve Button
içeren bir view aşağıdaki kod ile oluşturulmuştur.
struct ContentView: View {
@State private var animationAmount = 1.0
var body: some View {
VStack {
Stepper("Scale amount", value: $animationAmount.animation(), in: 1...10)
Spacer()
Button("Tap Me") {
animationAmount += 1
}
.padding(40)
.background(.red)
.foregroundStyle(.white)
.clipShape(.circle)
.scaleEffect(animationAmount)
}
}
}
Gördüğünüz gibi, stepper animationAmount
’u yukarı ve aşağı hareket ettirebilir ve düğmeye dokunarak ona 1 ekler. Her ikisi de aynı veriye bağlıdır ve bu da butonun boyutunun değişmesine neden olur. Bununla birlikte, butona dokunmak animationCount
’u hemen değiştirir, bu nedenle buton daha büyük boyuta atlar. Buna karşılık, stepper $animationAmount.animation()
’a bağlıdır, bu da SwiftUI’nin değişikliklerini otomatik olarak canlandıracağı anlamına gelir.
Şimdi bir deney olarak, body
başlangıcını şu şekilde değiştirelim;
var body: some View {
print(animationAmount)
return VStack {
Burada bazı view dışı kodlarımız olduğundan, Swift’in hangi kısmın geri gönderilen view olduğunu anlaması için VStack
’ten önce return eklememiz gerekir. Ancak print(animationAmount)
eklemek önemlidir ve nedenini görmek için programı tekrar çalıştıralım ve stepper’ın değerini değiştirelim.
Görmeniz gereken şey, 2.0, 3.0 4.0 vb. yazdırdığıdır. Aynı zamanda, buton sorunsuz bir şekilde yukarı ve aşağı ölçekleniyor, doğrudan 2,3,4 değerlerine atlamayarak bu işlemi animasyon ile gerçekleştiriyor. Aslında olan şey, SwiftUI’nin binding değişmeden önce view’ın state’ini incelemesi, binding değiştikten sonra view’ın hedef state’ini incelemesi ve ardından A noktasından B noktasına ulaşmak için bir animasyon uygulamasıdır.
Bu yüzden bir Boolean değişimini animate edebiliyoruz. Swift bir şekilde false ve true arasında yeni değerler icat etmiyor, sadece değişimin sonucu olarak ortaya çıkan view değişikliklerini animate ediyor.
Bu binding animasyonları, view’larda kullandığımız benzer bir animation()
modifier kullanır. Bu sebeple aynı animation modifier’ları kullanabiliriz.
Stepper("Scale amount", value: $animationAmount.animation(
.easeInOut(duration: 1)
.repeatCount(3, autoreverses: true)
), in: 1...10)
İpucu : animation()
modifier’ının bu çeşidi ile, değişiklikleri izlemek için hangi değeri izlediğimizi belirtmemiz gerekmez. Tam olarak izlemesi gereken değere eklenir.
Bu binding animasyonları ile animasyonu bir view üzerinde ayarlamak ve onu bir state değişikliği ile implict olarak animate etmek yerine, view üzerinde hiçbir şey ayarlamıyoruz ve onu bir state değişikliği ile animate ediyoruz.
SwiftUI Explict Animasyonlar #
SwiftUI’de bir view’a animation()
modifier ekleyerek implict animasyon oluşturmayı ve bir binding’e animation()
modifier ekleyerek binding animation oluşturmayı gördük. Ancak animasyonlar oluşturabileceğimiz üçüncü bir yol var: SwiftUI’den bir state değişikliğinin sonucu olarak meydana gelen değişiklikleri animate etmesini isteyebiliriz.
Bu hala animasyonun her karesini elle oluşturduğumuz anlamına gelmiyor. Bu hala SwiftUI’nin işi ve state değişikliği uygulanmadan önce ve sonra view’ın state’ine bakarak animasyonu buluyor.
Ancak artık bir state değişikliği meydana geldiğinde bir animasyon gerçekleşmesini istediğimizi belirtiyoruz. Bu bir binding’e bağlı değil, sadece bir state değişikliği nedeniyle belirli bir animasyonun gerçekleşmesini istiyoruz.
Bunu göstermek için tekrar basit bir buton örneğine geri dönelim.
struct ContentView: View {
var body: some View {
Button("Tap Me") {
// do nothing
}
.padding(50)
.background(.red)
.foregroundStyle(.white)
.clipShape(.circle)
}
}
Bu butona dokunulduğunda, 3D efektiyle etrafında dönmesini sağlayacağız. Bunun için başka bir modifier olan rotation3DEffect()
gerekir. Bu modifier’a derece cinsinden bir dönüş miktarı ve view’ın nasıl döneceğini belirleyen bir eksen verilebilir. Bu ekseni view’ın içinden geçen bir şiş gibi düşünün;
- Şişi View’ı X ekseni boyunca (yatay olarak) geçirirsek, ileri ve geri dönebilecektir.
- Şişi View’ı Y ekseni boyunca (dikey olarak) geçirirsek, sağa ve sola dönebilecektir.
- Şişi View’ı Z ekseni boyunca (derinlik) geçirirsek, kendi eksninde sağa ve sola dönebilecektir.
Bu animasyonu yapmak için değiştirebileceğimiz bir state gerekir ve dönüş dereceleri Double
olarak belirtilir. Bu yüzden bu property’yi ekleyelim;
@State private var animationAmount = 0.0
Daha sonra, butondan Y ekseni boyunca animationAmount
derece dönmesini isteyeceğiz, yani sola ve sağa dönecek. Bu modifier’ı butona ekleyelim;
.rotation3DEffect(.degrees(animationAmount), axis: (x: 0, y: 1, z: 0))
Şimdi önemli kısama gelelim: butonun action’a bazı kodlar ekleyeceğiz, böylece her dokunulduğunda animationAmount
değerine 360 ekleyecek.
Eğer sadece animationAmount += 360
yazarsak, butona bağlı bir animasyon modifier olmadığı için değişiklik hemen gerçekleşecektir. İşte burada explict animasyonlar devreye girer. withAnimation()
fonksiyonunu kullanırsak SwiftUI, yeni durumdan kaynaklanan tüm değişikliklerin otomatik olarak canlandırılmasını sağlayacaktır.
Buton action’a aşağıdaki kodları yazalım;
withAnimation {
animationAmount += 360
}
Butona her dokunduğumuzda 3D uzayda dönüyor ve yazması çok kolaydı. Bunu farklı eksenler için de deneyebilirsiniz.
withAnimation()
fonksiyonuna bir animasyon parametresi verilebilir ve SwiftUI’nin başka yerlerinde kullanabileceğimiz tüm animasyonlar kullanılabilir. Örneğin, aşağıdaki gibi bir withAnimation()
çağrısı kullanarak dönme efektimizin bir spring animasyonu kullanmasını sağlayabiliriz.
withAnimation(.spring(duration: 1, bounce: 0.5)) {
animationAmount += 360
}
Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.