Tüm Yazılar
KategoriSwiftUI
Okuma Süresi
24 dk
Yayın Tarihi
...
Kelime Sayısı
1.456kelime

Kahveni hazırla - bu içerikli bir makale!

iOS 17+ Spring, Keyframe, Phase Animator ile akıcı animasyonlar. Micro-interactions, gesture-driven animations, hero transitions ve performans optimizasyonu. Awwwards kalitesinde UX!

SwiftUI Custom Animations: Micro-Interactions ve Motion Design Mastery

Animasyonlar, kullanıcı deneyimini zenginleştiren ve uygulamanıza kişilik katan kritik unsurlardır. iOS 17 ile gelen Keyframe ve Phase Animator API'leri, SwiftUI animasyonlarını yeni bir seviyeye taşıdı. Bu rehberde, Awwwards kalitesinde micro-interactions oluşturmayı öğreneceksiniz.


İçindekiler


Animasyon Felsefesi

"İyi animasyon görünmez. Kullanıcı sadece doğal hisseder." - Apple HIG
Animasyon Tipi
Kullanım
Süre
Micro-interaction
Buton tap, toggle
0.1-0.3s
Page transition
Navigation
0.3-0.5s
Modal
Sheet, alert
0.25-0.35s
Loading
Skeleton, spinner
1-2s loop

Animasyon Temelleri

Implicit vs Explicit Animations

swift
1struct AnimationBasics: View {
2 @State private var isExpanded = false
3
4 var body: some View {
5 VStack(spacing: 40) {
6 // Implicit Animation
7 Circle()
8 .fill(isExpanded ? .blue : .red)
9 .frame(width: isExpanded ? 200 : 100)
10 .animation(.spring(response: 0.5, dampingFraction: 0.6), value: isExpanded)
11
12 // Explicit Animation
13 Circle()
14 .fill(.green)
15 .frame(width: isExpanded ? 200 : 100)
16
17 Button("Toggle") {
18 withAnimation(.spring(response: 0.5, dampingFraction: 0.6)) {
19 isExpanded.toggle()
20 }
21 }
22 }
23 }
24}

Spring Animations

swift
1struct SpringShowcase: View {
2 @State private var animate = false
3
4 var body: some View {
5 VStack(spacing: 20) {
6 // Bouncy spring
7 Text("Bouncy")
8 .offset(y: animate ? 0 : -100)
9 .animation(.spring(response: 0.5, dampingFraction: 0.3, blendDuration: 0), value: animate)
10
11 // Smooth spring
12 Text("Smooth")
13 .offset(y: animate ? 0 : -100)
14 .animation(.spring(response: 0.8, dampingFraction: 0.8), value: animate)
15
16 // Snappy spring
17 Text("Snappy")
18 .offset(y: animate ? 0 : -100)
19 .animation(.snappy(duration: 0.4), value: animate)
20
21 // Interactive spring (iOS 17+)
22 Text("Interactive")
23 .offset(y: animate ? 0 : -100)
24 .animation(.interactiveSpring(duration: 0.5, extraBounce: 0.2), value: animate)
25 }
26 .onTapGesture { animate.toggle() }
27 }
28}

Keyframe Animations (iOS 17+)

swift
1struct KeyframeDemo: View {
2 @State private var animate = false
3
4 var body: some View {
5 VStack {
6 Image(systemName: "star.fill")
7 .font(.system(size: 60))
8 .foregroundStyle(.yellow)
9 .keyframeAnimator(
10 initialValue: AnimationValues(),
11 trigger: animate
12 ) { content, value in
13 content
14 .scaleEffect(value.scale)
15 .rotationEffect(value.rotation)
16 .offset(y: value.yOffset)
17 } keyframes: { _ in
18 KeyframeTrack(\.scale) {
19 SpringKeyframe(1.5, duration: 0.2)
20 SpringKeyframe(1.0, duration: 0.2)
21 SpringKeyframe(1.2, duration: 0.15)
22 SpringKeyframe(1.0, duration: 0.15)
23 }
24
25 KeyframeTrack(\.rotation) {
26 LinearKeyframe(.degrees(-10), duration: 0.1)
27 LinearKeyframe(.degrees(10), duration: 0.1)
28 LinearKeyframe(.degrees(-5), duration: 0.1)
29 LinearKeyframe(.degrees(0), duration: 0.1)
30 }
31
32 KeyframeTrack(\.yOffset) {
33 SpringKeyframe(-20, duration: 0.2)
34 SpringKeyframe(0, duration: 0.3)
35 }
36 }
37
38 Button("Animate") { animate.toggle() }
39 }
40 }
41}
42 
43struct AnimationValues {
44 var scale: Double = 1.0
45 var rotation: Angle = .zero
46 var yOffset: Double = 0
47}

