UIDynamics 예제

예전에 관련된 내용을 작성한 적이 있는데, 여기서는 내용과 예제를 좀 더 보강한 버전이다. 또한 해당 예제들은 모두 Swift3 버전으로 작성되었다.

UIDynamicAnimator

다이내믹 애니메이터는 물리연산과 관련된 계산이나 애니메이션을 다이내믹 아이템에 적용하고, 그 결과로 계산된 애니메이션 컨텍스트를 제공한다.

개요

다이내믹 아이템UIDynamicItem 프로토콜을 따르는 임의의 클래스 인스턴스로, UIView, UICollectionViewLayoutAttributes 클래스는 이 프로토콜을 따르고 있다. (iOS7+) 커스텀 클래스를 디자인할 때 이 프로토콜을 따르도록하면, 회전이나 위치 변경에 대해서 애니메이터가 계산한 결과에 따라 반응하는 객체를 만들 수 있다.

동역학의 적용을 위해서는 최소 하나의 역학적 동작(dynamic behavior)을 구성해야 하며, 그것을 애니메이터에 연결해야 한다. iOS가 제공하는 동작의 종류로는 다음과 같은 것들이 있다.

  • UIAttatchmentBehavior: 다른 오브젝트에 붙어서 움직임
  • UICollisionBehavior: 다른 오브젝트와 충돌하여 튐
  • UIDynamicItemBehavior: 일반적인 physics body로서의 여러 특성들 (속도, 각속도, 관성, 탄성, 마찰 등)을 추가로 정의할 때 사용한다.
  • UIGravityBehavior: 중력의 영향을 받아 아래로 쏠림
  • UIPushBehavior: 힘을 준다는 소리같음
  • UISnapBehavior: 근처에 가면 찰싹 달라붙는듯

암튼 큰 그림에서는 아래의 과정으로 동역학 애니메이션을 구성할 수 있다.

  1. UIDynamicItem 프로토콜을 따르는 객체가 있고
  2. 이 객체에 UIDynamicItemBehavior를 구성해준다음
  3. 다시 동작(behavior)을 애니메이터에 추가한다.

애니메이터가 개별 아이템과 상호작용하는 방식은 아래와 같다.

  1. 아이템에 동작을 추가하기전에 반드시 시작 위치, 회전값, 바운드를 설정해야 한다.
  2. 동작(behavior)을 애니메이터에 추가하면, 각 아이템들의 이러한 속성들은 애니메이터가 관리하게 되며, 애니메이션이 진행됨에 따라 각각의 속성값을 업데이트한다.
  3. 애니메이터가 아이템의 상태에 대한 제어를 되돌려주면, 그러한 속성값을 수동으로 변경할 수 있다. (updateItem(usingcurrentState:))

두 개 이상의 동작은 UIDynamicBehavior 객체의 자식 동작으로 붙여서 조합할 수 있다. 다시 이러한 조합세트는 다른 조합이나 다른 동작과 함께 재조합되는 것도 가능하다.

애니메이터

동역학 애니메이터를 고용하기 위해서는 먼저 애니메이트될 다이내믹 아이템들의 타입을 명시해야 한다. 이 선택은 어떤 초기화 메소드를 호출할 것인가를 결정하게되며, 다음으로는 좌표계가 어떻게 설정되어야 할 것인가를 결정한다.

  • 일반적인 뷰에 대해서 init(referenceView:)를 이용해서 애니메이터를 생성한다. 참조뷰의 좌표계가 애니메이터가 사용하는 좌표계가 되며, 여기에 사용할 아이템들은 모두 UIView 타입이고, 참조뷰의 하위뷰들이어야 한다. 또한 충돌의 외곽을 참조뷰의 상대적인 바운더리로 설정할 수 잇다. (setTranslatesReferenceBoundsIntoBoundary(with:))

  • 컬렉션 뷰를 애니메이트하기 위해서는 init(collectionViewLayout:)를 사용한다. 이렇게 생성한 애니메이터는 컬렉션뷰 레이아웃을 채용하여 좌표계에 사용한다. 이 애니메이터는 UICollectionViewLayoutAttributes 객체들을 다이내믹 아이템으로 사용한다.

  • 그외의 커스텀 객체(UIDynamicItem 프로토콜을 따름)를 이용할 때는 init()을 사용하여 애니메이터를 생성한다. 이렇게 생성된 애니메이터는 추상 좌표계를 사용하며 화면이나 특정 뷰에 묶이지 않는다.

애니메이터는 타입에 상관없이 다음의 특징을 공유한다.

  • 각각의 동역학 애니메이터는 서로 독립적이다.
  • 하나의 다이내믹 아이템에 대해서 여러 개의 동작(behavior)을 할당할 수 있고, 이 때 동작들은 동일한 애니메이터에 귀속된다.
  • 모든 아이템이 정지상태에 도달하면 애니메이터는 멈춘다. 그리고 새로운 아이템이 추가되거나, 동작의 파라미터가 변경되면 자동으로 재시작 된다.

상황

잠금화면의 카메라 아이콘을 탭하면, 잠금화면이 살짝 위로 들렸다가 아래로 떨어지며 콩 하고 되튀는 모습이 관찰된다. 이 구현은 UIKit Dynamics로 구현되어 있는데 다음과 같은 매커니즘으로 돌아가게끔 되어 있다.

  1. 탭하는 순간 배경화면 뷰에 UIPushBehavior가 적용되면서 순간적으로 뷰를 위로 밀어 올린다.
  2. 배경화면 뷰에는 UIGravityBehavior가 적용되어 있어서 올라간 뷰는 자연스럽게 아래로 다시 내려온다.
  3. 배경화면 뷰는 스크린 가장자리와 충돌하게끔 되어 있어서 충분한 속도를 가지고 내려온 뷰는 다시 위로 튀어오른다.
  4. 2~3을 반복하면서 뷰는 점점 속도를 잃고 최종적으로 멈춘다.

중력과 충돌


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) } }

여기까지 코딩하면 뷰가 로드되면 파란색 서브뷰는 중력의 영향으로 아래로 떨어지게 된다. 다음 코드는 뷰의 테두리에 아이템들이 충돌하게끔하는 장치를 더한다.


// 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는 특정 지점에 용수철이 달린 것처럼 날아가서 고정되는 동작을 의미한다. 여기서는 탭한 위치에 뷰가 날아가서 붙는 동작을 만들어보자.


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는 두개의 아이템 혹은 하나의 아이템과 다른 한 점이 딱딱한 막대기로 연결되어 있는 것처럼 행동하게 한다. 만약 고정점이 있고, 뷰가 고정점에 연결돼 있다면 고정점을 움직이면 해당 뷰가 같이 움직이게 된다. 뷰에 중력을 적용시켜 놓으면 마치 진자가 된 것 같이 뷰가 움직이게도 할 수 있다.


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는 두 가지 모드를 가지고 있는데 하나는 지속적으로 힘을 가하는 것이고 다른 하나는 한순간에 힘만 주는 것이다. 전자의 경우에는 주어진 힘의 방향으로 가속되는 동작을 만들게 된다.


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( }