Ana içeriğe geç
  1. 100 Günde SwiftUI Notları/

33.Gün - SwiftUI Animasyonlar: Animating Gestures, View Transitions, Custom Transition

Animasyonlar sadece güzel bir görüntü için değil aynı zamanda kullanıcıya bilgi aktarmak için kullanılabilir. SwiftUI’nin animasyon sistemi, karmaşık animasyonlar oluşturmamızı kolaylaştırır. Geçişler (transitions) kullanıcıların arayüzdeki değişiklikleri anlamalarına yardımcı olur.

Bugünkü yazımızda;

  • Animasyonları özelleştirmeyi,
  • Anlamalı animasyonlar oluşturmayı,
  • Geçişlerin (transitions) ne olduğunu ve ne işe yaradığını,
  • Birden fazla animasyonu, gesture animation ve transition öğreneceğiz.

Animation Stack’in Kontrolü #

Bu noktada, ayrı ayrı zaten anladığımız fakat birlikte başımızı biraz ağrıtabilecek iki şeyi bir araya getireceğiz.

Daha önce modifier’ların sırasının önemli olduğuna öğrenmiştik. Yani eğer şöyle bir kod yazsaydık;

Button("Tap Me") {
    // do nothing
}
.background(.blue)
.frame(width: 200, height: 200)
.foregroundStyle(.white)

SwiftUI modifier order-1

Sonuç, aşağıdaki gibi bir koddan farklı görünecektir.

Button("Tap Me") {
    // do nothing
}
.frame(width: 200, height: 200)    
.background(.blue)
.foregroundStyle(.white)

SwiftUI modifier order-2

Bunun nedeni frame’i ayarlamadan önce arka planı renklendirirsek, genişletilmiş alan yerine yalnızca orijinal alanın renklendirilmesidir. Hatırlarsanız, bunun altında yatan neden SwiftUI’nin view’ları modifierlar ile sararak aynı modifier’ı birden çok kez uygulamamıza izin vermesidir.

Bu birinci kavramdır: modifier sırası önemlidir, çünkü SwiftUI view’ları uygulandıkları sırayla modifier’larla sarar.

İkinci kavram, bir view’a bir animation() modifier uygulayarak değişiklikleri dolaylı olarak canlandırmasını sağlayabilmemizdir.

Bunu göstermek için, buton kodumuzu bazı durumlara bağlı olarak farklı renkler gösterecek şekilde değiştirebiliriz. İlk olarak state’i tanımlarız;

@State private var enabled = false

Butonun action’ı içinde bunu true ve false arasında değiştirebiliriz;

enabled.toggle()

Ardından background() modifier’ının içinde koşullu bir değer kullanarak butonun mavi veya kırmızı olmasını sağlayabiliriz;

.background(enabled ? .blue : .red)

Son olarak, bu değişiklikleri animate etmek için butona animation() modifier’ını ekliyoruz;

.animation(.default, value: enabled)

Swiftui animation order

Kodu çalıştırırsanız, butona dokunduğunuzda renginin mavi ve kırmızı arasında değiştiğini görürsünüz.

Yani, modifier sırası önemlidir ve bir modifier’ı bir view’a birkaç kez ekleyebiliriz. Ayrıca animation() modifier ile implict animasyonlar oluşturabiliriz.

animation() modifier’ını birkaç kez ekleyebiliriz ve hangi sırada kullanıldığı önemlidir.

Bunu göstermek için, diğer tüm modifier’lardan sonra bu modifier’ı butonumuza ekleyelim;

.clipShape(.rect(cornerRadius: enabled ? 60 : 0))

swiftui animation order - 2

Bu enabled Boolean’ın durumuna bağlı olarak butonun kare veya yuvarlak dikdörtgen arasında değişmesine neden olacaktır.

Programı çalıştırdığınızda butona dokunmanın kırmızı ve mavi arasında hareket etmesine neden olduğunu, ancak kare ve yuvarlak dikdörtgen arasında atladığını göreceksiniz.

clipShape() modifier’ını animasyondan önceye taşıyalım;

.frame(width: 200, height: 200)
.background(enabled ? .blue : .red)
.foregroundStyle(.white)
.clipShape(.rect(cornerRadius: enabled ? 60 : 0))
.animation(.default, value: enabled)

swiftui animation order - 3

Şimdi kodu çalıştırdığımızda hem arka plan hem de clipShape animate edilir.

Bu nedenle, animasyonları uygulama sıramız önemlidir. Yalnızca animation() modifier’ından önceki modifier’lar animate edilir.

Şimdi eğlenceli kısmı. Birden fazla animation() modifier uygularsak, her biri kendisinden önceki her şeyi bir sonraki animasyona kadar kontrol eder. Bu, state değişikliklerini tüm property’ler için aynı şekilde değil, her tür için farklı şekilde animate etmemizi sağlar.