Phase Animations (iOS 17+)

swift
1struct PhaseAnimationDemo: View {
2 @State private var animate = false
3
4 var body: some View {
5 Image(systemName: "heart.fill")
6 .font(.system(size: 80))
7 .foregroundStyle(.red)
8 .phaseAnimator([false, true], trigger: animate) { content, phase in
9 content
10 .scaleEffect(phase ? 1.2 : 1.0)
11 .opacity(phase ? 1.0 : 0.8)
12 } animation: { phase in
13 phase ? .spring(duration: 0.2) : .spring(duration: 0.5)
14 }
15 .onTapGesture { animate.toggle() }
16 }
17}

Gesture-Driven Animations

swift
1struct DraggableCard: View {
2 @State private var offset = CGSize.zero
3 @State private var isDragging = false
4
5 var body: some View {
6 RoundedRectangle(cornerRadius: 20)
7 .fill(
8 LinearGradient(
9 colors: [.blue, .purple],
10 startPoint: .topLeading,
11 endPoint: .bottomTrailing
12 )
13 )
14 .frame(width: 300, height: 200)
15 .shadow(
16 color: .black.opacity(isDragging ? 0.3 : 0.1),
17 radius: isDragging ? 20 : 10,
18 y: isDragging ? 10 : 5
19 )
20 .scaleEffect(isDragging ? 1.05 : 1.0)
21 .rotationEffect(.degrees(Double(offset.width) / 20))
22 .offset(offset)
23 .gesture(
24 DragGesture()
25 .onChanged { gesture in
26 withAnimation(.interactiveSpring) {
27 offset = gesture.translation
28 isDragging = true
29 }
30 }
31 .onEnded { gesture in
32 withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
33 if abs(gesture.translation.width) > 150 {
34 offset = CGSize(
35 width: gesture.translation.width > 0 ? 500 : -500,
36 height: gesture.translation.height
37 )
38 } else {
39 offset = .zero
40 }
41 isDragging = false
42 }
43 }
44 )
45 }
46}

Micro-Interactions

swift
1struct LikeButton: View {
2 @State private var isLiked = false
3 @State private var particles: [Particle] = []
4
5 var body: some View {
6 ZStack {
7 // Particles
8 ForEach(particles) { particle in
9 Circle()
10 .fill(particle.color)
11 .frame(width: particle.size, height: particle.size)
12 .offset(particle.offset)
13 .opacity(particle.opacity)
14 }
15
16 // Heart button
17 Image(systemName: isLiked ? "heart.fill" : "heart")
18 .font(.system(size: 40))
19 .foregroundStyle(isLiked ? .red : .gray)
20 .scaleEffect(isLiked ? 1.0 : 0.9)
21 }
22 .onTapGesture {
23 withAnimation(.spring(response: 0.3, dampingFraction: 0.5)) {
24 isLiked.toggle()
25 }
26
27 if isLiked {
28 generateParticles()
29 }
30 }
31 }
32
33 private func generateParticles() {
34 let colors: [Color] = [.red, .pink, .orange, .yellow]
35
36 for i in 0..<12 {
37 let angle = Double(i) * (360.0 / 12.0)
38 let particle = Particle(
39 id: UUID(),
40 color: colors.randomElement()!,
41 size: CGFloat.random(in: 4...8),
42 offset: .zero,
43 opacity: 1
44 )
45 particles.append(particle)
46
47 let targetOffset = CGSize(
48 width: cos(angle * .pi / 180) * CGFloat.random(in: 40...80),
49 height: sin(angle * .pi / 180) * CGFloat.random(in: 40...80)
50 )
51
52 withAnimation(.easeOut(duration: 0.5)) {
53 if let index = particles.firstIndex(where: { $0.id == particle.id }) {
54 particles[index].offset = targetOffset
55 particles[index].opacity = 0
56 }
57 }
58 }
59
60 DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
61 particles.removeAll()
62 }
63 }
64}
65 
66struct Particle: Identifiable {
67 let id: UUID
68 let color: Color
69 let size: CGFloat
70 var offset: CGSize
71 var opacity: Double
72}

