이 글에서는 iOS의 CAAnimation을 활용해 부드럽고 자연스러운 애니메이션을 구현하는 방법을 다룹니다.
iOS 개발을 시작한 초창기, 디자이너가 다가와 "앱에 애니메이션 좀 넣어주세요"라고 요청할 때면 저는 늘 마음이 조마조마했습니다.

애니메이션의 디자인을 기획하는 것은 어렵지 않다고 생각했습니다. 하지만 막상 그것을 코드로 구현하는 일은 전혀 다른 문제였습니다.
구현에 막힐 때마다 Google, StackOverflow, 그리고 동료들에게 도움을 청했고, 그 과정에서 어느새 '애니메이션 공포증'이 생겨 항상 피하려고만 했습니다. 그러던 어느 날, 모든 것이 바뀌었습니다.
CAAnimation을 만나다
어느 날 뷰 안에서 여러 이미지를 순차적으로 애니메이션 처리해야 하는 작업이 생겼습니다. 그래서 제 첫 번째 단계는? 당연히 StackOverflow였죠!
검색 결과 가장 위에 있던 코드는 다음과 같았습니다.
let image_1 = UIImage(named: "image-1")!
let image_2 = UIImage(named: "image-2")!
let image_3 = UIImage(named: "image-3")!
let images = [image_1, image_2, image_3]
let animatedImage = UIImage.animatedImage(with: images, duration: 2.0)
imageView.image = animatedImage
꽤 간단해 보이죠? 하지만 정말 이렇게 간단했다면 굳이 이 글을 쓰고 있지 않았을 겁니다.
실제로 요구된 애니메이션은 아래와 같았습니다.

예상하셨겠지만, 제 결과물은 목표와는 거리가 멀었습니다. 완전히 막혀버렸습니다. 이렇게 많은 커스터마이징을 어떻게 넣고, 모든 애니메이션을 어떻게 하나로 동기화해야 할까요?
그때 동료가 CAAnimation을 사용해보라고 조언해 주었습니다. 관련 자료를 읽고 샘플 프로젝트에 적용해 본 결과, 놀랍도록 강력하면서도 사용하기 쉬웠습니다.
Core Animation이란 무엇일까?
Core Animation은 거의 CPU 사용량 없이 여러 애니메이션을 실행할 수 있게 해주는 프레임워크입니다.
높은 프레임 레이트를 유지하면서도, 아주 적은 코드만으로 다양한 커스터마이징을 적용할 수 있습니다.
자세한 내용은 Apple 공식 문서에서 확인할 수 있습니다: https://developer.apple.com/documentation/quartzcore
저는 몇 시간 만에 기본적인 구현을 완성할 수 있었습니다.
func addAnimation(firstImageView: UIImageView, secondImageView: UIImageView) {
let basicAnimation1 = getBasicAnimation(withInitialPostion: centerPosition, finalPos: finalPosition)
firstImageView.layer.add(basicAnimation1, forKey: "position")
let basicAnimation2 = self.getBasicAnimation(withInitialPostion: self.initalPosition, finalPos: self.centerPosition)
secondImageView.layer.add(basicAnimation2, forKey: "position")
self.addNextImage(forImageView: firstImageView)
}
func getBasicAnimation(withInitialPostion initialPos: CGPoint, finalPos: CGPoint) -> CABasicAnimation {
let basicAnimation = CABasicAnimation(keyPath: "position")
basicAnimation.fromValue = NSValue(cgPoint: initialPos)
basicAnimation.toValue = NSValue(cgPoint: finalPos)
basicAnimation.duration = 1
basicAnimation.isRemovedOnCompletion = false
basicAnimation.fillMode = CAMediaTimingFillMode.forwards
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
return basicAnimation
}
이 구현에는 CABasicAnimation을 사용했습니다.
CABasicAnimation 클래스는 레이어 속성(배경색, 투명도, 위치, 크기 등)을 두 값 사이에서 애니메이션 처리해 줍니다. 시작 값과 끝 값만 지정하면 나머지는 알아서 처리되며, 애니메이션은 다음 런 루프에서 즉시 시작됩니다.
자, 이제 본론으로 돌아가 봅시다
구현을 위해 이미지 뷰 두 개를 준비하고 각각 다른 이미지를 넣은 뒤, CAAnimation으로 두 이미지를 차례대로 애니메이션 처리했습니다.
소스 코드는 여기에서 확인할 수 있습니다.
위 GIF를 자세히 살펴보면 뭔가 어색한 점이 보입니다. 선물 상자 이미지가 화면 밖으로 완전히 나가기도 전에 헤드폰 이미지가 잠깐 깜빡인 후 올라오는 것입니다.
왜 이런 현상이 발생할까요?
이미지 뷰에 애니메이션을 추가하자마자 곧바로 다음 이미지를 해당 뷰에 넣기 때문입니다(5번째, 6번째 줄):
private func addAnimation(firstImageView: UIImageView, secondImageView: UIImageView) {
let basicAnimation1 = getBasicAnimation(withInitialPostion: centerPosition, finalPos: finalPosition)
firstImageView.layer.add(basicAnimation1, forKey: "position")
let basicAnimation2 = self.getBasicAnimation(withInitialPostion: self.initalPosition, finalPos: self.centerPosition)
secondImageView.layer.add(basicAnimation2, forKey: "position")
self.addNextImage(forImageView: firstImageView)
}여기서 우리는 두 이미지를 애니메이션 안에서 어떻게 동기화할 것인가라는 문제에 직면합니다. 하지만 다행히 CAAnimation에는 언제나 해결책이 있습니다.
CATransaction으로 애니메이션 동기화하기
CATransaction은 여러 애니메이션을 하나로 묶어 동시에 시작하도록 동기화해 줍니다. 함께 묶인 모든 애니메이션이 정확히 같은 시점에 시작되는 것을 보장합니다.
또한 애니메이션에 completion 블록을 지정할 수 있어, 하나의 묶음(bundle)에 포함된 모든 애니메이션이 끝난 후 특정 작업을 실행할 수도 있습니다.
private func addAnimation(firstImageView: UIImageView, secondImageView: UIImageView) {
CATransaction.begin()
CATransaction.setCompletionBlock {
self.addNextImage(forImageView: firstImageView)
}
let basicAnimation1 = getBasicAnimation(withInitialPostion: centerPosition, finalPos: finalPosition)
firstImageView.layer.add(basicAnimation1, forKey: "position")
CATransaction.commit()
let basicAnimation2 = self.getBasicAnimation(withInitialPostion: self.initalPosition, finalPos: self.centerPosition)
secondImageView.layer.add(basicAnimation2, forKey: "position")
}먼저 CATransaction.begin()으로 트랜잭션을 시작합니다. 그다음 동기화하고 싶은 애니메이션들을 작성하고, 마지막으로 CATransaction.commit()을 호출하면 블록 안의 애니메이션이 시작됩니다.
이제 애니메이션이 어떻게 바뀌었는지 확인해 볼까요?