Örneğin, renk değişiminin varsayılan animasyonla gerçekleşmesini sağlayabilir, ancak clip shape için spring kullanabiliriz;

Button("Tap Me") {
    enabled.toggle()
}
.frame(width: 200, height: 200)
.background(enabled ? .blue : .red)
.animation(.default, value: enabled)
.foregroundStyle(.white)
.clipShape(.rect(cornerRadius: enabled ? 60 : 0))
.animation(.spring(duration: 1, bounce: 0.6), value: enabled)

Swiftui 2 animation

Tasarımı oluşturmak için ihtiyaç duyduğumuz kadar animation() modifier’ına sahip olabiliriz. Bu da bir state değişikliğini ihtiyaç duyduğumuz kadar çok segmente bölmemizi sağlar.

Daha da fazla kontrol için, modifier’a nil değeri vererek animasyonları tamamen devre dışı bırakmak mümkündür. Örneğin, renk değişiminin hemen gerçekleşmesini ancak clip shape’in animasyonu korumasını isteyebiliriz, bu durumda şunu yazabiliriz.

Button("Tap Me") {
    enabled.toggle()
}
.frame(width: 200, height: 200)
.background(enabled ? .blue : .red)
.animation(nil, value: enabled)
.foregroundStyle(.white)
.clipShape(.rect(cornerRadius: enabled ? 60 : 0))
.animation(.spring(duration: 1, bounce: 0.6), value: enabled)

SwiftUI Animating Gestures #

SwiftUI, herhangi bir view’a gesture eklememize izin verir ve bu gesture etkileri de canlandırılabilir. Herhangi bir view’ın dokunmalara yanıt vermesini sağlayan tap gesture, parmağımızı bir görünüm üzerinde sürüklememize yanıt veren drag gestures vb. gibi üzerinde çalışabileceğimiz bir dizi gesture’ımız vardır.

Gesture konusunu daha sonra ayrıntılı olarak inceleyeceğiz, ancak şimdilik nispeten basit bir şey deneyelim. Ekranda sürükleyebileceğimiz, ancak bıraktığımızda orijinal konumuna geri dönen bir kart.

İlk layoutumuzu oluşturalım;

struct ContentView: View {
    var body: some View {
        LinearGradient(colors: [.yellow, .red], startPoint: .topLeading, endPoint: .bottomTrailing)
            .frame(width: 300, height: 200)
            .clipShape(.rect(cornerRadius: 10))
    }
}

SwiftUI Card View

Bu ekranın ortasına kart benzeri bir view çizer. Parmağımızın konumuna göre bunu ekranın etrafında hareket ettirmek istiyoruz ve bu üç adım gerektiriyor.

İlk olarak, sürükleme miktarlarını saklamak için bir state’e ihtiyacımız var.

@State private var dragAmount = CGSize.zero

İkinci olarak, bu boyutu kartın ekrandaki konumunu etkilemek için kullanmak istiyoruz. SwiftUI bize bunun için offset() adında özel bir modifier verir, bu da view’ın X ve Y koordinatını etrafındaki diğer view’ları hereket ettirmeden ayarlamamızı sağlar. İsterseniz ayrı X ve Y koordinatlarını aktarabilirsiniz, ancak offset() doğrudan bir CGSize da alabilir.

İkinci adım , bu modifer’ı linear gradient’e eklemektir.

.offset(dragAmount)

Bir DragGesture oluşturabilir ve onu card’a ekleyebiliriz. Drag gesture’ın burada işimize yarayacak iki ekstra modifier’ı vardır;

  • onChanged() : kullanıcı parmağını her hareket ettirdiğinde bir closure çalıştırmamızı sağlar
  • onEnded() : kullanıcı parmağını ekrandan kaldırıp sürüklemeyi sonlandırdığında bir closure çalıştırmamızı sağlar.

Bu closure’ların her ikisine de sürüklenme işlemini tanımlayan tek bir parametre verilir (nerede başladığı, şu anda nerede olduğu, ne kadar ilerlediği) onChanged() modifier’ı ile sürüklemenin başlangıç noktasından ne kadar uzaklaştığını söyleyecek. Bu değeri doğrudan dragAmaount ’a atayabiliriz. Böylece view parmak hareketi ile birlikte hareket eder. onEnded() için girdiyi tamamen görmezden geleceğiz, çünkü dragAmount ’u sıfır değerini vereceğiz.

Aşağıdaki kodları linear gradient’e ekleyelim.

.gesture(
    DragGesture()
        .onChanged { dragAmount = $0.translation }
        .onEnded { _ in dragAmount = .zero }
)

