Skip to content

[정주] WWDC23 ‐ Explore SwiftUI Animation 정리

유정주 JeongJu Yu edited this page Aug 4, 2024 · 1 revision

SwiftUI 애니메이션 살펴보기

  • SwiftUI를 개발하게 된 핵심 동기는 앱에 애니메이션을 쉽게 추가할 수 있도록 하는 것
  • Anatomy of an update (뷰의 렌더링을 업데이트)
  • Animatable로 애니메이션 적용 대상을 결정
  • Animation으로 시간에 따른 값을 보간, Transaction으로 현재 업데이트의 Context 전파 방법 살펴보기

View Rendering 업데이트

  • SwiftUI는 화면에 보이는 종속 상태를 추적함
  • 종속 항목이 하나라도 변경되면 View가 무효화
  • 각각의 노드를 Attribute라고 말함. Attribute는 UI의 세부 항목에 매핑됨
스크린샷 2024-08-04 13 16 55
  • 트랜잭션이 종료될 때 프레임워크는 body를 호출해서 새 값을 생성 후 렌더링을 새로 고침함
    • Tap이벤트 발생 → 새로운 트랜잭션 생성 → @State 값 변경 → View 무효화 → body 호출 → 값이 업데이트 → 트랜잭션 닫힘
    • 스크린샷 2024-08-04 13 24 49

애니메이션

  • withAnimation {}을 사용하면 트랜잭션 애니메이션이 설정됨
스크린샷 2024-08-04 13 24 03
  • 애니메이션이 가능한 속성(e.g. scaleEffect)의 값이 변경되면 트랜잭션에 애니메이션이 설정되어 있는지 확인함
    • 설정되어 있다면 copy하고 애니메이션을 사용해 시간에 따라 이전 값에서 새 값으로 변화함
  • 애니메이션이 가능한 속성은 modal과 presentation 값을 모두 가지고 있음
  • SwiftUI는 애니메이션이 속성 그래프에 포함되는 시점을 파악 후 적절한 애니메이션이 가능한 속성을 호출, 다음 프레임 생성
    • scaleEffect처럼 자동으로 애니메이션이 가능한 속성이라면 SwiftUI가 효율적임
    • 스크린샷 2024-08-04 13 27 46

Animatable과 Animation

  • Animatable은 애니메이션을 적용할 데이터를 결정
  • Animation은 시간에 따른 데이터의 변화를 결정함

Animatable

스크린샷 2024-08-04 13 39 58
  • Animatable을 채택하면 animatableData 속성을 준수해야 함

    • animatableData: 애니메이션이 가능한 속성
    public protocol Animatable {
    	associatedtype AnimatableData: VectorArithmetic
    	var animatableData: AnimatableData { get set }
    }
  • Animatable은 View에 맞춰 애니메이션을 커스텀하고 싶을 때 사용함

    • RadialLayout은 기본적으로 최단 거리로 애니메이션됨
      • 세 개의 Avatar가 원 중앙을 거쳐서 최단 거리 직선으로 이동
    • Podium의 Angle값을 이용해서 정의해서 호를 따라 이동하도록 구현할 수 있음
    • 스크린샷 2024-08-04 13 46 25
    • 딱 한 번만 계산하던 흐름에서 SwiftUI가 매번 각도를 계산해서 레이아웃을 다시 실행하도록 하기 때문
    • 커스텀한 애니메이션은 매 애니메이션 프레임마다 body를 실행하기 때문에 애니메이션 비용이 훨씬 많이 든다.
    • 따라서 내장된 애니메이션으로 원하는 결과를 얻을 수 없을 때만 사용해야 함 → 가벼운 마음으로 사용하면 안 됨

