https://github.com/mynameisjaehoon/AnimationBackground-Swift
제 AnimationBackground-Swift 라이브러리의 계획했던 마지막 기능인 Bounding View를 구현하는 과정, 어떤고민이 있었는지 적어보도록 하겠습니다. 먼저 Bounding View가 어떤 뷰인지 먼저 보여드리겠습니다.
위 화면이 완성된 화면인데요, 사용자가 이미지를 등록하면 회전하면서 움직이고, 경계면에 도달했을 때 충돌하여 다른 방향으로 나아가는 애니메이션 입니다.
GIF 에도 볼수 있듯이 크게 세가지 동작이 있습니다.
구현한 순서이기도 하지만 난이도의 순서이기도 합니다. 마지막 3번을 구현하는 것에 제일 시간을 많이 사용하였습니다. 그럼 하나하나 천천히 살펴보겠습니다.
이번에 구현하면서 가장쉬웠던 부분입니다. 모든 view에는 CALayer타입의 layer가 존재하고, 이 layer에 CABasicAnimation만 추가시키면 간단하게 구현할수 있습니다. 사실거의 있는 API를 그대로 이용하고, 제가 고민했던 부분은 거의 없는 부분입니다. 간단한 키워드를 제공하는 것만으로 구현할 수 있습니다. 코드를 먼저 살펴보겠습니다.
private func rotateView() {
for imageView in imageViews {
let rotation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.toValue = Double.pi * 2 * [1, -1].randomElement()!// 2pi
rotation.duration = option.rotationSpeed
rotation.repeatCount = Float.infinity
imageView.layer.add(rotation, forKey: "rotationAnimation")
}
}
먼저 imageViews
라는 변수는 사용자가 등록한 이미지를 사용해서 UIImageView
를 만들고, 그것들을 저장해놓은 배열입니다. for문으로 순회하면서 하나하나 회전 애니메이션을 적용시켜 주었습니다. toValue를 지정하는 부분에서 한바퀴를 회전시키고, 해당 애니메이션을 반복시켜야하기 때문에 2 pi 값을 주고있습니다. 값이 양수가 되면 시계방향으로 회전하고, 음수가 되면 반시계 방향으로 회전합니다. 저는 사용자가 보기에 애니메이션이 조금 자유분방한(?) 제멋대로 움직이는 모습을 보여주고 싶었기 때문에 회전방향도 랜덤으로 설정해주었습니다.
duration
변수는 애니메이션을 몇초동안 지속할 것인가, 여기서는 한바퀴를 도는데 몇초를 소모할 것인가를 나타내는 변수입니다. 아직 나오지 않은 option
변수의 rotationSpeed
라는 프로퍼티를 넣고있는데, 이 option
변수는 나중에 소개할 사용자가 애니메이션을 커스텀할 수 있는 구조체 인스턴스 입니다.
이미지 이동시키는거, 그거 그냥 UIView.animate
사용하면 되는것 아닌가? 라고 생각하실 수 있지만 이번 경우에는 좀 특수 했습니다. 경계면에 충돌하였을 때, 이동하는 방향을 바꿔야하는 애니메이션을 이어서 수행해야 했고, 그래서 completion handler를 어떻게 수행해야 하는가에 대해 고민이 있었습니다.
바로 이전단계에 CABasicAnimation
을 이용해서 회전애니메이션을 구현하였기 때문에 이동도 같은 방법으로 시도하려 했습니다. 하지만 CABasicAnimation
에는 기본적으로 completion handler를 제공할 수 있는 옵션이 있었고, CATransaction
을 이용해서 completion handler를 등록할 순 있었지만, 핸들러를 등록하는 메서드가 정적메서드로 구현되어 있어 개별 이미지 뷰에 대해서 핸들러를 적용시키고 싶었던 상황에서 사용하기에는 부적절하다고 판단하였습니다.
따라서 저는 UIView.animate
를 이용해서 구현하였습니다. 애니메이션을 테스트로 이동시켜보니, 컴플리션핸들러를 수행하기 전에 애니메이션이 잠시 느려졌다가 중간지점에서 빨라지는 느낌을 받았습니다. 제가원하는 대로 중간에 느려지거나 빨라지는 효과 없이 구현하기 위해서는 animate메서드의 options
매개변수로 .curveLinear
옵션을 제공해주어야 했습니다. UIView.animate
를 사용한 코드를 한번 살펴보겠습니다.
private func moveView(with view: UIView, direction: ProgressDirection) {
let collideInfo = makeCollideInfo(current: view.center, direction: direction)
let collideDirection = collideInfo.direction
let nextPoint = collideInfo.nextPoint
let nextFrame = CGRect(origin: nextPoint, size: CGSize(width: option.imageSize, height: option.imageSize))
let duration = makeDurationTime(current: view.center, next: nextPoint)
UIView.animate(
withDuration: duration,
delay: 0,
options: [.curveLinear]) { [weak self] in
view.center = nextPoint
} completion: { [weak self] finished in
let newDirection = self?.makeNextDirection(current: direction, collide: collideDirection)
self?.moveView(with: view, direction: newDirection!)
}
}