Kodu çalıştırırsanız, artık gradient kartı sürükleyebileceğinizi ve sürüklemeyi bıraktığınızda merkeze geri atlayacağını göreceksiniz. Kartın offseti dragAmount tarafından belirlenir ve bu da sürüklenme hareketi tarafından ayarlanır.

Artık her şey çalıştığına göre, bu hareketi biraz animasyonla hayata geçirebiliriz ve iki seçeneğimiz var. Sürüklemeyi ve bırakmayı animate etmek için implict animation eklemek veya sadece bırakmayı animate etmek için explict animation eklemek.

implict animation’u çalışırken görmek için bu modifier’ı linear gradient’e ekleyelim;

.animation(.bouncy, value: dragAmount)

swiftui implict animation

Explict animasyonu çalışırken görmek için animation() modifier’ını kaldırın ve mevcut onEnded() drag gesture koduna bunu ekleyelim;

.onEnded { _ in
    withAnimation(.bouncy) {
        dragAmount = .zero
    }
}

swiftui explict animation

Artık card’ı sürüklediğimizde animasyonsuz hareket edecek, fakat card’ı bıraktıktan sonra animasyon ile yerine dönecektir.

Offset animasyonları, drag gesture ve biraz gecikme ile çok fazla kod kullanmadan eğlenceli animasyonlar oluşturabiliriz.

struct ContentView: View {
    let letters = Array("Hello SwiftUI")
    @State private var enabled = false
    @State private var dragAmount = CGSize.zero

    var body: some View {
        HStack(spacing: 0) {
            ForEach(0..<letters.count, id: \.self) { num in
                Text(String(letters[num]))
                    .padding(5)
                    .font(.title)
                    .background(enabled ? .blue : .red)
                    .offset(dragAmount)
                    .animation(.linear.delay(Double(num) / 20), value: dragAmount)
            }
        }
        .gesture(
            DragGesture()
                .onChanged { dragAmount = $0.translation }
                .onEnded { _ in
                    dragAmount = .zero
                    enabled.toggle()
                }
        )
    }
}

swiftui example

SwiftUI Views Transitions #

SwiftUI’nin en güçlü özelliklerinden biri, view’ların gösterilme ve gizlenme şeklini özelleştirme yeteneğidir. Daha önce, view’ları koşullu olaraka dahil etmek için normal if koşullarını nasıl kullanabileceğimizi görmüştük. Bu koşul değiştiğinde view’ları hiyerarşiye ekleyip kaldırabileceğimiz anlamına gelir.

Transitions (Geçişler) bu ekleme ve çıkarmanın nasıl gerçekleşeceğini kontrol eder ve built-in transitions ile çalışabilir, bunları farklı şekillerde birleştirebilir ve hatta tamamen custom (özel) transitions oluşturabiliriz.

Bunu göstermek için, bir buton ve dikdörtgen içeren bir VStack oluşturalım;

struct ContentView: View {
    var body: some View {
        VStack {
            Button("Tap Me") {
                // do nothing
            }

            Rectangle()
                .fill(.red)
                .frame(width: 200, height: 200)
        }
    }
}

Dikdörtgenin yalnızca belirli bir koşul sağlandığında görünmesini sağlayabiliriz. İlk olarak, manipüle edebileceğimiz bazı durumları ekliyoruz;

@State private var isShowingRed = false

Daha sonra bu state’i, dikdörtgenimizi göstermek için bir koşul olarak kullanırız;

if isShowingRed {
    Rectangle()
        .fill(.red)
        .frame(width: 200, height: 200)
}

Son olarak, butonun eyleminde isShowingRed öğesini true ve false arasında değiştirebiliriz

isShowingRed.toggle()

Uygulamayı çalıştırırsanız, butona basmanın kırmızı kareyi gösterdiğini ve gizlediğini göreceksiniz. Animasyon yoktur, sadece aniden görünür ve kaybolur.

SwiftUI’nin varsayılan view geçişini (transition), withAnimation() kullanarak state değişikliğini şu şekilde sararak elde edebiliriz;

withAnimation {
    isShowingRed.toggle()
}

SwiftUI Manuel Transition

Bu küçük değişiklik ile, uygulama şimdi kırmızı dikdörtgeni içeri ve dışarı soldururken aynı zamanda yer açmak için butonu yukarı taşıyor. İyi görünüyor, ancak transition() modifier ile daha iyisini yapabiliriz.

Örneğin, sadece transition() modifier’ını ekleyerek dikdörtgenin gösterildiği gibi yukarı ve aşağı ölçeklenmesini sağlayabiliriz.

Rectangle()
    .fill(.red)
    .frame(width: 200, height: 200)
    .transition(.scale)

