-
SwiftUI 애니메이션 살펴보기
SwiftUI의 강력한 애니메이션 기능을 살펴보고, 그 기능들을 함께 활용하여 인상적인 시각 효과를 만들어내는 방법을 알아보세요. SwiftUI로 뷰 렌더링을 새로 고치고, 애니메이션 적용 대상을 결정하고, 시간에 따라 값을 보간하며, 현재 트랜잭션에 컨텍스트를 전파하는 방법을 알아보세요.
챕터
- 1:03 - Anatomy of an update
- 6:40 - Animatable
- 11:36 - Animation
- 20:00 - Transaction
리소스
관련 비디오
WWDC23
-
다운로드
안녕하세요 SwiftUI 팀원 카일입니다 애니메이션은 현대 앱 디자인의 핵심 요소입니다 감각적인 애니메이션으로 UI에 선명함과 생동감을 부여하죠 저희가 SwiftUI를 개발하게 된 핵심 동기는 앱에 애니메이션을 쉽게 추가할 수 있도록 하는 거였죠 그래서 SwiftUI가 지금의 모습이 된 겁니다 이 세션에서는 SwiftUI의 강력한 애니메이션 기능과 인상적인 시각 효과를 만드는 방법을 알아볼 겁니다 지금부터 저와 함께 SwiftUI로 뷰의 렌더링을 새로 고치고 Animatable을 사용해 애니메이션 적용 대상을 결정하며 Animation으로 시간에 따른 값을 보간하고 Transaction으로 현재 업데이트의 콘텍스트 전파 방법을 살펴봅니다
최근 몇 년 동안 동료들 사이에서 어떤 반려동물이 최고인지를 두고 논쟁이 있었습니다 합의점을 찾기가 어려워 투표용 앱을 만들었죠
각 반려동물에 대응하는 투표 버튼이 있는데 버튼을 탭하면 득표수가 바뀌고 아바타가 현재 순위를 반영하여 움직입니다 마지막 투표에서는 예상대로 고양이가 1위를 했습니다 근소한 차이였죠 그냥 운에 맡긴 채 이대로 질 수는 없었기 때문에 새로운 기능을 추가했습니다 버튼을 탭하면 선택한 아바타가 커지면서 사람들이 그 반려동물에 투표하도록 유도할 수 있습니다 한 번 더 탭하면 원래 크기로 돌아가죠 기능은 잘 작동하고 있지만 애니메이션을 추가하면 훨씬 나을 겁니다 애니메이션을 추가하기 전에 SwiftUI가 뷰 렌더링을 새로 고치는 방법을 추적해 뷰 업데이트의 구조를 이해하고 넘어갑시다 이 예제에서는 반려동물 아바타를 집중적으로 살펴보겠습니다 SwiftUI는 화면에 보이는 상태 변수 selected 같은 뷰의 종속 항목을 추적합니다 화면을 탭하는 이벤트가 발생하면 업데이트 트랜잭션이 열립니다
종속 항목 중 하나라도 변경되면 뷰가 무효화되며 트랜잭션이 종료될 때 프레임워크는 본문을 호출해 새 값을 생성하여 렌더링을 새로 고칩니다
이 뷰의 본문은 탭 제스처와 스케일 효과, 이미지로 구성됩니다
코드 뒤에는 뷰와 뷰 데이터의 수명을 관리하는 장기 종속성 그래프가 있습니다 이 그래프 속 각각의 노드는 속성이라고 부르며 UI의 세부 항목에 매핑됩니다 selected 상태 변수가 true로 변경되면 각 다운스트림 속성값이 만료됩니다 새 뷰 값을 한 번에 한 레이어씩 언랩하며 새로 고치죠
해당하는 그래프 속성이 업데이트되면 뷰 본문 값은 폐기됩니다
마지막으로, 그래프가 렌더링 업데이트를 위해 사용자 대신 드로잉 커맨드를 보냅니다
속성의 수명을 시각화하기 위해 그래프만 확대해 보겠습니다
속성은 초깃값으로 생성됩니다 이벤트가 입력되면 업데이트 트랜잭션이 열립니다 업스트림 종속성이 변경되고 프레임워크는 본문을 호출하죠 속성값이 업데이트되고 트랜잭션이 닫힙니다 이런 식으로 그래프의 속성값은 시간에 따라 진화합니다
뷰 업데이트의 구조는 이렇습니다 이제 애니메이션을 추가해 봅시다
내 상태 변경을 withAnimation으로 랩핑하면 탭 제스처 클로저가 실행될 때 트랜잭션 애니메이션이 설정됩니다 그 후 selected가 토글되고 다운스트림 속성이 만료됩니다 전과 마찬가지로 본문이 호출되어 새 속성값을 제공합니다
여기가 흥미로운 지점이죠 scaleEffect는 특수 속성으로 애니메이션이 가능한 속성입니다 애니메이션이 가능한 속성의 값이 변경되면 트랜잭션에 애니메이션이 설정되어 있는지 확인합니다 설정되어 있다면 사본을 만들고 애니메이션을 사용해 시간에 따라 이전 값에서 새 값으로 보간합니다
애니메이션이 가능한 속성인 scaleEffect를 확대하여 작동 원리를 살펴봅시다 먼저 눈에 띄는 건 애니메이션이 가능한 속성이 모델값과 프레젠테이션값을 모두 갖고 있다는 겁니다 지금은 두 값이 동일합니다 이벤트가 입력되고 트랜잭션이 열립니다 이번에는 애니메이션이 포함되었죠 상태가 변경되고 본문이 호출되어 만료된 속성값을 새로 고칩니다 값이 변경되었으므로 속성은 애니메이션의 로컬 사본을 만들어 현재 프레젠테이션값을 계산하죠
SwiftUI는 애니메이션이 속성 그래프에 포함되는 시점을 파악해 적절한 애니메이션이 가능한 속성을 호출한 뒤 다음 프레임을 생성합니다 scaleEffect처럼 내장된 애니메이션이 가능한 속성엔 SwiftUI가 효율적입니다 뷰 코드를 호출하지 않고도 메인 스레드에서 이 작업을 수행할 수 있죠 애니메이션이 작동하는 걸 봅시다
잘됐네요 애니메이션이라는 말은 보통 시간에 따라 뷰가 변하는 전반적인 시각적 경험을 의미하죠
지금까지 살펴본 결과 SwiftUI의 전반적인 시각 경험에는 두 가지 직교적인 측면이 기여하고 있습니다 scaleEffect처럼 애니메이션이 가능한 속성은 애니메이션을 적용할 데이터를 결정하는 반면 애니메이션은 시간에 따른 데이터의 변화를 결정합니다 하나씩 더 자세히 살펴봅시다 애니메이션 대상을 결정하는 Animatable부터 시작하죠 SwiftUI는 애니메이션이 가능한 프로토콜을 준수하는 모든 뷰에 애니메이션이 가능한 속성을 빌드합니다 단, 뷰가 애니메이션을 적용하려는 데이터의 읽기-쓰기 벡터를 정의해야 합니다 데이터는 VectorArithmetic을 준수해야 하죠 VectorArithmetic은 수학 교과서의 벡터 정의와 같은 의미입니다 벡터 덧셈과 스칼라곱을 지원합니다 벡터가 가물가물하거나 잘 모르더라도 걱정하지 마세요 벡터는 기본적으로 길이가 고정된 숫자 목록입니다 SwiftUI 애니메이션에서 벡터는 대체로 그 목록의 길이를 추상화하기 위해 쓰입니다 예를 들어, CGFloat와 Double은 1차원 벡터를 정의하고 CGPoint와 CGSize는 2차원 벡터를 정의하며 CGRect는 4차원 벡터를 정의합니다
SwiftUI는 벡터를 통해 단일한 범용 구현으로 이 모든 유형의 애니메이션을 구현합니다
지금까지는 간단한 설명을 위해 scaleEffect를 1차원 스케일 요소처럼 표현해 봤습니다
1차원 scaleEffect의 애니메이션 가능 적합성은 간단합니다 animatableData가 그냥 CGFloat죠 실제로는 scaleEffect를 사용해 독립적으로 변환의 너비와 높이 상대 앵커 포인트를 구성할 수 있고 모두 애니메이션도 적용 가능합니다 실제로 scaleEffect는 애니메이션이 가능한 데이터의 4차원 벡터를 정의하며 너비와 높이 스케일에 대한 CGSize와 상대 앵커의 UnitPoint와 한 쌍인 4차원 벡터도 정의합니다
AnimatablePair는 두 벡터를 더 큰 하나의 벡터로 합칩니다 공개된 유형이므로 여러분도 쓸 수 있습니다 여러분의 뷰를 Animatable에 맞추려 할 때 유용하죠
scaleEffect는 SwiftUI에 내장된 애니메이션이 가능한 여러 시각 효과 중 하나일 뿐이니 보통은 Animatable API를 직접 사용할 필요가 없습니다 하지만 드물게 고급 사용 사례에서 뷰를 Animatable에 맞춰야 할 때가 있죠 반려동물 Podium 뷰를 예시로 보면 커스텀 RadialLayout으로 원의 호를 따라 서브뷰를 배포합니다 기본값으로 애니메이션 내에서 오프셋 각도를 변경하면 반려동물 아바타가 직선 경로의 새 위치로 애니메이션됩니다 반려동물들이 최단 거리로 이동해 원 안쪽을 침범하고 있죠? 제가 원했던 동선이 아닙니다
아바타가 원의 둘레를 따라 움직였으면 좋겠어요 그러려면 Podium을 애니메이션 가능으로 맞추고 오프셋 각도를 애니메이션 가능 데이터로 사용합니다
설명을 위해 아바타가 직선으로 움직였던 기본 동작부터 Podium 뷰의 각 버전에 대한 애니메이션 업데이트를 단계별로 살펴보겠습니다
Podium의 본문은 RadialLayout과 세 아바타로 구성됩니다 트랜잭션이 열릴 때 오프셋 각도가 변경되었다면 본문을 호출해 만료된 다운스트림 속성값을 새로 고칩니다 그러면 레이아웃이 실행되어 서브뷰의 위치를 업데이트합니다 기본 버전의 애니메이션 업데이트 과정은 이렇습니다 애니메이션 가능 활성 데이터는 뷰 위치의 CGPoint이며 직교 좌표 공간에서 보간됩니다 즉, 아바타가 직선을 따라 움직인다는 걸 의미하죠 커스텀 버전에서는 Podium을 Animatable에 맞춰 본문이 애니메이션이 가능한 활성 속성이 되고 오프셋 각도가 애니메이션이 가능한 데이터가 됩니다 이렇게 하면 왜 아바타가 원을 따라 움직일까요?
이 커스텀 버전에서는 애니메이션의 모든 프레임에 대해 SwiftUI가 새로운 오프셋 각도로 본문을 호출하고 레이아웃이 다시 실행됩니다
정말 강력한 기능이죠 때때로 커스텀 레이아웃이나 드로잉 코드를 애니메이팅할 때 원하는 효과를 얻는 유일한 방법이기도 합니다 유의할 점은 커스텀한 애니메이션 가능 적합성은 애니메이션의 모든 프레임에 바디를 실행하기 때문에 애니메이션 비용이 훨씬 많이 들 수 있습니다 그러니 이 기능은 내장된 효과만으로 원하는 결과를 얻을 수 없을 때만 활용하세요 다음으로 Animation을 살펴보죠 애니메이션 가능 데이터를 시간에 따라 보간하는 범용 알고리듬입니다
상태 변화를 withAnimation으로 래핑하여 반려동물 아바타 뷰에 애니메이션을 추가했었죠
스프링 효과 같은 특정한 애니메이션을 전달해 사용자화할 수도 있습니다
SwiftUI에는 여러 강력한 애니메이션이 내장되어 있습니다 크게 다음 세 가지로 분류할 수 있죠 타이밍 커브 애니메이션과 스프링 애니메이션 기본 애니메이션을 수정한 고차원 애니메이션입니다
타이밍 커브 애니메이션은 가장 익숙한 애니메이션 범주일 겁니다 예를 들어, easeInOut도 타이밍 커브 애니메이션이죠
타이밍 커브 애니메이션은 애니메이션의 속도를 정의하는 커브와 지속 시간을 갖습니다
타이밍 커브는 베지어 곡선의 제어점을 사용해 만듭니다 시작 및 종료 제어점을 조정해 애니메이션의 초기 및 최종 속도를 변경합니다
UnitCurve 유형은 독립적으로 사용 가능하며 0과 1 사이의 상대 지점에서 값과 속도를 계산합니다
SwiftUI에는 다양한 타이밍 커브 프리셋이 내장되어 있습니다 Linear와 easeIn easeOut 그리고 easeInOut입니다
타이밍 커브 애니메이션은 지속 시간을 사용자화할 수 있죠
다음 카테고리인 스프링 애니메이션은 스프링 시뮬레이션을 실행해 주어진 시점의 값을 결정합니다
스프링을 조절하는 기존 방식을 이미 알고 있는 분도 계실 겁니다 예를 들면 질량, 강성, 감쇠인데요 하지만 이런 방식은 직관적이지 않아서 새로운 방법을 고안했습니다 간단하게 애니메이션의 인식 지속 시간과 스프링의 탄력 정도를 지정하면 됩니다 훨씬 직관적이죠
UnitCurve와 비슷하게 스프링 유형은 독립적으로 특정 시간의 스프링값과 속도 계산에도 쓸 수 있습니다
SwiftUI에는 스프링 프리셋 세 가지가 내장되어 있습니다 탄력이 없는 smooth와 약간의 탄력이 있는 snappy 탄력이 큰 bouncy입니다 스프링 애니메이션을 매개변수로 쓰는 게 불편하다면 이런 프리셋을 사용해 좋은 느낌을 얻어낼 수 있습니다
여러분에게 스프링 애니메이션을 적극 추천합니다 속도를 유지하다가 자연스럽게 멈추는 방식으로 UI에 유기적인 느낌을 주기 때문이죠 실제로도 스프링 애니메이션의 장점이 크다고 여겨 iOS 17과 관련 릴리즈에서 smooth 스프링을 새 기본값으로 설정하여 withAnimation을 사용했죠
마지막 범주 고차원 애니메이션은 기본 애니메이션을 수정합니다 애니메이션을 느리게 하거나 빠르게 하죠 기본 애니메이션이 시작하기 전에 지연 시간을 넣을 수도 있습니다 기본 애니메이션을 몇 번이고 반복할 수도 있고 정방향 재생과 역방향 재생을 토글 설정 할 수도 있습니다
이제 완전히 새로운 범주의 애니메이션을 소개합니다 바로 커스텀 애니메이션입니다 CustomAnimation 프로토콜로 SwiftUI 내장 애니메이션 구현에 사용된 것과 동일한 하위 레벨 범용 진입점에 엑세스할 수 있습니다 CustomAnimation 프로토콜에는 세 가지 함수가 필요합니다 animate, shouldMerge 그리고 velocity입니다
animate를 중점으로 살펴봅시다 shouldMerge와 velocity는 선택 사항이므로 나중에 살펴볼 겁니다
animate에는 애니메이션을 적용할 벡터 애니메이션 시작 후 경과 시간 추가 애니메이션 상태를 포함하는 콘텍스트가 들어갑니다 animate는 애니메이션의 현재 값을 반환하거나 애니메이션이 끝났다면 nil을 반환합니다 value 벡터의 출처는 어디일까요? 바로 뷰의 애니메이션 가능 데이터입니다 반려동물 아바타 뷰에서는 스케일 효과죠 scaleEffect의 애니메이션 가능 데이터는 4차원 벡터라는 걸 떠올려 보세요 2차원 너비와 높이 스케일을 포함하죠 아바타를 선택하면 애니메이션 스케일 계수가 1 대 1에서 1.5 대 1.5로 적용됩니다 벡터 덧셈과 스칼라 곱 연산으로 SwiftUI에서 이 두 벡터를 서로 빼 두 벡터 사이의 델타를 계산할 수 있습니다
즉, scaleEffect 애니메이션 가능 속성에서 실행되는 애니메이션은 1에서 1.5로 보간하는 게 아니라 0에서 0.5로 보간한다는 것입니다 이를 통해 애니메이션 메서드 구현이 더 편리해집니다 직접 보여드리죠
보간 지속 시간으로 구성된 리니어 타이밍 커브 애니메이션을 구현해 보겠습니다
애니메이션을 적용할 델타 벡터가 전달된다는 점을 떠올려 보세요 스칼라 곱을 사용하여 경과한 기간의 비율만큼 벡터의 크기를 조정합니다 전체 기간이 경과하면 애니메이션이 완료되어 제거할 수 있음을 표시하기 위해 nil을 반환합니다 이게 끝입니다 범용성이 있는 구현이므로 여러 차원의 애니메이션이 가능한 데이터에서 작동합니다 Animatable과 Animation을 함께 활용해 인상적인 UI 시각 효과를 만들어 봤습니다
다음으로 CustomAnimation의 두 선택 요건인 shouldMerge와 velocity를 살펴봅시다 어떤 용도일까요?
애니메이션 가능 속성 scaleEffect의 입장에서 볼까요? 사용자가 탭하고 트랜잭션이 열립니다 값이 변경되며 애니메이션의 로컬 복사본을 만들죠 그리고 신나게 델타 벡터를 애니메이션하기 시작할 겁니다 모든 게 순조롭습니다 성가신 사용자가 애니메이션이 끝나기 전에 다시 탭을 하는군요 그럼 어떻게 될까요? 새 애니메이션을 설정하고 shouldMerge를 호출합니다
기본 구현으로 false를 반환합니다 타이밍 커브 애니메이션의 역할이죠 이 경우, 두 애니메이션이 함께 실행되고 그 결과가 시스템에 의해 결합됩니다 SwiftUI 애니메이션이 델타 벡터로 처리하는 또 하나의 이유입니다 여러 애니메이션이 실행 중일 때 정확한 결합 프레젠테이션값을 쉽게 계산할 수 있습니다 타이밍 커브 애니메이션 대신 스프링 애니메이션을 선택했다면 어떨까요? 스프링 애니메이션은 shouldMerge를 재정의해 true를 반환하고 이전 애니메이션 상태를 통합하죠 이렇게 하면 속도를 유지하면서 새 값으로 리타기팅할 수 있어 타이밍 커브 애니메이션처럼 덧셈으로 결합하는 것보다 더 자연스럽게 느껴질 겁니다 마지막 함수 velocity의 용도를 살펴봤습니다 이를 통해 실행 중인 애니메이션이 새 애니메이션과 병합될 때 속도를 유지할 수 있습니다 이제 리니어 타임 커브 애니메이션은 velocity 구현을 끝으로 마무리하겠습니다
이 강연에서 '트랜잭션'은 특정 UI 업데이트에 수행되는 일련의 작업을 지칭하기 위해 사용했습니다 SwiftUI 코드에서 Transaction은 이와 관련한 강력한 데이터 흐름 구조와 API 제품군을 의미하기도 합니다 Environment와 Preferences는 이미 익숙하신 분도 있을 겁니다 SwiftUI의 뷰 계층 위아래에 각각 암시적으로 전달하는 딕셔너리죠 Transaction도 유사합니다 딕셔너리의 하나로, SwiftUI가 현재 업데이트에 대한 콘텍스트 특히 애니메이션을 암시적으로 전파하는 데 사용합니다
앞선 설명에서는 애니메이션 가능 속성이 어떻게 애니메이션을 읽는지가 다소 모호했습니다 아바타 뷰의 다른 애니메이션 업데이트를 추적해 봅시다 이번에는 구체적으로 설명하겠습니다 탭 제스처 클로저가 실행되면 withAnimation이 루트 트랜잭션 딕셔너리에서 애니메이션을 설정합니다
본문이 호출되어 속성값을 업데이트합니다 트랜잭션 딕셔너리가 속성 그래프에 전파됩니다 애니메이션 가능 속성에 도달하면 속성은 애니메이션의 설정 여부를 확인합니다 설정되었다면 사본을 만들어 프레젠테이션값을 구동하죠 트랜잭션은 특정 업데이트에만 관련되므로 만료된 속성이 새로 고쳐지면 트랜잭션은 폐기됩니다 트랜잭션 딕셔너리 내에서 애니메이션을 뷰 계층 구조 아래로 흐르게 하면 애니메이션이 뷰에 적용되는 시기와 방법을 제어하는 여러 강력한 API를 사용할 수 있죠
현재 반려동물 아바타 뷰는 탭으로만 선택할 수 있습니다 selected 상태 변수를 바인딩으로 바꿔 봅시다 그러면 프로그래밍 방식으로도 선택할 수 있습니다
뷰 속성을 프로그래밍 방식으로 어떻게 애니메이팅할까요? 트랜잭션 수정자를 사용해 애니메이션에 엑세스해 트랜잭션 딕셔너리 내부의 뷰 계층 구조를 따라 내려갑니다
이 수정자 내에서 애니메이션을 설정하면 본문이 호출될 때마다 트랜잭션에 애니메이션이 없거나 다른 애니메이션이 있어도 속성이 애니메이션을 재정의합니다 스케일 효과에 다다르면 애니메이션이 스케일 계수 보간에 사용됩니다
하지만 이 패턴에는 문제가 있습니다 SwiftUI가 뷰를 새로 고칠 때마다 모든 파생 애니메이션을 무차별적으로 재정의하면 돌발 애니메이션이 발생할 수 있습니다 이런 사용 예를 위해 SwiftUI는 애니메이션 뷰 수정자를 제공합니다 이 수정자는 추가로 값 인수를 받아 훨씬 더 정확하게 효과의 범위를 지정합니다 값이 변경될 때만 애니메이션을 트랜잭션에 기록하죠
이제 연결되었으니 이 withAnimation은 아무 작업도 수행하지 않으므로 제거해도 됩니다
애니메이션 뷰 수정자는 뷰의 여러 부분에 다른 애니메이션을 적용하려 할 때도 유용합니다
예를 들어, 반려동물 아바타에는 그림자가 있지만 설명을 간단하게 하려고 생략했었는데요 아바타를 선택하면 그림자의 반경이 증가하며 아바타가 배경 위로 올라간 듯한 느낌을 줍니다
실제로 써 보고 나니 그림자 애니메이션을 스케일 효과보다 차분하게 만들고 싶어졌습니다 이를 위해 스케일 효과와 그림자 사이에 다른 애니메이션 뷰 수정자를 삽입합니다 이제 트랜잭션이 스케일 효과 애니메이팅에 쓸 bouncy 스프링을 선택합니다
그림자 반경 애니메이팅에는 좀 더 약한 smooth 스프링을 선택합니다
애니메이션 수정자는 값이 변경되었을 때만 활성화되므로 돌발 애니메이션이 발생할 확률이 크게 줄어듭니다 하지만 아바타 이미지가 우연히 같은 트랜잭션에서 변경되었다면 콘텐츠 트랜잭션을 위해 smooth 스프링 애니메이션을 이어받았을 겁니다 더 깊이 들여다봅시다 이 애니메이션 뷰 수정자는 전체 하위 계층 구조를 제어할 수 있는 리프 컴포넌트에서 잘 작동합니다 하지만 임의의 하위 콘텐츠를 포함하는 비 리프 컴포넌트에서는 돌발 애니메이션 발생률이 훨씬 높아집니다
예를 들어 반려동물과 관련이 없는 다른 앱에서 아바타를 재사용하려면 임의의 하위 콘텐츠를 허용하여 일반적인 아바타를 만들 수 있죠 이 시나리오에서는 선택 항목이 변경될 때 하위 콘텐츠도 변경되지 않을 확률이 낮습니다
즉, 돌발 애니메이션 발생 확률이 높아지죠 이런 좋은 소식입니다 이 같은 사용 사례를 위해 특별히 설계된 새 버전의 애니메이션 뷰 수정자도 있습니다 이 수정자는 애니메이션의 범위를 본문 클로저에 지정된 애니메이션이 가능한 속성으로 좁힙니다 작동법을 살펴봅시다 트랜잭션에 애니메이션이 없다고 가정해 보죠 트랜잭션이 애니메이션 뷰 수정자 속성에 도달하면 지정된 애니메이션으로 채워진 사본이 만들어집니다 사본이 다운스트림으로 전파되지만 그 대상은 범위가 지정된 애니메이션 가능 속성뿐이죠 작업이 완료되면 사본은 삭제되고 원본 트랜잭션이 중단 지점부터 재개됩니다
즉, 트랜잭션이 하위 콘텐츠에 도달할 때 원본 트랜잭션은 중간 애니메이션 뷰 수정자의 영향을 받지 않으므로 돌발 애니메이션이 발생할 위험이 없습니다 제한된 트랜잭션 API는 SwiftUI 초기 버전부터 사용할 수 있었습니다 이제 커스텀 트랜잭션 키를 정의하는 기능이 도입되어 트랜잭션 딕셔너리를 활용해 업데이트 관련 데이터를 암시적으로 전파할 수 있습니다
커스텀 환경 키를 정의해 본 적이 있다면 커스텀 트랜잭션 키를 정의하는 것도 익숙할 겁니다 트랜잭션 키 프로토콜을 준수하는 고유한 유형을 생성하는 패턴이죠 유일한 요구 사항은 defaultValue를 제공하는 겁니다 그런 다음, 키를 사용하여 트랜잭션 딕셔너리에서 읽고 쓰는 계산된 프로퍼티를 트랜잭션의 확장으로 정의합니다 여기서는 불리언 키를 정의해 주어진 업데이트에 아바타의 탭 여부를 추적합니다 그 값에 따라 어떤 애니메이션을 사용할지 결정할 겁니다 아바타가 대화형으로 선택되었다면 더 생동감 있는 스프링을 써 아바타의 크기를 늘리고 줄입니다 아바타가 프로그래밍 방식으로 선택되었다면 더 차분한 스프링을 사용해 크기를 조정합니다 상태 변경을 트랜잭션으로 래핑해 주어진 업데이트의 트랜잭션 딕셔너리값을 설정할 수 있습니다 아마 익숙할 겁니다 withAnimation은 withTransaction을 둘러싼 얇은 래퍼입니다
withTransaction에 전달되는 인수는 트랜잭션의 계산된 프로퍼티 키 경로와 설정할 값입니다
SwiftUI의 암시적 데이터 흐름 구조체에서 트랜잭션은 고유합니다 업데이트가 끝날 때마다 버려지기 때문이죠 즉, 현재 업데이트에 대해 명시적으로 설정하지 않는 한 모든 트랜잭션 딕셔너리값은 해당 키의 기본값으로 돌아갑니다
아바타 뷰에서 탭 제스처 클로저가 실행되면 현재 업데이트에 대해 avatarTapped가 true로 설정됩니다
트랜잭션에는 애니메이션 키의 기본값인 nil도 포함됩니다
트랜잭션은 트랜잭션 수정자에 도달할 때까지 뷰 계층 구조를 통해 전파됩니다 여기서 아바타 뷰는 avatarTapped를 읽고 그 값에 따라 적절한 애니메이션을 설정해
이 방법은 꽤 괜찮지만 앞서 살펴보았듯이 돌발 애니메이션이 발생할 수 있습니다 트랜잭션 수정을 더 세밀하게 제어할 수 있도록 트랜잭션 수정자의 새 변형을 두 가지 소개합니다 하나는 값 인수를 사용해 범위를 지정할 수 있습니다 다른 하나는 본문 클로저에 정의된 하위 계층으로 범위를 지정할 수 있습니다 이 변형들은 앞서 다룬 범위 지정 애니메이션 뷰 변형자를 반영하죠
이번 세션에는 SwiftUI의 강력한 애니메이션 프리미티브인 Animatable과 Animation Transaction을 설명해 드렸습니다
다음 단계로 아래 두 세션을 살펴보시길 바랍니다 '스프링 애니메이션 만들기'에서는 앱에서 스프링 애니메이션을 효과적으로 사용하는 방법과 이유에 대해 상세히 다룹니다 'SwiftUI의 고급 애니메이션 활용하기'에서는 다단계 애니메이션을 구성하기 위한 강력한 새 도구를 소개합니다 이 콘텐츠를 통해 SwiftUI 애니메이션의 작동 방식을 더 잘 이해하고 앱에서 애니메이션을 더 능숙하게 활용할 수 있기를 바랍니다 감사합니다 ♪ ♪
-
-
2:14 - Pet Avatar - Unanimated
struct Avatar: View { var pet: Pet private var selected: Bool = false var body: some View { Image(pet.type) .scaleEffect(selected ? 1.5 : 1.0) .onTapGesture { selected.toggle() } } }
-
4:13 - Pet Avatar - Animated
struct Avatar: View { var pet: Pet private var selected: Bool = false var body: some View { Image(pet.type) .scaleEffect(selected ? 1.5 : 1.0) .onTapGesture { withAnimation { selected.toggle() } } } }
-
11:49 - Pet Avatar - Explicit Animation
struct Avatar: View { var pet: Pet private var selected: Bool = false var body: some View { Image(pet.type) .scaleEffect(selected ? 1.5 : 1.0) .onTapGesture { withAnimation(.bouncy) { selected.toggle() } } } }
-
12:48 - UnitCurve Model
let curve = UnitCurve( startControlPoint: UnitPoint(x: 0.25, y: 0.1), endControlPoint: UnitPoint(x: 0.25, y: 1)) curve.value(at: 0.25) curve.velocity(at: 0.25)
-
13:56 - Spring Model
let spring = Spring(duration: 1.0, bounce: 0) spring.value(target: 1, time: 0.25) spring.velocity(target: 1, time: 0.25)
-
17:25 - MyLinearAnimation
struct MyLinearAnimation: CustomAnimation { var duration: TimeInterval func animate<V: VectorArithmetic>( value: V, time: TimeInterval, context: inout AnimationContext<V> ) -> V? { if time <= duration { value.scaled(by: time / duration) } else { nil // animation has finished } } }
-
19:50 - MyLinearAnimation with Velocity
struct MyLinearAnimation: CustomAnimation { var duration: TimeInterval func animate<V: VectorArithmetic>( value: V, time: TimeInterval, context: inout AnimationContext<V> ) -> V? { if time <= duration { value.scaled(by: time / duration) } else { nil // animation has finished } } func velocity<V: VectorArithmetic>( value: V, time: TimeInterval, context: AnimationContext<V> ) -> V? { value.scaled(by: 1.0 / duration) } }
-
22:44 - Pet Avatar - Animation Modifier
struct Avatar: View { var pet: Pet var selected: Bool var body: some View { Image(pet.type) .scaleEffect(selected ? 1.5 : 1.0) .animation(.bouncy, value: selected) .onTapGesture { selected.toggle() } } }
-
23:44 - Pet Avatar - Multiple Animation Modifiers
struct Avatar: View { var pet: Pet 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() } } }
-
25:20 - Generic Avatar - Scoped Animation Modifiers
struct Avatar<Content: View>: View { var content: Content 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() } } }
-
28:45 - Pet Avatar - Transaction Modifier
struct Avatar: View { var pet: Pet var selected: Bool var body: some View { Image(pet.type) .scaleEffect(selected ? 1.5 : 1.0) .transaction(value: selected) { $0.animation = $0.avatarTapped ? .bouncy : .smooth } .onTapGesture { withTransaction(\.avatarTapped, true) { selected.toggle() } } } } private struct AvatarTappedKey: TransactionKey { static let defaultValue = false } extension Transaction { var avatarTapped: Bool { get { self[AvatarTappedKey.self] } set { self[AvatarTappedKey.self] = newValue } } }
-
28:58 - Generic Avatar - Scoped Transaction Modifier
struct Avatar<Content: View>: View { var content: Content var selected: Bool var body: some View { content .transaction { $0.animation = $0.avatarTapped ? .bouncy : .smooth } body: { $0.scaleEffect(selected ? 1.5 : 1.0) } .onTapGesture { withTransaction(\.avatarTapped, true) { selected.toggle() } } } } private struct AvatarTappedKey: TransactionKey { static let defaultValue = false } extension Transaction { var avatarTapped: Bool { get { self[AvatarTappedKey.self] } set { self[AvatarTappedKey.self] = newValue } } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.