iOS/SwiftUI

[WWDC 2023] Wind your way through advanced animations in SwiftUI

Hamp 2025. 10. 30. 13:35
반응형

🏁 학습할 내용

  • Animation phase
  • Keyframes
  • Tip and tricks 

♻️ Animation phases

 

 

🧩 역할

정해진 phase를 순환하며 애니메이션을 적용해주는 modifier

 

 

🤖 코드 및 결과

 

1️⃣  phaseAnimator(phase, content, animation)

nonisolated public func phaseAnimator<Phase>(
_ phases: some Sequence, 
@ViewBuilder content: @escaping (PlaceholderContentView<Self>, Phase) -> some View, 
animation: @escaping (Phase) -> Animation? = { _ in .default }) -> some View where Phase : Equatable
  • phase: 단계 Sequence
  • content: 적용될 container와 현재 phase
  • animation: 적용할 애니메이션 default는 spring
    VStack {
      Text("Hello, World!")
        .phaseAnimator([false, true]) { content, phase in
          content
            .foregroundStyle(phase ? .red : .blue)
        } animation: { phase in
            .easeInOut
        }
    }

 

 

정해진 phase를 무한히 반복함, 어떨 떄는 한번 phase들을 돌고나면 끝내고 싶음

 

그 때, trigger파리미터를 넘겨줌

 

2️⃣  phaseAnimator(phase,  trigger , content, animation)

  • 파라미터 역할은 모두 같고, trigger는 계속 애니메이션을 실행하지말고, Trigger가 변경됐을 떄만 한번 진행
enum CustomPhase: CaseIterable {
  case `init`
  case mid
  case end

  var scale: CGFloat {
    switch self {
    case .`init`:
      return 2.0
    case .mid:
      return 1
    case .end:
      return 3.0
    }
  }

  var offset: CGFloat {
    switch self {
    case .`init`:
      return -10
    case .mid:
      return 10
    case .end:
      return 10
    }
  }
}

struct SimpleAnimatableDemo: View {
  @State private var trigger: Bool = false

  var body: some View {
    VStack {
      Text("Hello, World!")
        .phaseAnimator(CustomPhase.allCases, trigger: trigger) { content, phase in
          content
            .scaleEffect(phase.scale)
            .offset(y: phase.offset)
        } animation: { phase in
          switch phase {
          case .`init`:
            return .spring
          case .mid:
            return .smooth
          case .end:
            return .easeInOut(duration: 0.5)
          }
        }
        .onTapGesture {
          trigger.toggle()
        }
    }
  }
}


 

🧩 역할

 

타이밍과 움직임을 완전히 제어할 수 있는 복잡하고 조직화된 애니메이션을 정의함

 

🤷 Animation Phase와 다른 점

Animation Phase

Animation Phase는 Phase에 설정된 모든 프로퍼티는 동시에 애니메이팅된다. 

하지만, 만약 각 프로퍼티를 독립적으로 애니메이팅하고 싶다면?? 그 때 사용하는게 Keyframes이다.

 

✅ 특징

  • 키 프레임은 애니메이션 안에서 특정 시간의 값을 정의한다.
  • 점은 키프레임을 나타낸다.
  • SwiftUI는 키 프레임 사이의 값을 보간한다.
  • 키 프레임은 고유 타이밍으로 개별 트랙을 정의해, 여러 가지 효과를 동시에 적용할 수 있다.

 

🤖 예제 코드

public protocol KeyframeTrackContent<Value> {

    associatedtype Value : Animatable = Self.Body.Value

    associatedtype Body : KeyframeTrackContent
    
    @KeyframeTrackContentBuilder<Self.Value> var body: Self.Body { get }
}
  • Keyframe을 정의하기위한 프로토콜, Value는 Animatable을 채택한 값이 들어와야함

 

  • initValue: 초기값
  • trigger: 시작 트리거
  • content: 적용될 container와 value가 전달됨
  • keyframes: value가 전달되며, 원하는 Keyframe을 반환
        VStack {
            Text("!@4214214")
                .keyframeAnimator(initialValue: AnimationValues(), trigger: trigger, content: { content, value in
                    content
                        .rotationEffect(value.angle)
                        .scaleEffect(value.scale)
                        .scaleEffect(y: value.verticalStretch)
                        .offset(y: value.verticalTranslation)
                }, keyframes: { value in
                    
                    KeyframeTrack(\.angle) { // 키프레임이 각 value의 값을 보간
                        LinearKeyframe(.zero, duration: 0.36)
                        SpringKeyframe(.degrees(90), duration: 0.8, spring: .bouncy)
                        SpringKeyframe(.degrees(.zero), duration: 0.8, spring: .bouncy)
                    }

                    
                    KeyframeTrack(\.scale) { // 키프레임이 각 value의 값을 보간
                        LinearKeyframe(1.0, duration: 0.36)
                        SpringKeyframe(1.5, duration: 0.8, spring: .bouncy)
                        SpringKeyframe(1.0, duration: 0.8, spring: .bouncy)
                    }
                })
                .onTapGesture {
                    trigger.toggle()
                }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)

 

  • KeyframeTrack을 통해 보간할 값들을 keyPath로 지정
  • LinearKeyframeSringKeyframe을 이용하여, 해당 키프레임에 변경될 목표값과, duration, 애니메이션 지정
  • 현재 애니메이션은, 오른쪽 90도로 꺾이면서, 1.5배 됐다가, 다시 각도와 크기가 복구됨

 