SwiftUI Transition Usage

Şimdi butona dokunmak çok daha iyi görünüyor. Butona basınca dikdörtgen büyüyor, ardından tekrar dokunulduğunda küçülüyor.

Deneyebileceğimiz başka geçişler (transitions) de vardır. Bunlardan kullanışlı olanı, view gösterilirken bir transition ve kaybolurken başka bir transition kullanmamızı sağlayan .asymmetric ’tir. Denemek için dikdörtgenin mevcut geçişini (transition) bununla değiştirin.

.transition(.asymmetric(insertion: .scale, removal: .opacity))

SwiftUI Asymmetric Transition

ViewModifier Kullanarak Özel Transition Oluşturma #

SwiftUI için tamamen yeni geçişler oluşturmak mümkündür ve aslında şaşırtıcı derecede kolaydır, bu da tamamen özel animasyonlar kullanarak view’lar eklememize ve kaldırmamıza olanak tanır.

Bu işlevsellik, istediğimiz herhangi bir view modifier kabul eden .modifier transition ile mümkündür. Buradaki sorun, modifier’ın instance’ını kullanabilmemizdir. Bu da kendi oluşturduğumuz bir modifier olması gerektiği anlamına gelir.

Bunu denemek için Keynote’daki Pivot animasyonunu taklit etmemizi sağlayan bir view modifier yazabiliriz (pivot animasyonu yeni bir slaydın sol üst köşesinden dönmesine neden olur). SwiftUI’de bu, view’ın içinde olması gereken sınırlardan kaçmadan bir köşeden içeri dönmesine neden olan bir view modifier oluşturmak anlamına gelir. SwiftUI aslında bize tam bunu yapmamız için modifier’lar verir. rotationEffect() bir view’ı 2D uzayda döndürmemizi sağlar ve clipped() view’ın dikdörtgen alanın dışına çizilmesini durdurur.

rotationEffect(), her zaman Z ekseni etrafında dönmesi dışında rotation3DEffect() ’e benzer. Bununla birlikte, bize rotasyonun anchor point’ini de kontrol etme yeteneği verir.( anchor point : view’ın hangi kısmı rotasyonun merkezi olarak yerinde sabitlenmelidir ) SwiftUI bize anchor point’i kontrol etmemiz için UnitPoint tipini verir, bu da rotasyon için tam bir X/Y noktası belirtmemize veya birçok yerleşik seçenekten birini kullanmamıza olanak tanır ( .topLeading , .bottomTrailing , .center vb.)

Tüm bunları, rotasyonun nerede gerçekleşeceğini kontrol etmek için bir anchor point ve ne kadar rotasyon uygulanacağını kontrol etmek için CornerRotateModifier gerekmektedir. struct oluşturarak bunları koda dökelim;

struct CornerRotateModifier: ViewModifier {
    let amount: Double
    let anchor: UnitPoint

    func body(content: Content) -> some View {
        content
            .rotationEffect(.degrees(amount), anchor: anchor)
            .clipped()
    }
}

Burada clipped() modifier’ının eklenmesi, view döndüğünde doğal dikdörtgenin dışında kalan kısımların çizilmeyeceği anlamına gelir.

Bunu doğrudan .modifier transition kullanarak deneyebiliriz, ancak bu biraz kullanışsızdır. Daha iyi bir fikir, bunu AnyTransition ’a bir extension ile sarmak ve üst ön köşesinde -90’dan 0’a dönmesini sağlamaktır;

extension AnyTransition {
    static var pivot: AnyTransition {
        .modifier(
            active: CornerRotateModifier(amount: -90, anchor: .topLeading),
            identity: CornerRotateModifier(amount: 0, anchor: .topLeading)
        )
    }
}

Artık aşağıdaki kodu herhangi bir view’a ekleyerek pivot animasyonuna sahip olmasını sağlayabiliriz.

.transition(.pivot)

Örneğin, onTapGesture() modifier’ını kullanarak kırmızı bir dikdörtgenin ekran üzerinde dönmesini sağlayabiliriz;

struct ContentView: View {
    @State private var isShowingRed = false

    var body: some View {
        ZStack {
            Rectangle()
                .fill(.blue)
                .frame(width: 200, height: 200)

            if isShowingRed {
                Rectangle()
                    .fill(.red)
                    .frame(width: 200, height: 200)
                    .transition(.pivot)
            }
        }
        .onTapGesture {
            withAnimation {
                isShowingRed.toggle()
            }
        }
    }
}

SwiftUI Custom Transition


Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.

Bu yazı, SwiftUI Day 33 adresinde bulunan yazılardan kendim için aldığım notları içermektedir. Orjinal dersi takip etmek için lütfen bağlantıya tıklayın.