Animation

  • SwiftUI에는 여러 강력한 애니메이션이 내장되어 있음
    • Timing curve
    • Spring
    • Higher order (기본 애니메이션을 수정한 고차원 애니메이션)
  • Timing curve는 애니메이션 속도를 정의하는 curve와 duration을 가짐
    • curve는 Bezier 곡성의 제어점을 사용해 만듬
    • duration을 커스텀할 수 있음
  • Spring 애니메이션은 스프링 시뮬레이션을 실행해 주어진 시점의 값을 결정함
    • mass, stiffeness, damping을 조절하는 방식이었는데, 이걸 조절하는건 직관적이지 않음
    • 그래서 bounce를 조절하는 방식으로 변경함
    • duration과 extraBounce(탄력도)를 조절할 수 있음
    • Spring 애니메이션은 UI에 유기적인 느낌을 주기 때문에 강추
      • iOS 17에서는 withAnimation의 기본값이 Spring임
  • Higher order 애니메이션은 기본 애니메이션을 커스텀할 수 있음
    • 반복, 속도변경, 역재생 등

Custom Animation

  • Custom Animation 프로토콜이 추가됨

  • animate, shouldMerge, velocity 메서드를 구현해야 함

    • animate는 필수, 나머지는 선택
    public protocol CustomAnimation: Hashable {
    	func animate<V: VectorArithmetic>(
    		value: V, time: TimeInterval, context: inout AnimationContext<V>
    	) -> V?
    }
  • shouldMerge는 애니메이션 도중에 새로운 애니메이션이 생성됐을 때 애니메이션 상태를 통합할 수 있음

  • velocity는 애니메이션이 통합될 때 속도를 유지할 수 있음

Transaction

  • Dictionary를 사용하여 애니메이션 상태를 기록한다.

  • animation 모디파이어를 사용해 특정 값이 바뀌었을 때 애니메이션을 트랜잭션에 기록할 수 있다.

    • transaction 을 사용해서 여러 애니메이션을 정의하면 예상치 못한 돌발 애니메이션이 발생할 수 있음
    • 이를 방지하기 위해 animation 모디파이어가 생김
  • animation 모디파이어의 순서를 조정하면 효과에 따라 적용할 애니메이션을 다르게 기록할 수 있음

    struct Avatar: View {
        var pet: Pet
        @Binding var selected: Bool
    
        var body: some View {
            Image(pet.type)
                .shadow(radius: selected ? 12 : 8)
                .animation(.smooth, value: selected)
                .scaleEffect(selected ? 1.5 : 1.0)
                .animation(.bouncy, value: selected)
                .onTapGesture {
                    selected.toggle()
                }
        }
    }
    • shadow에는 smooth, scaleEffect에는 bouncy가 적용됨
      • 아래에서 위로 적용되는 게 포인트
      • bouncy로 기록 → scaleEffect에 적용 → smooth로 기록 → shadow에 적용
    • Dictionary로 관리되기 때문에 가능한 원리
  • Leaf 컴포너트가 아니면 돌발 애니메이션이 발생할 확률이 훨씬 높아짐

    • 이전에 적용된 트랜잭션 기록을 이어 받을 수 있기 때문
    • 이 상황을 방지하기 위해 Scoped Animation 모디파이어가 생김
    struct Avatar<Content: View>: View {
        var content: Content
        @Binding var selected: Bool
    
        var body: some View {
            content
                .animation(.smooth) {
                    $0.shadow(radius: selected ? 12 : 8)
                }
                .animation(.bouncy) {
                    $0.scaleEffect(selected ? 1.5 : 1.0)
                }
                .onTapGesture {
                    selected.toggle()
                }
        }
    }
    • 특정 범위에만 애니메이션을 적용함
      • 애니메이션 트랜잭션을 복사한 뒤 사본에만 애니메이션을 기록하여 적용하는 원리
      • 스크린샷 2024-08-04 14 15 52
      • 애니메이션 작업이 완료되면 사본은 제거됨
      • 원본은 그대로 애니메이션이 nil이므로 하위 View에는 애니메이션이 적용되지 않음
  • 트랜잭션 Key를 적용하여 트랜잭션 Dictionary 값을 설정할 수 있음

  • 모든 트랜잭션 값을 구조체 생명주기에서 고유함. 트랜잭션은 View 업데이트가 끝날 때마다 버려지기 때문

    • 매 업데이트마다 명시적으로 트랜잭션을 정의해야 함
    • 그렇지 않으면 기본값으로 설정됨
  • Transaction 모디파이어도 돌발 애니메이션을 방지하기 위해 값을 설정하거나 body로 범위를 지정할 수 있음

Clone this wiki locally