iOS 7.0부터 UIDynmic이라는 새로운 기술이 UIKit에 도입되었다. 이 기술은 UIView
요소에 대해서 키 프레임 애니메이션등으로 구현하기 힘든 자연스러운 움직임을 쉽게 구현할 수 있게 해주는데, 대략 다음과 같은 것들이 있다.
- iOS 9 이하에서 대기화면을 위로 쓸어올려 카메라를 열 때, 올라간 뷰가 다시 떨어지는 애니메이션 (자유 낙하)
- 위 예에서 떨어진 뷰가 화면 하단에 부딪히면서 통통 튀는 효과 (충돌, 자유 낙하)
- 뷰가 자연스럽게 특정한 방향으로 가속하면서 밀리는 효과 (iOS9의 대기화면을 위로 밀어올리는 효과)
- 뷰와 뷰가 마치 보이지 않는 스프링으로 연결된 것처럼 따라 움직이는 효과 (메시지 앱의 말풍선)
- 뷰가 특정한 위치로 끌려가는 듯 한 효과
이는 마치 2차원 UIView에 대해 각 뷰가 질량을 가지는 강체로 가정하고 중력, 질량가속도, 밀도, 탄성등을 적용한 물리 엔진에 의한 애니메이션 계산을 가능케 하는 것이다.
SpriteKit
이 나오면서 이러한 효과를 마치 게임처럼 SpriteKit
으로 UI를 구성하는 방식도 많이 사용되고 있어서 UIDynimc
에 대한 직접적인 사용이 그리 많지 않은 것 같긴한데, 혹시 나중에 쓸일이 있을지 몰라서 간략하게 정리해본다.
작동 방식
UIDynamic은 UIView
들을 대상으로 하는 애니메이션 엔진이며, 위치, 크기, 회전요소들이 계산에 고려된다. 매 순간 각 뷰가 받는 힘과 힘에 의한 효과는 UIDynamicAnimator
인스턴스에 의해서 계산된다.
계산의 효율을 높이기 위해 UIDynamicAnimator
는 각 뷰가 취해야하는 동작의 종류를 가급적 줄이려 한다. 예를 들어 특정 지점에 연결된 용수철 효과만이 고려되는 뷰에 대해서 다른 뷰와의 충돌을 계산할 필요가 없듯이 각각의 동작의 종류는 카테고리화 되고, 애니메이터는 각 카테고리별로 계산해야 할 항목이 다르다.
이러한 항목들은 UIDynamicItemBehavior
라는 클래스에 대해서 정의된다. 그리고 UIDynamic은 이 추상 클래스의 몇 가지 서브 클래스들을 이용해서, 푸시, 낙하, 충돌, 스냅, 연결등의 효과를 정의해두었다. 아마 대부분의 경우에는 이러한 효과들을 그대로 사용하면 되고, 별도의 behavior를 작성해야 할 일은 극히 드물 것이다.
다시 UIDynamicItemBehaivor
의 서브클래스들은 자신이 정의하고 있는 동작에 영향을 받는 아이템들 생성시에 알고 있어야 한다. 이들 아이템은 UIDynamicItem
이라는 프로토콜을 따르고 있어야 하는데, 이 프로토콜은 앞에서 말한 ‘크기’, ‘위치’, ‘각도’ 등이 정의되어 있는 화면에 표시될 수 있는 요소이다. 기본적으로 UIView
와 그 서브 클래스들은 이 프로토콜을 기본적으로 지원하고 있다.
그렇다면 우리가 이러한 효과를 사용하기 위해서 해야할일은, 각 뷰들이 어떤 동작을 지원할것인지를 결정하고 그에 맞는 UIDynamicItemBehavior
객체를 생성하여, UIDynamicAnimator
인스턴스에 추가해주면 된다. 그러면 뷰들의 초기 상태가 결정되고 나면 애니메이터가 자동으로 각 뷰들이 받는 힘을 계산하여 매 순간의 뷰의 위치를 알려주고, 이들을 모두 포함하는 부모뷰는 그 결과에 따라 하위 뷰들을 애니메이트한다.
셋업
따라서 우리가 해야 할 일은 다음과 같은 순서를 따른다.
- 애니메이션에 참여할
UIView
들에 대한 참조점을 알고 있어야 한다. - 해당 뷰들을 모아서
UIDynamicItemBehavior
객체를 만든다. - 부모뷰를 기준으로하는
UIDynamicAnimatoer
객체를 만든다. - 2의 객체를 3의 애니메이터에 추가해준다. (
addBehavior(_:)
) - profit!
예제
간단한 예제를 하나 작성해보겠다. 뷰 컨트롤러를 하나 작성할 건데, 이 뷰는 작은 서브뷰 하나를 가지고 있다. (일단 귀찮으니 인터페이스 빌더에서 추가한 것으로 치고…) 이 서브뷰는 터치를 통해서 이리 저리 옮길 수도 있고, 손을 떼면 중력의 영향을 받아서 아래로 떨어져 바닥에 부딪힌다.
class MYViewController: UIViewController {
@IBOulet weak var draggableView: UIView!
lazy var panRecognizer: UIPanGestureRecognizer = { [unowned self] in
let p = UIPanGestureRecognizer(target: self,
action: #selector(self.panned(:))
return p
}
lazy var animator: UIDynamicAnimator = { [unowned self] in
let amin = UIDynamicAnimator(referencingView: self.view)
return amin
}()
override func viewDidLoad() {
super.viewDidLoad()
let collision = UICollisionBehaivor(items:[draggableView])
let gravity = UIGravityBehaivior(items:[draggableView])
// collision.translatesReferenceBoundsIntoBoundary = true
// collision.collisionMode = .boundaries
collision.addBoundary(withIdentifier:"floor", from:CGPoint.zero, to:CGPoint(x:view.bounds.width, y:0))
animator.addBehavior(gravity)
animator.addBehavior(collision)
}
func panned(_ sender: UIPanGestureRecognizer) {
if sender.state == .ended {
animator.updateItem(usingCurrentState: draggableView)
}
let translation = sender.translation(in: view)
if let sview = sender.view {
sview.center = CGPoint(x:sview.center.x + translation.x,
y:sview.center.y + translation.y)
// translation을 매순간 이동하는 delta 값으로 쓰기 위해서
// view의 좌표를 변경한 후에는 초기화시켜준다.
sender.translation = CGPoint.zero
}
}
}
이 예제는 크게 두 가지 포인트에 초점을 맞추고 있다.
- 첫째, 뷰의 드래깅을 위해서
UIPanGestureRecognizer
를 사용하며, 드래깅이 일어나는 중에 뷰의 위치를 변경하는 방법을 사용한다. - 둘째, 앞서 언급한 요건들을 충족하기 위해서 두가지
DynamicBehavior
를 사용하고 있다. 하나는 자유 낙하 구현을 위한UIGravityBehavior
이고 다른 하나는 바닥과의 충돌을 시뮬레이트하기 위한UICollisionBehavior
이다.
여기서 놓치지 말아야 할 포인트는 두 군데인데,
collision
을 생성한 후addBoundary(withIdentifier:from:to:)
를 이용해서 뷰 바닥에 경계선을 추가해준다. 이 과정이 없으면 자유낙하한 서브뷰가 바닥을 뚫고 아래로 사라져버린다.UIPanGestureRecognizer
가 작동할 때 호출되는panned(_:)
에서 터치가 종료될 때 애니메이터에 대해서updateItem(usingCurrentState:)
를 호출해주는 점이다. 애니메이터는 물리 엔진의 계산 결과와 다르게 수동으로 변경이 발생하는 아이템에 대해서는 이후 계산을 포기한다. 따라서 현재 위치로부터 계산을 다시 시작할 수 있도록 상태를 업데이트해주는 과정이 필요하다.
정리
UIDynamic
은 단순한 시각적 장식 요소보다는 사용자와 상호작용할 수 있는 뷰들을 대상으로 메뉴 펼침등의 뷰 이동이나, 사용자 상호작용 시 애니메이션에 대해 자연스러운 움직임을 구현하는데 사용될 수 있다.- 서브뷰들을 호스팅하는 루트 뷰에서
UIDynamicAnimator
객체를 생성해서 사용한다. 애니메이터는 연결된 참조뷰의 좌표계를 적용하여 서브 뷰들의 힘과 운동에 관련된 요소들을 계산하여 각 요소의 크기, 위치, 회전값등을 계산하고 애니메이팅한다. - 특정한 서브뷰 요소들이 어떤방식으로 동작할 것인지는 어떤
UIDynamicBehavior
가 설정되어 있는지에 관계한다. 이는UIDynamicBehavior
인스턴스를 만들고, 여기에 해당 동작을 적용받을 뷰를 추가하면 된다. - 실제 애니메이터가 고려하는 기본요소는 크기, 위치, 회전값이며 이는
UIDynamicItem
프로토콜의 필요조건이다. 이는 기본적으로UIView
에는 존재하는 프로퍼티들이므로,UIView
는 별다른 커스터마이징 없이 UIDynamic의 적용을 받을 수 있다. Behavior
요소들은 하위 behavior들을 가질 수 있다. 따라서 새로운 행동양식 클래스를 생성하는 것보다, 하위 행동 양식 요소를 결합하는 방식으로 특정 요소의 움직임을 보다 풍부하게 표현할 수 있다.- 개별 행동양식과 관련된 예제들은 공식 문서 및 유튜브의 예제 영상들을 참고하자.