이전 포스팅에선 Core Animation과 CALayer의 기본 개념에 대해 알아보았다. 

이번 포스팅에선 Custom Animation을 구현해보자.

 

특정 Layer에 Animation을 추가하고 싶다면 add(_:forKey:) 메서드를 통해 추가한다.

func add(
    _ anim: CAAnimation,
    forKey key: String?
)

 

이때 CAAnimation을 통해 Animation을 정의할 수 있는데,

이는 abstract class이기 때문에 다음과 같은 concrete subclass를 사용한다. 

  • CABasicAnimation
  • CAKeyframeAnimation
  • CAAnimationGroup
  • CATransition

이렇게 여러가지가 있지만, Core Animation 중 가장 기본적인 클래스인 

CABasicAnimation만 제대로 파악하면 나머지는 어렵지 않다. 

 

CABasicAnimation

CABacisAnimationlayer의 프로퍼티에 대해 기본적인, "single-keyframe animation"을 제공하는 객체이다.

 

우선 "key frame"은 영상에서 쓰이는 용어이다. 어떤 물체가 이동할 때 전체 frame을 생성하는 것이 아닌, 

중간중간 핵심 frame(key frame)만 생성하고 이를 연결해 영상을 만든다. 

 

CABasicAnimation은 "single keyframe"으로 시작값과 최종값만 설정해 Animation을 생성한다.

 

또한, layer 프로퍼티에 대해 Animation을 적용하기에, 다양한 Animation을 생성할 수 있다. 

layer.backgroundColor 
layer.position
layer.transform.rotatation.z 
...

 

Example - Move

우선 x축 방향으로 이동 예시부터 살펴보자.

Layer에선 position을 통해 위치를 옮긴다. 

물론 frame으로도 가능하지만, frame은 bounds와 position에서 파생되는 값이기에 
frame보단 position을 사용하는 것이 좋다. 
// 1. Animation 생성
let animation = CABasicAnimation()
animation.keyPath = "position.x"

// 2. Animation의 속성 지정
animation.fromValue = 20 + 140/2
animation.toValue = 300
animation.duration = 0.5

// 3. Layer에 animation 추가
animationView.layer.add(animation, forKey: "Move")

 

  1. CABasicAnimation은 다음과 같이 keyPath를 통해 Layer객체에서 Animation을 적용할 프로퍼티를 지정한다.
  2. 다음으로 Layer 프로퍼티의 처음값과 마지막 값을 설정하면, 나머지는 Core Animation이 Interporlation한다. 또한, duration으로 지속시간을 지정할 수 있는데 default는 0.25이다.
  3. 최종적으로 적용할 Layer에 animation을 추가한다. 

다음과 같이 keyPath만 변경해주면 여러 Animation이 가능하다.

// 1. Animation 생성
let animation = CABasicAnimation()
animation.keyPath = "transform.scale"

// 2. Animation의 속성 지정
animation.fromValue = 1
animation.toValue = 2
animation.duration = 0.5

// 3. Layer에 animation 추가
animationView.layer.add(animation, forKey: "Scale")

// 4. 최종상태 변경
animationView.layer.transform = CATransform3DMakeScale(2, 2, 1)
// 1. Animation 생성
let animation = CABasicAnimation()
animation.keyPath = "transform.rotation.z"

// 2. Animation의 속성 지정
animation.fromValue = 0
animation.toValue = CGFloat.pi / 2
animation.duration = 0.5

// 3. Layer에 animation 추가
animationView.layer.add(animation, forKey: "Rotate")

// 4. 최종상태 변경
animationView.layer.transform = CATransform3DMakeRotation(CGFloat.pi / 2, 0, 0, 1)

 

Animation 상태 유지하기 

하지만 다음과 같이 Animation이 끝난 후 최종 상태가 아닌 Animation 시작 전의 상태로 돌아오는 것을 볼 수 가 있다. 

 

Core Animation은 Animation 상태를 저장하는 "Presentation Layer"와 현재 상태를 저장하는 "Model Layer"를 가지고 있다. 

우선, 앞선 코드에선 "Presentation Layer"에 Animation 상태를 저장했지만,

현재 상태는 변경하지 않았기 때문에 위와 같은 문제가 발생하게 된다.

animation.toValue = 300

 

기본적으로 Swift에선 이러한 Animation은 한번 수행되면 remove되기 때문에,

Animation이 끝나면 Animation의 상태는 유지되지 않고, 기존의 Model Layer의 상태로 돌아가게 된다. 

 

Animation이 끝난 후 최종상태를 유지하기 위한 방법은 다음과 같다. 

  • Model Layer의 상태를 변경하기. 
  • Animation이 끝난후 삭제되지 않게 하기. 

 

Model Layer 상태 변경

위와 같은 문제는 Presentation Layer의 상태는 변경했지만, 

Animation이 끝나고 Animation이 remove되어 Model Layer의 상태로 돌아가게 되기 때문이다. 

 

따라서, 가장 간단한 해결방법은 "Model  Layer"의 현재 상태를 변경하면 된다. 

// 4. 최종위치 변경
animationView.layer.position = CGPoint(x: 300, y: 100 + 100/2)

 

fillMode 

두번째 방법으로는 Animation이 remove되지 않도록 하는것이다. 

 

우선 삭제되지 않도록,  isRemovedOnCompletionfalse로 설정하고,

fillModeforwards로 설정하면 된다. 

animation.fillMode = .forwards
animation.isRemovedOnCompletion = false

fillModeLayer의 Animation이 끝난 후 혹은 시작 전에 Presentation의 상태를 유지할지 제거할지를 결정한다. 