🧱 종류

 

1️⃣ Linear Keyframe

  • 이전 키 프레임의 벡터 공간을 선형적으로 보간

2️⃣ Spring Keyframe

  • 스프링 기능을 사용해 이전 키 프레임의 타깃값을 보간

3️⃣ Cubit Keyframe

  • 키 프레임 사이 보간에 베지어 곡선을 이용
  • 여러개를 조합해 시퀀스를 만들면 캣멀롬 스플라인과 일치하는 곡선이 나옴

4️⃣ MoveKey Keyframe

  • 보간 없이, 바로 이동

 

같은 뷰에 keyframes 쪽에 다음 형태로 넣어줘보자.

                    KeyframeTrack(\.angle) {
                        CubicKeyframe(.zero, duration: 0.58)
                        CubicKeyframe(.degrees(16), duration: 0.125)
                        CubicKeyframe(.degrees(-16), duration: 0.125)
                        CubicKeyframe(.degrees(16), duration: 0.125)
                        CubicKeyframe(.zero, duration: 0.125)
                    }
                    
                    KeyframeTrack(\.verticalStretch) {
                        CubicKeyframe(1.0, duration: 0.1)
                        CubicKeyframe(0.6, duration: 0.15)
                        CubicKeyframe(1.5, duration: 0.1)
                        CubicKeyframe(1.05, duration: 0.15)
                        CubicKeyframe(1.0, duration: 0.88)
                        CubicKeyframe(0.8, duration: 0.1)
                        CubicKeyframe(1.04, duration: 0.4)
                        CubicKeyframe(1.0, duration: 0.22)
                    }
                    
                    KeyframeTrack(\.scale) {
                        LinearKeyframe(1.0, duration: 0.36)
                        SpringKeyframe(1.5, duration: 0.8, spring: .bouncy)
                        SpringKeyframe(1.0, spring: .bouncy)
                    }
                    
                    KeyframeTrack(\.verticalTranslation) {
                        LinearKeyframe(0.0, duration: 0.1)
                        SpringKeyframe(20.0, duration: 0.15, spring: .bouncy)
                        SpringKeyframe(-60.0, duration: 1.0, spring: .bouncy)
                        SpringKeyframe(0.0, spring: .bouncy)
                    }

 

우리가 쓴 키프레임을 시각화한걸 살펴보자.

 

 

 

🧐 키 프레임에 수동으로 값을 부여하는 방법

  • KeyframeTimeline을 이용하여, 키프레임과 트랙집합을 캡처
  • 이후, duration을  가장 긴 트랙의 지속시간으로 설정
  • 애니메이션의 범위안에서는 어느 시간의 값도 얻을  수 있음
  • 이런걸 이용해면, Swift Chart를 이용해 키프레임을 쉽게 시각화 할 수 있음

 

⚠️ 주의

  • 키프레임은 미리 정의된 애니메이션이다
  • 유동적, 상호 작용형 UI가 필요한 상황에서 대체 불가한 애니메이션이다.
  • 키프레임은 재생 가능한 비디오 클립과 가깝다
  • 얼마든지 제어가 가능하지만, 애니메이션 도중 키프레임을 바꾸는 것에 주의하자.
  • 프레임 마다 업게이트가 일어나므로, 비싼 작업은 피하자

📷 Tip and tricks 

 

발표자는 자동 줌인을 통해 코스를 따라가는 카메라 무빙을 원함

이때, 맵킷에서는 키 프레임을 사용해 카메라가 이동함


출처

https://developer.apple.com/videos/play/wwdc2023/10157/

 

Wind your way through advanced animations in SwiftUI - WWDC23 - Videos - Apple Developer

Discover how you can take animation to the next level with the latest updates to SwiftUI. Join us as we wind our way through animation...

developer.apple.com

 

https://developer.apple.com/documentation/swiftui/phaseanimator

 

PhaseAnimator | Apple Developer Documentation

A container that animates its content by automatically cycling through a collection of phases that you provide, each defining a discrete step within an animation.

developer.apple.com

 

반응형