iOS에서 사용할 수 있는 애니메이션 구현 기법들에 대한 정리

애니메이션 구현 방법

iOS의 애니메이션 구현은 크게 두 가지로 나눌 수 있는데 하나는 UIKit의 애니메이션 API를 사용하는 것이고, 다른 하나는 코어 애니메이션을 사용하는 것이다. 오늘은 각각의 세부적인 구현보다는 각각의 API의 차이와 기본적인 사용방법에 대해서 살펴보도록 하겠다.

UIKit의 애니메이션

구버전의 API

UIKit은 전통적으로 UIView.beginAnimations(), UIView.commitAnimations()라는 두 개의 클래스 메소드를 이용한 방법을 사용한다. 이 API의 사용방법은 다음과 같다.

  1. UIView.beginAnimations() 메소드를 호출한다.
  2. 이 메소드가 호출되고 나면, 화면상의 뷰의 위치나 색상등 시각적 표현과 관련된 코드는 즉각적으로 뷰에 반영되지 않는다.
  3. 이와 비슷하게 UIView의 클래스 메소드를 호출하여 애니메이션의 지속시간 등의 애니메이션 속성을 지정하거나 애니메이션이 완료되거나 중단되었을 때의 동작을 셀렉터를 통해서 지정할 수 있다.
  4. 애니메이션의 내용과 속성에 대한 지정이 완료되면 UIView.commitAnimations()를 호출한다. 이 메소드가 호출되면 시스템은 별도의 스레드에서 애니메이션을 계산하고 화면에 표현한다.

대략 이런 방식이다. (Swift)

UIView.beginAnimations()
self.circleView.frame = CGRect(x:100, y:20, width:100, height:100)
UIView.commitAnimations()

이 방식은 무려 iOS4 전의 것으로, 이 때는 코드블럭이 도입되기 전이었다. 이 방식은 사용하기는 어렵지 않았지만, 애니메이션 지정 구간이 문법적으로 구분되지 않고 또 애니메이션 자체의 속성과 애니메이션과 관련된 이벤트처리가 셀렉터로만 넘겨질 수 있기 때문에 애니메이션 코드를 작성하는 것이 번거로웠다.

코드블럭을 사용한 새로운 API

iOS4에서부터 코드 블럭 문법이 도입되면서 앞의 문법의 문제점을 수정한 버전의 API가 새로 도입되었다. 이는 UIView.animate(withDuration:animations:) 와 그 패밀리 함수들을 사용하는 것이다.

UIView.animate(withDuration: 1.5){
    circleView.center = CGPoint(x:100, y:20)
}

지속시간, 애니메이션의 옵션(타이밍 함수 등), 완료핸들러등을 메소드의 각 인자로 분리하고, 애니메이션의 내용은 클로저(=코드블럭)안에 작성하여 가독성을 높이고, 특히 중첩된(nested) 애니메이션 구조를 알아보기 쉽게 작성할 수 있는 장점이 있으며 현재에도 가장 널리 쓰이는 애니메이션이다.

이 API와 관련된 함수들은 아래와 같은 것들이 있다.

  • +animate(withDuration:animations:) – 기본 타이밍 함수를 이용해서 애니메이트
  • +animate(withDuration:animations:completion:) – 완료핸들러를 추가
  • +animate(withDuration:delay:options:animations:completion:) – 딜레이 및 다른 애니메이션 옵션을 사용할 수 있게 한다.
  • +animate(withDuration:delay: usingSpringWithDamping:initialSpringVelocity: options:animations:completion:) – 일반적인 곡선 타이밍 함수가 아닌 스프링에 의한 타이밍함수를 사용하여 애니메이팅하는 함수 (iOS10)
  • +animateKeyframes(withDuration:delay:options: animations:completion:) – 키 프레임 애니메이션 표시
  • +transition(with:duration:options: animations:completion:) – 뷰 트랜지션
  • +transition(from:to:duration:options:completion:) – 제3의 뷰를 트랜지션
  • +perform(_:on:options:animations:completion:)
  • +performWithoutAnimation(_:)

