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
- Animasyon Temelleri
- Spring Animations
- Keyframe Animations (iOS 17+)
- Phase Animations (iOS 17+)
- Gesture-Driven Animations
- Micro-Interactions
- Loading Animations
- Performans Optimizasyonu
- Custom Timing Curves
- Sonuç ve Öneriler
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 = false3 4 var body: some View {5 VStack(spacing: 40) {6 // Implicit Animation7 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 Animation13 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 = false3 4 var body: some View {5 VStack(spacing: 20) {6 // Bouncy spring7 Text("Bouncy")8 .offset(y: animate ? 0 : -100)9 .animation(.spring(response: 0.5, dampingFraction: 0.3, blendDuration: 0), value: animate)10 11 // Smooth spring12 Text("Smooth")13 .offset(y: animate ? 0 : -100)14 .animation(.spring(response: 0.8, dampingFraction: 0.8), value: animate)15 16 // Snappy spring17 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 = false3 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: animate12 ) { content, value in13 content14 .scaleEffect(value.scale)15 .rotationEffect(value.rotation)16 .offset(y: value.yOffset)17 } keyframes: { _ in18 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.045 var rotation: Angle = .zero46 var yOffset: Double = 047}Phase Animations (iOS 17+)
swift
1struct PhaseAnimationDemo: View {2 @State private var animate = false3 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 in9 content10 .scaleEffect(phase ? 1.2 : 1.0)11 .opacity(phase ? 1.0 : 0.8)12 } animation: { phase in13 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.zero3 @State private var isDragging = false4 5 var body: some View {6 RoundedRectangle(cornerRadius: 20)7 .fill(8 LinearGradient(9 colors: [.blue, .purple],10 startPoint: .topLeading,11 endPoint: .bottomTrailing12 )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 : 519 )20 .scaleEffect(isDragging ? 1.05 : 1.0)21 .rotationEffect(.degrees(Double(offset.width) / 20))22 .offset(offset)23 .gesture(24 DragGesture()25 .onChanged { gesture in26 withAnimation(.interactiveSpring) {27 offset = gesture.translation28 isDragging = true29 }30 }31 .onEnded { gesture in32 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.height37 )38 } else {39 offset = .zero40 }41 isDragging = false42 }43 }44 )45 }46}Micro-Interactions
swift
1struct LikeButton: View {2 @State private var isLiked = false3 @State private var particles: [Particle] = []4 5 var body: some View {6 ZStack {7 // Particles8 ForEach(particles) { particle in9 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 button17 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: 144 )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 = targetOffset55 particles[index].opacity = 056 }57 }58 }59 60 DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {61 particles.removeAll()62 }63 }64}65 66struct Particle: Identifiable {67 let id: UUID68 let color: Color69 let size: CGFloat70 var offset: CGSize71 var opacity: Double72}Loading Animations
swift
1struct PulsingLoader: View {2 @State private var animate = false3 4 var body: some View {5 ZStack {6 ForEach(0..<3) { index in7 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: animate17 )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 tetikler2@State private var value: CGFloat = 03 4// ✅ İyi - AnimatableModifier kullan5struct AnimatableScale: AnimatableModifier {6 var scale: CGFloat7 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 rendering19ComplexAnimatedView()20 .drawingGroup()Performans Optimizasyonu
swift
1// ❌ Kötü - Her frame'de view rebuild2struct BadAnimation: View {3 @State private var value: CGFloat = 04 var body: some View {5 Circle()6 .scaleEffect(value)7 .onAppear {8 withAnimation(.linear(duration: 2).repeatForever()) {9 value = 1.510 }11 }12 }13}14 15// ✅ İyi - AnimatableModifier kullan16struct AnimatableScale: AnimatableModifier {17 var scale: CGFloat18 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 renderCustom Timing Curves
swift
1extension Animation {2 // Custom ease curve3 static var smoothBounce: Animation {4 .timingCurve(0.34, 1.56, 0.64, 1, duration: 0.5)5 }6 7 // iOS 17+ custom spring8 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
- Apple Developer - SwiftUI Animation
- WWDC23 - Animate with springs
- WWDC23 - Wind your way through advanced animations
- Apple HIG - Motion
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?

