예전에 관련된 내용을 작성한 적이 있는데, 여기서는 내용과 예제를 좀 더 보강한 버전이다. 또한 해당 예제들은 모두 Swift3 버전으로 작성되었다.
UIDynamicAnimator
다이내믹 애니메이터는 물리연산과 관련된 계산이나 애니메이션을 다이내믹 아이템에 적용하고, 그 결과로 계산된 애니메이션 컨텍스트를 제공한다.
개요
다이내믹 아이템은 UIDynamicItem
프로토콜을 따르는 임의의 클래스 인스턴스로, UIView
, UICollectionViewLayoutAttributes
클래스는 이 프로토콜을 따르고 있다. (iOS7+) 커스텀 클래스를 디자인할 때 이 프로토콜을 따르도록하면, 회전이나 위치 변경에 대해서 애니메이터가 계산한 결과에 따라 반응하는 객체를 만들 수 있다.
동역학의 적용을 위해서는 최소 하나의 역학적 동작(dynamic behavior)을 구성해야 하며, 그것을 애니메이터에 연결해야 한다. iOS가 제공하는 동작의 종류로는 다음과 같은 것들이 있다.
UIAttatchmentBehavior
: 다른 오브젝트에 붙어서 움직임UICollisionBehavior
: 다른 오브젝트와 충돌하여 튐UIDynamicItemBehavior
: 일반적인 physics body로서의 여러 특성들 (속도, 각속도, 관성, 탄성, 마찰 등)을 추가로 정의할 때 사용한다.UIGravityBehavior
: 중력의 영향을 받아 아래로 쏠림UIPushBehavior
: 힘을 준다는 소리같음UISnapBehavior
: 근처에 가면 찰싹 달라붙는듯
암튼 큰 그림에서는 아래의 과정으로 동역학 애니메이션을 구성할 수 있다.
UIDynamicItem
프로토콜을 따르는 객체가 있고- 이 객체에
UIDynamicItemBehavior
를 구성해준다음 - 다시 동작(behavior)을 애니메이터에 추가한다.
애니메이터가 개별 아이템과 상호작용하는 방식은 아래와 같다.
- 아이템에 동작을 추가하기전에 반드시 시작 위치, 회전값, 바운드를 설정해야 한다.
- 동작(behavior)을 애니메이터에 추가하면, 각 아이템들의 이러한 속성들은 애니메이터가 관리하게 되며, 애니메이션이 진행됨에 따라 각각의 속성값을 업데이트한다.
- 애니메이터가 아이템의 상태에 대한 제어를 되돌려주면, 그러한 속성값을 수동으로 변경할 수 있다. (
updateItem(usingcurrentState:)
)
두 개 이상의 동작은 UIDynamicBehavior
객체의 자식 동작으로 붙여서 조합할 수 있다. 다시 이러한 조합세트는 다른 조합이나 다른 동작과 함께 재조합되는 것도 가능하다.
애니메이터
동역학 애니메이터를 고용하기 위해서는 먼저 애니메이트될 다이내믹 아이템들의 타입을 명시해야 한다. 이 선택은 어떤 초기화 메소드를 호출할 것인가를 결정하게되며, 다음으로는 좌표계가 어떻게 설정되어야 할 것인가를 결정한다.
- 일반적인 뷰에 대해서
init(referenceView:)
를 이용해서 애니메이터를 생성한다. 참조뷰의 좌표계가 애니메이터가 사용하는 좌표계가 되며, 여기에 사용할 아이템들은 모두 UIView 타입이고, 참조뷰의 하위뷰들이어야 한다. 또한 충돌의 외곽을 참조뷰의 상대적인 바운더리로 설정할 수 잇다. (setTranslatesReferenceBoundsIntoBoundary(with:)
) - 컬렉션 뷰를 애니메이트하기 위해서는
init(collectionViewLayout:)
를 사용한다. 이렇게 생성한 애니메이터는 컬렉션뷰 레이아웃을 채용하여 좌표계에 사용한다. 이 애니메이터는UICollectionViewLayoutAttributes
객체들을 다이내믹 아이템으로 사용한다. - 그외의 커스텀 객체(
UIDynamicItem
프로토콜을 따름)를 이용할 때는init()
을 사용하여 애니메이터를 생성한다. 이렇게 생성된 애니메이터는 추상 좌표계를 사용하며 화면이나 특정 뷰에 묶이지 않는다.
애니메이터는 타입에 상관없이 다음의 특징을 공유한다.
- 각각의 동역학 애니메이터는 서로 독립적이다.
- 하나의 다이내믹 아이템에 대해서 여러 개의 동작(behavior)을 할당할 수 있고, 이 때 동작들은 동일한 애니메이터에 귀속된다.
- 모든 아이템이 정지상태에 도달하면 애니메이터는 멈춘다. 그리고 새로운 아이템이 추가되거나, 동작의 파라미터가 변경되면 자동으로 재시작 된다.
상황
잠금화면의 카메라 아이콘을 탭하면, 잠금화면이 살짝 위로 들렸다가 아래로 떨어지며 콩 하고 되튀는 모습이 관찰된다. 이 구현은 UIKit Dynamics로 구현되어 있는데 다음과 같은 매커니즘으로 돌아가게끔 되어 있다.
- 탭하는 순간 배경화면 뷰에
UIPushBehavior
가 적용되면서 순간적으로 뷰를 위로 밀어 올린다. - 배경화면 뷰에는
UIGravityBehavior
가 적용되어 있어서 올라간 뷰는 자연스럽게 아래로 다시 내려온다. - 배경화면 뷰는 스크린 가장자리와 충돌하게끔 되어 있어서 충분한 속도를 가지고 내려온 뷰는 다시 위로 튀어오른다.
- 2~3을 반복하면서 뷰는 점점 속도를 잃고 최종적으로 멈춘다.
중력과 충돌
<br />class GravityView: UIView {
lazy var squareView: UIView = { [unowned self] in
let sv = UIView(frame: CGRect(x:100, y:100, width:100, height:100))
self.view.addSubView(sv)
return sv
}()
lazy var gravity = { [unowned self] in
return UIGravityBehavior(items: [self.squareView])
}()
lazy var animator: UIDynamicAnimator = { [unowned self] in
return UIDynamicAnimator(referenceView: self.view)
}()
override init(frame: CGRect) {
super.init(frame:frame)
squareView.backgroundColor = UIColor.blueColor()
animator.addBehavior(gravity)
}
}
여기까지 코딩하면 뷰가 로드되면 파란색 서브뷰는 중력의 영향으로 아래로 떨어지게 된다. 다음 코드는 뷰의 테두리에 아이템들이 충돌하게끔하는 장치를 더한다.
<br />// in class GravityAndCollisionViewController
lazy var collision: UICollisionBehavior = { [unowned self] in
let cl = UICollisionBehavior(items:[self.squareView])
return cl
}
override init(frame: CGRect) {
//...
collisioncl.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collision)
}
만약 콩콩 튀는 효과를 주고싶다면, 별도의 커스텀 Behavior를 더해준다.
override init(frame: CGRect) {
// ...
let itemBehavior = UIDynamicItemBehavior(items: [squareView])
itemBehavior.elasticity = 0.8
animator.addBehavior(itemBehavior)
}
스냅
UISnapBehavior
는 특정 지점에 용수철이 달린 것처럼 날아가서 고정되는 동작을 의미한다. 여기서는 탭한 위치에 뷰가 날아가서 붙는 동작을 만들어보자.
<br />import UIKit
class SnapView: UIView {
required init?(coder aDecoder: NSCoder){
super.init(coder:aDecoder)
}
lazy var animator: UIDynamicAnimator = { [unowned self] in
let anim = UIDynamicAnimator(referenceView: self)
return anim
}()
var snap: UISnapBehavior?
lazy var squareView: UIView = {
let s = UIView(frame: CGRect(x:100, y:100, width:100, height:100))
s.backgroundColor = UIColor.blue()
return s
}()
override init(frame: CGRect){
super.init(frame:frame)
addSubView(squareView)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let tapPoint = touch.location(in: self)
if snap != nil {
animator.removeBehavior(snap!)
}
snap = UISnapBehavior(item: squareView, snapTo: tapPoint)
animator.addBehavior(snap!)
}
}
}
이를 응용하면 특정 뷰를 잡아서 이동했다가, 손을 떼는 시점에 원 위치로 띠용하고 돌아가게 하는것도 가능하겠다.
Attatch
UIAttachmentBehavior
는 두개의 아이템 혹은 하나의 아이템과 다른 한 점이 딱딱한 막대기로 연결되어 있는 것처럼 행동하게 한다. 만약 고정점이 있고, 뷰가 고정점에 연결돼 있다면 고정점을 움직이면 해당 뷰가 같이 움직이게 된다. 뷰에 중력을 적용시켜 놓으면 마치 진자가 된 것 같이 뷰가 움직이게도 할 수 있다.
<br />import UIKit
class AttachView: UIView {
required init?(coder aDecoder: NSCoder){
super.init(coder:aDecoder)
}
lazy var animator: UIDynamicAnimator = { [unowned self] in
let anim = UIDynamicAnimator(referenceView: self)
return anim
}()
lazy var squareView: UIView = {
let s = UIView(frame: CGRect(x:100, y:100, width:100, height:100))
s.backgroundColor = UIColor.blue()
s.layer.cornerRadius = 28.0
return s
}()
lazy var gravity = { [unowned self] in
return UIGravityBehavior(items: [self.squareView])
}()
lazy var attach: UIAttatchmentBehavior = { [unowned self] in
let a = UIAttatchmentBehavior(item: self.squareView, toAnchorPoint: CGPoint(x:300, y:0))
return a
}()
override init(frame: CGRect){
super.init(frame:frame)
addSubView(squareView)
animator.addBehavior(attach)
animator.addBehavior(gravity)
}
}
밀기
UIPushBehavior
는 두 가지 모드를 가지고 있는데 하나는 지속적으로 힘을 가하는 것이고 다른 하나는 한순간에 힘만 주는 것이다. 전자의 경우에는 주어진 힘의 방향으로 가속되는 동작을 만들게 된다.
<br />class PushView: UIView {
var redView: UIVIew = {
let v = UIView(frame: CGRect(x:40, y:100, width:50, height:50))
v.backgroundColor = UIColor.red()
return v
}()
var blueView: UIView = {
let v = UIView(frame: CGRect(x:220, y:100, width:50, height: 50))
v.backgroundColor = UIColor.blue()
return v
}()
let continuousPush: UIPushBehavior = UIPushBehavior(items:[redView], mode:.Continuous)
let instantaneousPush: UIPushBehavior = UIPushBehavior(items:[blueView], mode:.instantaneous)
override init(frame: CGRect){
super.init(frame:frame)
addSubView(redView)
addSubView(blueView)
animator(
}