fillMode에는 다음과 같은 값들을 설정할 수 있다. 

  • removed(default) : Animtion이 끝나면 Presentation의 상태를 제거한다. 
  • backwards : Animation 시작전에 Presentation의 첫번째 상태(첫번째 프레임)을 보여준다. 
  • forwards : Animation 완료 후 Presentation의 마지막 상태(마지막 프레임)을 보여준다.
  • both :  backwards + forwards

 

 

CAKeyFrameAnimation

"CAKeyFrameAnimation"은 layer객체에 대해 keyFrame Animation을 제공한다.

 

기존의 CABasicAnimation은 Single-Frame으로 처음과 끝의 상태만 전달이 가능했지만, 

CAKeyFrameAnimation에선 여러개의 KeyFrame을 전달할 수 있다.

 

Shake View 

다음과 같이 x축으로 흔들리는 Animation을 구현해보자. 이는 SingleFrame으로 구현할 수 없기 때문에, CAKeyFrameAnimation을 사용한다. 

// 1. Animation 생성
let animation = CAKeyframeAnimation()
animation.keyPath = "position.x"

// 2. keyFrame에서의 layer 속성 값들을 기입
animation.values = [0, 10, -10, 10, 0]
// 3. 각 keyFrame에서의 시간을 지정 (0~1사이 값을 가진다.)
animation.keyTimes = [0, 0.16, 0.5, 0.83, 1]
animation.duration = 0.5

// 4. 현재 속성값을 기준으로 values가 계산됨
animation.isAdditive = true
animationView.layer.add(animation, forKey: "Shake")

 

우선 values에 각 KeyFrame에서 Layer의 상태를 기입하면 되는데, 

이때 isAdditivetrue로 설정하게 되면 현재 값을 기준으로 상대적으로 계산하게 된다. 

 

Circuling View 

다음으로 다음과 같이 원형의 경로를 따라 이동하는 Animation을 만들자. 

이렇게 Animation이 특정 경로를 따라 이동하는 경우 path프로퍼티를 사용한다. 

let boundingRect = CGRect(x: -diameter/2, y: -diameter/2, width: diameter, height: diameter)

let animation = CAKeyframeAnimation()
animation.keyPath = "position"

// 1. 경로 설정
animation.path = CGPath(ellipseIn: boundingRect, transform: nil)
animation.duration = 2

// 2. boundingRect을 상대값으로 x,y좌표를 잡았기 때문에 true로 설정
animation.isAdditive = true

// 3. keyFrame간에 어떻게 Interpolation할지 결정한다.
animation.calculationMode = .paced
animationView.layer.add(animation, forKey: "Shake")

 

CASpringAnimation

말그대로 Spring처럼 탄성 있는 Animation을 만들어내는 클래스이다. 

let animation = CASpringAnimation()
animation.keyPath = "transform.scale"

// 2. Animation의 속성 지정
animation.fromValue = 1
animation.toValue = 2
animation.duration = 0.5
// 3. 값을 크게 주면 탄성이 심해지고, 적게 주면 탄성이 줄어든다.
animation.damping = 2.0

// 4. Layer에 animation 추가
animationView.layer.add(animation, forKey: "Scale")

// 5. 최종상태 변경
animationView.layer.transform = CATransform3DMakeScale(2, 2, 1)

 

 

CAAnimationGroup

여러 Animation을 하나의 Animation인 것처럼 그룹을 지어줄 수 있다. 

다음과 같이 FadeOut과 Scale 여러개의 Animation을 하나의 Animation처럼 사용하고 싶은 경우 CAAnimationGroup을 사용한다. 

// 1. fadeOut Animation
let fadeOut = CABasicAnimation(keyPath: "opacity")
fadeOut.fromValue = 1
fadeOut.toValue = 0
fadeOut.duration = 1

// 2. Scale Animation
let expandScale = CABasicAnimation()
expandScale.keyPath = "transform.scale"
expandScale.fromValue = 1
expandScale.toValue = 3

// 3. Animation Group
let fadeAndScale = CAAnimationGroup()
fadeAndScale.animations = [fadeOut, expandScale]
fadeAndScale.duration = 1

animationView.layer.add(fadeAndScale, forKey: "FadeAndScale")

 

우선, duration의 경우, AnimationGroup의 duration이 우선시 된다. 

즉, 각 animation이 1.5초이고 group의 duration이 1초인 경우 animation은 처음 1초만 수행되고 짤리게 된다. 

 

또한, 앞서 결과를 봤듯이 그룹의 각 Animation들을 동시에 수행되는거지 순차적으로 수행되지는 않는다.

따라서, 서로다른 Animation을 순차적으로 배치하고 싶다면, 다음과 같이 직접 계산해줘야 한다.

// 1. fadeOut Animation
let fadeOut = CABasicAnimation(keyPath: "opacity")
...
fadeOut.duration = 1

// 2. Scale Animation
let expandScale = CABasicAnimation(keyPath: "transform.scale")
...
expandScale.beginTime = CACurrentMediaTime() + 1.0
expandScale.duration = 1

// 3. Animation Group
let fadeAndScale = CAAnimationGroup()
fadeAndScale.animations = [fadeOut, expandScale]
fadeAndScale.duration = 2

 

 

CATransition

마지막으로 "CATransition"은 Layer State간의 Transition effect를 쉽게 할 수 있도록 제공한다.

다음과 같이 Transition Effect를 typesubtype 프로퍼티를 통해 지정할 수 있다. 

let transition = CATransition()
transition.duration = 0.25

transition.type = .push
transition.subtype = .fromLeft

animationView.layer.add(transition, forKey: "transition")
animationView.backgroundColor = .blue

더 많은 typesubtype은 apple 공식문서에 나와있다.

 

 

References

Apple Documentation 

 

https://github.com/jrasmusson/swift-arcade/blob/master/Animation/CoreAnimation/Intro/README.md

복사했습니다!