UIKit Dynamics

UIView를 사용한 애니메이션은 뷰의 시작상태와 끝 상태를 프로그래머가 이미 알고 있고, 시스템은 지속시간과 타이밍 곡선을 이용하여 그 중간 상태의 증분값들을 계산하여 중간 프레임들을 자동으로 만들어 화면에 표현하는 기능을 수행한다.

UIKit Dynamics는 새롭게 도입된 애니메이션 엔진으로 2차원 뷰에 대해서 물리 연산을 수행한다. 따라서 중력이나 힘 벡터에 의해서 가속되며 서로 충돌하고 되튀거나 특정 지점을 따라 연결되거나, 어느 위치에 용수철을 이용해서 달라붙어 있는 등의 효과를 계산한다. 이는 기존의 UIKit 애니메이션이 알려진 두 지점간의 애니메이션을 생성했던데 비해서, 각 시각 요소들의 크기와 밀도, 힘 등의 물리적 속성에 의해 예측하지 못했던 애니메이션을 만들 수 있는 장점이 있다.

대신에 Dynamic Animation은 일반적인 UIView 애니메이션의 목적과는 달리 자연스러운 물리효과를 시각화하는 대 주안점을 두고 있음은 알아두도록 하자.

동역학 애니메이션은 다음과 같이 구현한다.

  1. 애니메이션에 참가할 뷰들을 가지고 UIDynamicBehavior의 서브 크래스의 인스턴스를 생성한다.
  2. 호스트 뷰(혹은 레퍼런싱 뷰)를 기반으로 하는 UIDynamicAnimator 인스턴스를 생성한다.
  3. 2의 애니메이터에 1의 behavior 객체들을 추가한다.
  4. 애니메이터는 행동양식이 추가되면, 그와 관련된 뷰 객체들에 미치는 각 힘과 상호작용을 계산하여 애니메이션을 생성하고 이를 호스트 뷰에 반영한다.

UIViewPropertyAnimator를 이용하는 방법

기존의 UIView 애니메이션은 비록 iOS4 이후에 API의 개선이 있기는 했지만 “단순한 뷰 상태 변경에 대한 증분값의 자동 내삽”이라는 범위를 벗어나지 못했다는 것이다. 즉 애니메이션 자체가 외부와 상호작용하게끔 구현하는 것이 끔찍하게 어렵거나 불가능한 점이 있었다.

사용자의 입력과 상호작용하는 애니메이션이 필요한 경우는 다음과 같은 것들이 있을 수 있는데…

  • 메뉴가 펼쳐지거나 하는 애니메이션이 진행되는 중간에 사용자가 작업을 취소하는 경우, 진행되던 애니메이션이 되감기 하여 원상태로 복귀하게 하려는 경우
  • A 지점으로 이동하던 뷰가 중간에 B 지점으로 목적지를 변경하는 경우. 기존 API에서는 B로 점프하여 A로 가거나, 중간 지점에서 날카롭게 B로 꺾였는데, 이를 자연스럽게 전환하고 싶은 경우.
  • 애니메이션의 일시 정지와 재개
  • 애니메이션 진행 상태를 사용자 입력에 맞추어 앞으로 빠르게 감거나 되감기 하여 다른 진행률로 이동하는 경우. 예를 들어 슬라이더를 앞으로 밀면 애니메이션이 슬라이더의 값에 따라 반응하는 경우

기존 UIView 애니메이션 API에서는 이러한 기능을 구현하는 것이 쉽지 않았다. 왜냐하면 이러한 기능을 구현하려면 애니메이션의 매 프레임의 상태가 실제적 값이어야 하는데, 기존 API에서 매 프레임의 상태는 시스템 프레임워크 내부의 private한 값으로 상태가 제어되고 있고, 개발자에게 노출되는 값은 시작값과 종료값밖에 없기 때문이었다. 1