Loading Animations

swift
1struct PulsingLoader: View {
2 @State private var animate = false
3
4 var body: some View {
5 ZStack {
6 ForEach(0..<3) { index in
7 Circle()
8 .fill(.blue.opacity(0.3))
9 .frame(width: 60, height: 60)
10 .scaleEffect(animate ? 1.5 : 0.5)
11 .opacity(animate ? 0 : 1)
12 .animation(
13 .easeInOut(duration: 1.2)
14 .repeatForever(autoreverses: false)
15 .delay(Double(index) * 0.4),
16 value: animate
17 )
18 }
19
20 Circle()
21 .fill(.blue)
22 .frame(width: 30, height: 30)
23 }
24 .onAppear { animate = true }
25 }
26}

Performans Optimizasyonu

swift
1// ❌ Kötü - View rebuild'i tetikler
2@State private var value: CGFloat = 0
3 
4// ✅ İyi - AnimatableModifier kullan
5struct AnimatableScale: AnimatableModifier {
6 var scale: CGFloat
7
8 var animatableData: CGFloat {
9 get { scale }
10 set { scale = newValue }
11 }
12
13 func body(content: Content) -> some View {
14 content.scaleEffect(scale)
15 }
16}
17 
18// drawingGroup() ile GPU rendering
19ComplexAnimatedView()
20 .drawingGroup()

Performans Optimizasyonu

swift
1// ❌ Kötü - Her frame'de view rebuild
2struct BadAnimation: View {
3 @State private var value: CGFloat = 0
4 var body: some View {
5 Circle()
6 .scaleEffect(value)
7 .onAppear {
8 withAnimation(.linear(duration: 2).repeatForever()) {
9 value = 1.5
10 }
11 }
12 }
13}
14 
15// ✅ İyi - AnimatableModifier kullan
16struct AnimatableScale: AnimatableModifier {
17 var scale: CGFloat
18
19 var animatableData: CGFloat {
20 get { scale }
21 set { scale = newValue }
22 }
23
24 func body(content: Content) -> some View {
25 content.scaleEffect(scale)
26 }
27}
28 
29// GPU rendering için drawingGroup()
30ComplexAnimatedView()
31 .drawingGroup() // Metal ile render

Custom Timing Curves

swift
1extension Animation {
2 // Custom ease curve
3 static var smoothBounce: Animation {
4 .timingCurve(0.34, 1.56, 0.64, 1, duration: 0.5)
5 }
6
7 // iOS 17+ custom spring
8 static var gentleSpring: Animation {
9 .spring(duration: 0.5, bounce: 0.2)
10 }
11}
💡 Altın İpucu: TimelineView(.animation) kullanarak 60/120 FPS'de sürekli güncellenen animasyonlar oluşturabilirsiniz. Canvas API ile birleştirildiğinde, Metal seviyesinde performansla karmaşık particle systems ve generative art efektleri yaratabilirsiniz. Bu kombinasyon SwiftUI'nin animasyon sınırlarını aşmanızı sağlar ve oyun benzeri deneyimler için idealdir.

Okuyucu Ödülü

Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var:

ALTIN İPUCU

Bu yazının en değerli bilgisi

Bu ipucu, yazının en önemli çıkarımını içeriyor.

Sonuç ve Öneriler

Key Takeaways

  • Spring Animations - Doğal, organik hissiyat
  • Keyframe API - Karmaşık, çok aşamalı animasyonlar
  • Phase Animator - State-driven transitions
  • Gesture Integration - Interactive, responsive UX
  • Performance - drawingGroup, AnimatableModifier

Kaynaklar

Easter Egg

Gizli bir bilgi buldun!

Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?

Etiketler

#SwiftUI#Animation#iOS#Motion#Transition#UI
Muhittin Çamdalı

Muhittin Çamdalı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz