ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Advanced animations in SwiftUI (feat. WWDC 2023)
    SwiftUI 2023. 6. 8. 16:18

    안녕하세요. 그린입니다🍏

    이번 포스팅에서는 WWDC 2023에서 소개된 SwiftUI의 향상된 애니메이션을 학습해보겠습니다🙌

     

    Wind your way through advanced animations in SwiftUI

    SwiftUI에서 개선된 애니메이션을 할 수 있는 방법에 대해 한번 살펴보겠습니다.

     

    우선 시작 전 SwiftUI에서 이미 알고 있는 애니메이션들에 대해 확인해볼까요?

     

    기존 SwiftUI에서 익숙한 애니메이션

    import SwiftUI
    
    struct ContentView: View {
      var xcodeImage: Image
      @State private var selected: Bool = false
    
      
      var body: some View {
        xcodeImage
          .scaleEffect(selected ? 1.5 : 1.0)
          .onTapGesture {
            withAnimation {
              selected.toggle()
            }
          }
      }
    }

    간단하게 요런 코드가 있습니다.

    엑코 아이콘을 탭하면 스케일을 키우고 줄이는 애니메이션이 작동하죠.

    withAnimation을 사용하거나 animation 모디파이어를 사용하는것처럼 기존에서 많이들 사용하고 계셨을 너무 익숙한 구동이죠?

     

    이 쉬운 구현들에서 이제는 복잡하고 단계가 많은 애니메이션을 만들기 위한 새로운 방법들에 대해 알아보시죠🙌

     

    Animation phases

    애니메이션을 구성하는 미리 정해둔 상태 Set를 이용해 SwiftUI가 자동으로 애니메이션을 진행하는걸 보겠습니다.

     

    PhaseAnimator

    phaseAnimator를 이용합니다.

    코드를 먼저 보실까요?

    import SwiftUI
    
    struct ContentView: View {
      var xcodeImage: Image
      @State private var selected: Bool = false
    
      
      var body: some View {
        VStack {
          xcodeImage
            .scaleEffect(selected ? 1.5 : 1.0)
            .onTapGesture {
              withAnimation {
                selected.toggle()
              }
            }
          
          Text("Xcode")
            .font(.title)
        }
        .phaseAnimator([false, true]) { content, phase in
          content
            .foregroundStyle(phase ? .red : .green)
        }
      }
    }

    보시면 phaseAnimator를 이용해주면 됩니다.

    순환하는건 false, true 두 값입니다. 그렇기에 계속 순환하죠.

    그리고 content에 phase값에 따라 색상을 변경해주도록 모디파이어를 붙여줍니다.

     

    그럼 어떻게 나올까요?

     

    T/F 값이 두개이니 두 단계를 계속 순환하면서 색을 변경하고 있습니다.

     

    그럼 이것들을 어떤 녀석으로 이뤄져있고 동작하는지 좀 더 딥하게 보겠습니다🙌

     

    우선 PhaseAnimator는 제공하는 단계적인 Set을 자동으로 순환하며 콘텐츠에 애니메이션을 적용하는 컨테이너로 각 단계는 애니메이션 내에서 개발 단계를 적용합니다.

    struct PhaseAnimator<Phase, Content> where Phase : Equatable, Content : View

    요렇게 Phase, 즉 순환할 단계와 뷰를 변경하여 보여줄 Content를 가집니다.

     

    해당 구조체를 직접 만들어 넣어도 되지만 기본적으로 단계 기반의 애니메이션을 만들기 위해 두가지의 뷰 모디파이어를 제공합니다.

     

    1️⃣ 제공된 Phase 단계를 계속 순환

    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

    요렇게 생겼습니다.

    매개변수로 phases는 순환할 상태를 정의하는 단계입니다.

    해당 시퀀스가 비어 있으면 안됩니다😳

    컴파일은 되지만 뷰 경고를 맛볼 수 있죠ㅎ..

     

    그 다음으로 content의 첫번째 인자는 현재 뷰 빌더 클로저로 수정된 뷰에 대한 프록시 값입니다.

    두번째 인자는 다음 단계를 의미합니다.

    즉, 해당 넘겨진 content에 모디파이어를 붙여 편하게 꾸며줄 수 있습니다.

     

    마지막으로, animation입니다.

    animation은 시퀀스 순으로 순환하며 다음 단계를 보여줄 때 사용할 애니메이션을 지정해줍니다.

    애니메이션을 주지 않는다면 default로 지정됩니다.

    기본적으로 SwiftUI는 Spring 애니메이션을 사용합니다.

     

    즉, 세가지를 모두 사용한다면 이렇게 구현해볼 수 있죠.

    .phaseAnimator([false, true]) { content, phase in
      content
        .foregroundStyle(phase ? .red : .green)
      } animation: { phase in
        .easeIn(duration: 2.0)
      }

     

    2️⃣ 트리거를 두어 값이 변경되면 Phase 단계를 순환

    func phaseAnimator<Phase>(
        _ phases: some Sequence,
        trigger: some Equatable,
        @ViewBuilder content: @escaping (PlaceholderContentView<Self>, Phase) -> some View,
        animation: @escaping (Phase) -> Animation? = { _ in .default }
    ) -> some View where Phase : Equatable

    phase, content, animation은 1번의 소개와 동일합니다.

    trigger는 이제 애니메이션 시킬 즉 순환 시키기 위해 변경 사항을 관찰하는 값입니다.

    다만 다른점 또 하나는 트리거가 되었을때 전체 순회를 하고 지속적으로 계속 반복하여 순회를 하진 않는다는 겁니다.

    import SwiftUI
    
    struct ContentView: View {
      var xcodeImage: Image
      @State private var selected: Bool = false
      
      var body: some View {
        VStack(spacing: 30) {
          xcodeImage
            .scaleEffect(selected ? 1.5 : 1.0)
            .onTapGesture {
              withAnimation {
                selected.toggle()
              }
            }
          
          Text("Xcode")
            .font(.title)
        }
        .phaseAnimator(
          [false, true],
          trigger: selected
        ) { content, phase in
          content
            .foregroundStyle(phase ? .red : .green)
        } animation: { phase in
            .spring
        }
      }
    }

    위 코드에서 trigger만 엑코 아이콘이 눌렸을때 값이 변경됨을 감지하여 trigger로 주었습니다.

     

    동작이 어떤지 한번 볼까요?

    요렇게 엑코가 눌릴때만 T/F 두 상태만 돌고 끝납니다.

    즉 계속 무한으로 돌지 않죠!

     

    아주 유용하게 어떤 값이 변경될때 애니메이션을 일으키도록 할 수 있어 많이 사용될것 같아요😃

     

    그럼 조금 더 나아가서 여러 Phase를 구성하고 애니메이션도 더 많이 넣어보면서 체계적으로 구성해보겠습니다🙌

     

    먼저 Phase부터 enum으로 구성해줍니다.

    enum Phase: CaseIterable {
      case initial
      case move
      case scale
      
      var verticalOffset: Double {
        switch self {
        case .initial:
          return 0
        case .move:
          return -100
        case .scale:
          return -100
        }
      }
      
      var scale: Double {
        switch self {
        case .initial:
          return 1.0
        case .move:
          return 1.0
        case .scale:
          return 2.0
        }
      }
    }

    이제 이 Phase를 순회할 애니메이터를 위한 뷰를 만들고 phaseAnimator를 이용해볼께요.

    private struct PhaseAnimatorView2: View {
      var xcodeImage: Image = Image("XcodeImage")
      
      @State private var selected: Bool = false
      
      var body: some View {
        xcodeImage
          .onTapGesture {
            withAnimation {
              selected.toggle()
            }
          }
          .phaseAnimator(
            Phase.allCases,
            trigger: selected
          ) { content, phase in
            content
              .scaleEffect(phase.scale)
              .offset(y: phase.verticalOffset)
          } animation: { phase in
            switch phase {
            case .initial: .smooth
            case .move: .easeInOut(duration: 0.3)
            case .scale: .spring(
              duration: 0.3,
              bounce: 0.7
            )
            }
          }
      }
    }

     

    이렇게 적절히 처리해주면 어떤 애니메이션이 나올까요?

     

    아주 원하는 애니메이션을 쉽게 구현할 수 있겠어요!

     

    이제는 Keyframes에 대해 알아보겠습니다🙋🏻

     

    Keyframes

    키프레임을 이용해 애니메이션을 더욱 발전시키는 방법을 보겠습니다.

    더 많은 제어가 필요할 경우에는 키프레임을 이용할 수 있습니다.

    타이밍과 움직임을 완벽히 제어하면서 복잡한 애니메이션을 구성할 수 있죠.

    지금까지 위에서 봤던 애니메이션은 순차적이지만 그 Phase 안에서는 offset과 scale등이 상태 전환이 발생하면 속성들이 동시에 애니메이션 됩니다.

    즉 해당 상태에서 갖는 애니메이션들은 모두 동시에 작동하는것이죠🙋🏻

     

    그럼 이 속성들을 독립적으로 애니메이션 하려면 어떻게 해야 할까요?

    바로 키프레임을 사용하면 됩니다!

     

    키프레임을 사용하면 애니메이션 내에서 특정 시간에 값을 정의할 수 있습니다.

    여기서의 점들이 키프레임을 나타냅니다.

    즉 애니메이션 중 각 지점에서 사용할 각도를 나타내죠.

    애니메이션이 재생되고 SwiftUI는 이러한 키프레임 사이에 값을 보간해 뷰에 모디파이어를 적용하면서 사용합니다.

     

    그럼 이 키프레임이 어떻게 코드에서 보이는지 살펴보시죠🕺🏻

     

    가장 우선적으로 애니메이션을 구동할 속성을 정의해야 합니다.

    이를 위해 독립적으로 애니메이션을 적용할 다양한 프로퍼티들을 포함하는 구조체를 만듭니다.

    키프레임은 Animatable 프로토콜을 준수하는 모든 값에 애니메이션을 적용할 수 있죠.

    이 키프레임을 통해 애니메이션 진행되는 동안 SwiftUI는 뷰를 업데이트 할 수 있도록 모든 프레임에서 이 유형의 값을 제공하죠.

    struct AnimationValues {
      var scale = 1.0
      var verticalStretch = 1.0
      var verticalTranslation = 0.0
      var angle = Angle.zero
    }

     

    그 다음으로 keyframeAnimator 모디파이어를 사용합니다.

    private struct KeyframesView: View {
      var xcodeImage: Image = Image("XcodeImage")
      
      @State private var selected: Bool = false
      
      var body: some View {
        xcodeImage
          .onTapGesture {
            withAnimation {
              selected.toggle()
            }
          }
          .keyframeAnimator(
            initialValue: AnimationValues(),
            trigger: selected
          ) { content, value in
            content
              .rotationEffect(value.angle)
              .scaleEffect(value.scale)
              .scaleEffect(y: value.verticalStretch)
              .offset(y: value.verticalTranslation)
          } keyframes: { _ in
            KeyframeTrack(\.scale) {
              LinearKeyframe(1.0, duration: 0.5)
              SpringKeyframe(1.5, duration: 0.8, spring: .bouncy)
              SpringKeyframe(1.0, spring: .bouncy)
            }
          }
      }
    }

    코드를 보면 이렇습니다.

    초기 값으로 만들어진 키프레임 구조체 인스턴스를 넣어줍니다.

    그 다음 애니메이션을 동작시킬 트리거를 넣습니다.

    그 후 value 값에 따라 content의 변화를 주도록 모디파이어를 설정합니다.

     

    마지막으로 keyframes 클로저를 이용하는데요.

    여기서 KeyframeTrack을 이용해 초기값으로 넣어준 키프레임의 프로퍼티들로 트랙을 구성해줍니다.

    각 키프레임 구조체가 4가지 존재하는데 여기서는 우선 두가지를 사용하였어요.

    각 키프레임을 통해 가질 scale의 값과 지속 시간을 설정하죠.

     

    그럼 어떻게 보일까요?

     

    키프레임 트랙 순서대로 잘 구현되는걸 볼 수 있어요.

    물론 여러 angle부터 offset까지 같이 구성해줄 수 있습니다.

     

    모두 다 지정해주면 이런 애니메이션도 가능하죠!

    즉 좀 더 시간에 따라 각 프로퍼티들을 가지고 독립적으로 애니메이션을 복합으로 구성할 수 있습니다.

     

    그럼 조금 더 공식문서를 보면서 살펴볼까요?

     

    KeyframeAnimator

    키프레임 애니메이터는 콘텐츠에 애니메이션 효과를 주는 컨테이너에요.

    struct KeyframeAnimator<Value, KeyframePath, Content> where Value == KeyframePath.Value, KeyframePath : Keyframes, Content : View

    클로저의 content는 애니메이션 하는 동안 모든 프레임을 업데이트 하기때문에 너무 많은 비용이 드는 작업을 수행하면 위험합니다.

    커스텀한 KeyframePath와 Content를 가지고 구성됩니다.

     

    SwiftUI에서는 키프레임을 만들기 위해 세가지 모디파이어를 제공합니다.

     

    1️⃣ 트리거 없이 계속 반복

    func keyframeAnimator<Value>(
        initialValue: Value,
        @ViewBuilder content: @escaping (PlaceholderContentView<Self>, Value) -> some View,
        @KeyframesBuilder<Value> keyframes: @escaping (Value) -> some Keyframes
    ) -> some View

     

    2️⃣ 트리거를 두어 한번만 실행

    func keyframeAnimator<Value>(
        initialValue: Value,
        trigger: some Equatable,
        @ViewBuilder content: @escaping (PlaceholderContentView<Self>, Value) -> some View,
        @KeyframesBuilder<Value> keyframes: @escaping (Value) -> some Keyframes
    ) -> some View

    지정된 트리거의 값이 변경되면 지정된 키프레임을 재생하여 적용한 모디파이어를 이용해 뷰를 업데이트 합니다.

     

    3️⃣ 무한 반복

    func keyframeAnimator<Value>(
        initialValue: Value,
        repeating: Bool = true,
        @ViewBuilder content: @escaping (PlaceholderContentView<Self>, Value) -> some View,
        @KeyframesBuilder<Value> keyframes: @escaping (Value) -> some Keyframes
    ) -> some View

    repeating 기본값이 true이기에 사실 1번과 3번의 이니셜라이저는 겹치는 느낌이긴 합니다.

     

    Keyframes

    시간 경과에 따른 값의 변경 사항을 정의하는 프로토콜입니다.

    protocol Keyframes<Value>

    해당 프로토콜을 KeyframeTrack과 @KeyframesBuilder에서 채택하여 결국 바디를 구성해주죠.

     

    KeyframeTimeline

    키프레임을 이용해 모델링한 시간 경과에 따른 값의 변화에 대한 설명입니다.

    struct KeyframeTimeline<Value>

    SwiftUI의 다른 애니메이션과 달리 키프레임은 SwiftUI가 상태 변경으로 제공하는동안 이전과 이후 사이 값을 보간해주지 않습니다.

    대신 키프레임은 본체를 구성하는 트랙을 사용해 시간이 지남에 따라 값이 취하는 경로를 완전히 정의하죠.

     

    KeyframeTrack

    루트 유형의 단일 프로퍼티에 애니메이션을 적용시키는 일련의 키프레임입니다.

    struct KeyframeTrack<Root, Value, Content> where Value == Content.Value, Content : KeyframeTrackContent

    Keyframes와 Sendable을 준수하죠.

    사용은 위 예시 코드에서 보셨던것과 같이 작성합니다.

     

    KeyframeTrackContentBuilder

    클로저 내 정의한 키프레임에 대한 키프레임 트랙 컨텐츠를 생성하는 빌더입니다.

    @resultBuilder
    struct KeyframeTrackContentBuilder<Value> where Value : Animatable

     

    KeyframesBuilder

    키프레임 컨텐츠 값을 단일 값으로 결합해주는 빌더입니다.

    @resultBuilder
    struct KeyframesBuilder<Value>

    즉 BuildBlock을 내부적으로 사용해 하나의 View로 구성해주는것이죠.

     

    KeyframeTrackContent

    애니메이션 가능한 값의 보간 곡선을 정의하는 키프레임 그룹 프로토콜입니다.

    protocol KeyframeTrackContent<Value>

    즉 앞으로 나올 4가지의 키프레임 구조체는 이 프로토콜을 따릅니다.

     

    자 이제 재밌는거 해볼께요!

     

    바로 4가지의 키프레임입니다.

    즉, 어떻게 보간되는지를 결정해줍니다.

     

    LinearKeyframe

    이전 키프레임의 벡터 공간에서 선형으로 보간됩니다.

    struct LinearKeyframe<Value> where Value : Animatable

    키프레임 값과 duration 그리고 timingCurve를 가집니다.

    여기서 timingCurve는 UnitCurve 타입으로 보간 속도를 제어하는 단위 곡선입니다.

     

    SpringKeyframe

    스프링 함수를 이용해 이전 키프레임에서 대상 값을 보간합니다.

    struct SpringKeyframe<Value> where Value : Animatable

    4가지의 인자를 가집니다.

    🍏🍏🍏🍏🍏🍏

    1️⃣ to: 키프레임의 값

    2️⃣ duration: 지속 시간

    3️⃣ spring: 키프레임에 연결된 세그먼트의 모양을 정의하는 스프링 (snappy, smooth, bouncy)

    4️⃣ startVelocity: 세그먼트 시작 시 값 혹은 nil로 부드러운 동작 유지를 위해 자동으로 계산합니다.

     

    CubicKeyframe

    3차 베지어 곡선을 사용해 키프레임을 보간합니다.

    여러 큐빅 키프레임을 순서대로 결합하면 결과 곡선은 Catmull-Rom 스플라인과 동일합니다.

    struct CubicKeyframe<Value> where Value : Animatable

    다른 키프레임과 마찬가지로 to, duration을 가지며 startVelocity와 endVelocity를 가지고 시작/종료 속도를 지정해줄 수 있습니다.

    지정하지 않는다면 SwiftUI가 자동으로 키프레임 간 부드러운 동작을 유지하는 곡선을 계산합니다.

     

    MoveKeyframe

    보간 없이 즉시 값으로 이동합니다.

    struct MoveKeyframe<Value> where Value : Animatable

     

    즉 정리해서 요런 키프레임들을 각 프로퍼티에 적용하여 동시적으로 애니메이션을 줄 수 있습니다🙌

     

    자 그럼 키프레임을 떠나기전 마지막으로 LinearKeyframe에서 나온 UnitCurve는 뭘까요?

     

    UnitCurve

    단위 곡선으로 0~1 범위로 매핑하는 2차원 곡선으로 정의된 함수입니다.

    곡선의 모양을 변경하여 애니메이션 또는 기타 보간의 속도를 변경할 수 있죠.

    struct UnitCurve

    정의된 static 프로퍼티들이 아래와 같이 있습니다.

     

    후 길었네요... 이제 마지막으로 Tip & Trikcs를 보겠습니다🙌

     

    Tip & Tricks

    앞서 본 API들을 최대한 활용하기 위해 몇가지 팁과 요령을 보겠습니다.

    MapKit에서 이러한것들을 유용하게 사용할 수 있다고해요!

    즉 경로를 따라 맵을 보면서 확대되고 줄어들고 이런 애니메이션을 줄 수 있죠.

     

    그리고 위에서 잠깐 살펴봤던 KeyframeTimeLine이 있습니다.

    이렇게 KeyframeTimeLine을 외부에서 구성해 키프레임의 전체 시간 및 특정 시간에 대한 값들을 얻어올 수 있습니다.

     

    이렇게 무궁무진하게 커스텀하게 사용할 수 있습니다🙃

     

    마무리

    너무 방대한 애니메이션이 들어왔는데 확실히 너무 좋아졌고 애니메이션을 SwiftUI에서 다채롭게 해줄 수 있네요!

    해당 위 코드들에 대한 삽질? 혹은 연습은 제 깃헙 레포에서 확인하실 수 있습니다.

    https://github.com/GREENOVER/playground/tree/main/animation_practice

     

    GitHub - GREENOVER/playground: 학습을 하며 간단한 예제들을 만들어 보는 작고 소중한 놀이터

    학습을 하며 간단한 예제들을 만들어 보는 작고 소중한 놀이터. Contribute to GREENOVER/playground development by creating an account on GitHub.

    github.com

     

     

    참고 자료

    https://developer.apple.com/wwdc23/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

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

     

    KeyframeAnimator | Apple Developer Documentation

    A container that animates its content with keyframes.

    developer.apple.com

Designed by Tistory.