이러한 한계를 극복하고 인터렉티브한 애니메이션을 가능하게 해주는 기능이 iOS10에서 도입되었는데 이것이 바로 UIViewPropertyAnimator 클래스이다. 이 클래스는 지속시간과 타이밍 곡선 그리고 애니메이션의 내용을 담은 클로저를 기반으로 생성된다.

이후 애니메이션이 시작되었을 때, 이를 일시정지나 중단할 수 있고, 역방향으로 되감거나 특정 위치로 옮겨가는 등의 동작을 처리할 수 있게 해준다. 사용법이 매우 직관적이기 때문에 쓰기도 쉽다.

let animator = UIViewPropertyAnimator(duration: 2.0,
        curve: .easeInOut) {
  circleView.center = CGPoint(x: 100, y: 80)
}
animator.startAnimations()
/// 이후 animator를 이용하여 여러 상호작용이 가능함.

이 방식의 놀라운 점은 애니메이션이 “자동으로 진행될 필요가 없는” 상황을 상정한다는 것이다. 예를 들어 뷰의 색상이 파랑에서 노랑으로 변하는 애니메이션을 만들어 놓고 일시정지 상태로 시작한다. 그런다음 뷰를 끌고 다닐 때 위치에 따라서 애니메이션의 진행률이 그에 따라 변하게 만들면 위치에 따라 색이 자연스럽게 변하는 효과를 만들 수 있다.

코어 애니메이션

코어 애니메이션은 UIKit 및 AppKit과 그래픽 하드웨어 및 OpenGL 사이의 계층에 있는 그래픽 드로잉 및 애니메이팅 인프라로, 뷰와 기타 시각 요소들에 애니메이션을 적용하는데 사용된다.

뷰에 대해 간단한 애니메이션을 사용하려는 경우에 반드시 코어 애니메이션을 직접 다룰 필요는 없지만, 그것이 앱의 인프라로서 수행하는 역할을 이해하는 것은 애니메이션 품질과 앱의 성능을 향상시키는 돌파구를 찾는데 있어서 중요하다.

코어 애니메이션의 핵심 클래스는 바로 CALayer로 이는 화면에 시각적으로 표시되는 콘텐츠를 캐리하는 역할을 담당하며, 모서리, 마스크, 그림자등의 드로잉 효과를 적용할 수 있으며, 애니메이션 시에는 이 콘텐츠를 비트맵으로 캐싱하여 사용한다. 이러한 과정에서 코어 애니메이션은 오직 CPU에만 의존하는 Core Graphic과는 달리 GPU에 의한 하드웨어 가속을 적극 지원한다. 따라서 UI를 일일이 그려서 표현하는 경우에 코어 그래픽보다 더 나은 성능을 낼 수 있기도 하다.

CALayer는 그리고자 하는 대상의 특성에 맞게 CAShapeLayer, CATileLayer, CATextLayer 등의 세부 서브 클래스들이 정의되어 있다.

한편 기존에 “화면에 표시되는 기준”인 UIView 클래스는 내부적으로 CALayer를 가지고 있고 (이를 layer-backing view라 한다) 프로그래머는 이 CALayer를 뷰의 콘텐츠에 맞게 다른 CALayer의 서브클래스 객체로 교체하여 사용할 수 있다.

코어 애니메이션은 크게 두 가지의 애니메이션을 지원하는데, 하나는 ‘암시적(implicit)’ 애니메이션이고 다른 하나는 명시적 애니메이션이다.

CALayer 객체에서 뷰(혹은 레이어)의 위치, 크기, 변형, 배경색, 투명도 등 시각적 특성을 나타내는 프로퍼티들을 변경하면, 각 프로퍼티의 키에 대해서 어떤 액션(=주로 애니메이션)이 정의되어 있는데 이 액션들에 의해 애니메이션이 구동된다.