마지막으로 남은 작업은 애니메이션에 스프링(탄성) 효과를 더하는 것이었습니다. 다행히 CAAnimation에는 이를 위한 해결책도 마련되어 있었습니다.
CASpringAnimation으로 탄성 효과 더하기
CASpringAnimation을 레이어에 추가하면 스프링 같은 탄성 효과가 적용되어, 마치 용수철에 끌려가는 것처럼 대상 지점을 향해 움직입니다.
레이어가 목표 지점에서 멀리 떨어져 있을수록 가속도가 커집니다.
스프링의 감쇠(damping)나 강성(stiffness) 같은 물리 기반 속성들을 제어할 수 있습니다. – Apple 공식 문서
자세한 내용은 Apple 문서에서 확인하세요: https://developer.apple.com/documentation/quartzcore/caspringanimation
기존 코드에 적용해 보겠습니다.
private func getSpringAnimation(withInitialPostion initialPos: CGPoint, finalPos: CGPoint) -> CASpringAnimation {
let basicAnimation = CASpringAnimation(keyPath: "position")
basicAnimation.fromValue = NSValue(cgPoint: initialPos)
basicAnimation.toValue = NSValue(cgPoint: finalPos)
basicAnimation.duration = basicAnimation.settlingDuration
basicAnimation.damping = 14
basicAnimation.initialVelocity = 5
basicAnimation.isRemovedOnCompletion = false
basicAnimation.fillMode = CAMediaTimingFillMode.forwards
return basicAnimation
}
드디어 원하는 결과물을 얻었습니다.
정리하자면, CA Animation을 사용할 때의 장점은 다음과 같습니다.
- 사용법과 구현이 쉽다
- 다양한 커스터마이징 옵션을 제공한다
- 여러 애니메이션을 동기화할 수 있다
- CPU 사용량이 거의 없다
이것은 몇 가지 장점일 뿐이며, 가능성은 무궁무진합니다.
이제 애니메이션 요청이 들어와도 자신 있게 설계하고 구현할 수 있게 되었습니다. 이 글을 읽으신 여러분도 같은 자신감을 느끼셨기를 바랍니다.
제안이나 피드백은 언제든 환영합니다.