명시적 애니메이션은 어떤 레이어 객체에서 애니메이트 되어야 하는 프로퍼티의 키패스에 대해 CAAnimation 객체를 생성하고 이 애니메이션 객체에서 애니메이션에 대한 세부 사항을 기술해준 다음, 애니메이션 객체를 레이어 객체에 ‘추가’하게되면 해당 애니메이션이 실행된다.

코어 애니메이션은 별도의 스레드에서 실행되며, 앞서도 말했듯이 그래픽 하드웨어 가속을 사용하므로 앱이 애니메이션 때문에 특별히 성능이 저하되는 일은 별로 없다. 또 최근 출시된 iOS 기기들의 그래픽 처리 성능이 상당히 좋아졌기 때문에 코어 애니메이션을 기반으로 게임을 만들어도 될 정도로 애니메이션 품질또한 매우 좋은 편이다.

UIKit은 내부적으로 코어 애니메이션과 밀접하게 연관되어 있고, 간단한 뷰 애니메이션만 사용하는 경우라면 위에서 언급한 UIView의 클래스 메소드들을 사용하는 애니메이션으로 충분히 구현가능할 것이다.

하지만 코어 애니메이션의 진정한 진가는 CALayer의 동작 방식에 있다. CALayer의 애니메이션은 기본적으로 프로퍼티의 키패스의 값의 변화를 추적하고 증분값을 계산하여 이를 매 프레임마다 표시하는 것인데, 이 과정은 다음과 같다.

  1. CALayer 클래스는 특정 키패스의 값이 변경될 때 이를 애니메이팅할 것인지를 결정하는 needsDisplay(forKey:)에 의해 결정한다.
  2. CALayer 클래스나 인스턴스는 특정한 키 패스 값이 변경될 때 이를 처리할 액션을 정의할 수 있으며 (action(forKey:)), 레이어의 기하학적/시각적 속성은 이미 기본적으로 이런 액션들을 정의해 두고 있다.
  3. 특정한 키패스의 값이 변경되며, 해당 키패스에 해당하는 액션 객체가 생성되고, 이것이 레이어에 add 되면서 실행된다. 액션은 매 프레임마다 display()를 호출한다.
  4. CALayer는 2개의 레이어 상태를 유지하는데 하나는 모델, 다른 하나는 프레젠테이션 레이어이다. 전자는 레이어 객체의 실제 속성값을 그대로 나타내는 값이며, 후자는 애니메이션이 진행되는 매 프레임마다 표시되는 일종의 캐시가 같는 순간값들이다.
  5. 2에서 결정된 액션은 매 프레임마다 display() 메소드를 호출하고, 이 메소드는 프레젠테이션 레이어의 상태를 화면에 렌더링하는 일을 수행한다.

따라서 만약 우리가 CALayer를 서브클래싱하면서 특정한 프로퍼티를 추가하고, needsDisplay(forKey:), action(forKey:), display() 세 개의 메소드를 올바르게 오버라이딩한다면 (심지어 시각적으로 표현되는 것과 아무런 관련이 없는) 값의 변경을 애니메이팅할 수 있다. 2

참고로 CALayer는 앞서 설명한 것처럼 UIView로 할 수 있는 많은 것들을 실질적으로 지원해주고 있으며, 여러가지 효과와 필터들을 적용할 수 있고 심지어 빠르기 까지 하기 때문에 애니메이션과 상관없이 복잡한 그래픽을 사용하는 UI를 구성하는데에도 괜찮은 선택이 된다.


  1. 물론 CALayer를 이용해서 현재 상태에 대한 상태값을 얻는 것은 불가능한 것은 아니지만, 이 때에는 코어 애니메이션을 사용해야 하며, 여러 UI 요소에 대해서 하나의 값으로 맵핑하는 설계가 복잡해질 가능성도 있다. 
  2. 숫자를 표시하는 UILabel에서 변경된 숫자를 즉시 반영하는 것이 아니라 순차적으로 값이 변경되게 할 수 있다.