AutoLayout을 코드로 정의하기 (Swift)

오토레이아웃

오토레이아웃이 도입되기 이전에도 코코아 및 코코아터치에서는 상위뷰나 윈도가 크기가 변하는 경우에 하위 뷰들의 크기가 그에 따라 어떻게 변할 것인지를 결정해주는 방법이 있었다. 흔히 spring & struts라 불리는 오토리사이징 마스크가 그것이다.

하지만 오토 리사이징 마스크는 한 가지 문제가 있는데, 그것은 오토 리사이징 마스크는 수퍼뷰와 서브뷰 둘 사이의 관계만을 정의하기 때문에 이를 따라 뷰의 크기나 위치가 움직일 때 서브 뷰 간의 레이아웃이 흐트러질 수 있는 가능성이 매우 많다는 것이다.

그리하여 예전에는 화면의 회전이나 키보드가 올라오는 등의 과정이 발생하면[^0-1] 뷰의 크기가 전환되는데 이때 변환된 크기에 따라서 다른 레이아웃을 갖도록 코드상에서 이를 일일이 수정해주어야 했다.

오토레이아웃

오토 리사이징 마스크가 한정된 몇 가지 요소에 의존할 수 없었던 것 때문에 명확한 한계를 가지고 있었고, 이를 보완하기 위해 등장한 오토레이아웃은 뷰의 프레임보다는 뷰의 관계에 대해서 접근하는 방식으로 패러다임 전환을 시도했다. 오토레이아웃은 두 뷰의 관계를 통해서 레이아웃을 결정하는 것이며, 따라서 일련의 제한요소(constraint)등을 정의하여 내적 / 외적인 레이아웃 변경 요인에 대해 전향적으로 대응할 수 있게 한다.

제한 요소

뷰 계층 구조의 레이아웃은 일련의 선형 방정식들에 의해서 결정되며, 이 때 하나의 방정식은 하나의 제한요소를 가리킨다.

image: ../Art/view_formula.pdf

위 그림은 애플 개발자 문서 중 오토레이아웃 가이드에서 가져온 것이다. 이는 제한 요소는 두 뷰의 관계, 그 중에서 특정한 제약 요소를 기술하는 것임을 보여준다.

  • 오른쪽 red 뷰의 왼쪽 지점은 왼쪽 blue 뷰의 오른쪽 끝 지점에 0.8 만큼의 오프셋을 둔 지점과 일치해야 한다.

대부분의 제한 요소는 두개의 UI 요소간의 관계를 정의하나 하나의 요소의 두 속성간의 관계를 정의할 수도 있다. (예를 들어 어떤 뷰의 높이는 가로폭의 절반이라거나 하는 식)

오토레이아웃 속성

오토 레이아웃에서 ‘속성'(attributes)이란 제한 요소의 적용을 받는 어떤 기능을 의미한다. 여기에는 보통 4개의 변과 폭/높이 그리고 가로/세로 방향의 중심이 온다. 이러한 모든 요소들은 NSLayoutAttributes enum타입에 정의되어 잇다.

물론 모든 속성들이 한꺼번에 다 정의될 필요도 없다. (그렇게 하기가 불가능한 경우도 많고) 오토레이아웃을 사용할 때의 목표는 오직 하나의 가능한 솔루션만을 갖는 일련의 방정식들을 정의내리는 것이다. 여기에는 개별 공식들이 충돌하여 솔루션을 만들지 못하는 경우도 있을 수 있고, 반대로 2개 이상의 솔루션을 만드는 모호한 경우가 있을 수도 있다.

일반적으로 2개의 축방향에 대해서 해당 방향으로의 뷰의 위치와 크기를 정의하는 것으로 오토레이아웃을 적용할 수 있다. 하지만 하나의 뷰의 레이아웃을 결정하는 공식 세트가 반드시 하나인 것은 아니다. 아래는 어떤 서브 뷰의 가로 위치와 크기를 결정하는 예이다.

image: ../Art/constraint_examples.pdf

  • 첫번재 예는 왼쪽 여백과 폭을 정의했다. 이는 뷰가 고정된 폭을 가지며, 부모 뷰의 크기가 변경될 때에는 오른쪽 여백만 변경하며 뷰의 위치와 크기가 변하지 않음을 암시한다.
  • 두번째 예는 왼쪽 및 오른쪽 여백을 고정값으로 정의했다. 이는 뷰의 폭이 부모뷰의 폭에서 특정한 오프셋 값만큼 뺀 양으로 정이된다는 것이다. 부모뷰의 크기가 변경될 때 서브 뷰는 여백의 크기를 유지한채로 폭이 변경된다.
  • 세번째 예는 부모 뷰아 중심점을 맞추고 왼쪽 여백을 고정했다.

두번째와 세번째 설정은 설정한 내용은 다르지만 외관상 동일하게 동작한다.

IB를 통한 오토레이아웃 설정 법

건너뛴다.

주요 정책

다음의 가이드라인을 참고하라. 물론 각 규칙에 대한 예외는 어김없이 존재하겠지만, 이러한 규칙을 참고하는 것은 성공적인 접근을 도와준다.

  • 뷰의 기하학적 정의를 frame, bounds, center를 통해서 정의하지 말 것
  • 가능하다면 스택뷰를 사용할 것. 스택뷰는 콘텐츠의 레이아웃을 자동으로 관리하며 따라서 제한요소에 필요한 로직을 대거 줄여줄 수 있다.
  • 뷰의 제한 요소는 가장 가까운 뷰와 관련지어 생성할 것
  • 뷰에 고정폭, 고정높이를 적용하는 것을 피할 것
  • 제한 요소를 설정하는데 어려움을 겪는다면, Pin, Align 도구를 활용할 것. 이는 컨트롤 드래깅(우클릭 드래깅)보다는 느리지만, 정확한 조정에 도움을 준다.
  • 프레임 업데이트를 주의깊게 사용할 것. 부정확한 설정 후에 이 동작을 실행하는 것은 예기치 못한 결과를 만들 수 있다.
  • left, right 대신에 항상 leading, trailing을 사용할 것
  • iOS에서 뷰의 엣지를 뷰 컨트롤러의 루트 뷰에 대응시킬 때 다음을 고려할 것
    • 수평 제한: 대부분의 컨트롤에 대해서 레이아웃 마진값에 대해 0 값을 사용하면 시스템이 알아서 적절한 여백을 생성해준다.
    • 텍스트 객체가 뷰의 폭을 가득 메울 가능성이 있다면 레이아웃 마진 대신에 가독성 콘텐츠 가이드를 사용하라
    • 루트 뷰의 변에서 변을 채우는 콘텐츠를 정의하려면 뷰 자체의 leading, trailing에 0 포인트로 맞춰라
    • 수직 제한: 뷰가 바 아래로 확장되는 경우에 top,bottom 마진을 사용하라.
  • 코드상에서 뷰를 초기화하는 경우 뷰의 translatesAutoresizingMaskIntoConstraints 값을 false로 바꿔라. 기본적으로 시스템에 의해 이 값은 true로 정해지고 그러면 오토레이아웃 제한자를 설정하는 것이 제대로 동작하지 않는다.
  • macOS와 iOS는 레이아웃을 계산하는 방식이 다르다.

스택뷰

image: ../Art/nested_stack_views_2x.png

스택뷰를 쓸 수 있으면 사용해라. 이런 복잡한 레이아웃도 사실 스택뷰를 쓰면 쉽게 정리할 수 있다.

image: ../Art/Nested_Stack_Views_Screenshot_2x.png
UIStackView를 이용하여 구성한 레이아웃

코드상에서 제한요소를 만들기

사실 가능하다면 인터페이스 빌더를 사용해서 오토레이아웃 제한 설정을 만드는 것이 좋다. 인터페이스 빌더는 제한요소를 시각화하고 편집/관리하며 디버깅할 수 있는 많은 도구를 제공한다. 제한 요소들을 분석함으로써 인터페이스 빌더는 디자인 타임에서 발생하는 많은 일반적인 오류들을 찾아내고 자동으로 고칠 수 있다.

또한 기존에는 프로그래밍적으로만 제어할 수 있었던 뷰의 가변 개수에 대한 레이아웃 처리도 스택뷰를 사용하여 IB에서 구성할 수도 있다.

만약 코드상에서 오토레이아웃을 관리하겠다면 여기에는 크게 세가지 접근법이 있다.

1. 앵커를 사용하기

NSLayoutAnchor는 제한요소를 만들기 위한 유틸리티 클래스로 사용하기 쉬운 이터페이스를 제공하여 다양한 제약 요소들을 생성할 수 있다. 대부분의 시각 요소들은 이러한 앵커들을 이미 가지고 있으며, 이들로부터 제약요소를 생성할 수 있다.

let margins = view.layoutMarginsGuide
myView.leadingAnchor.constraint(equalTo: margins.leadingAnchor).aotive = true
myView.trailingAnchor.constraint(equalTo: margins.trailingAnchor).active = true
myView.heightAnchor.constraint(equalTo: myView.widthAnchor, multiplier: 2.0)

위 코드는 myView의 좌우를 기본 레이아웃 마진을 사용하여 부모 뷰에 딱 고정한다. 그리고 myView의 높이를 가로의 2배로 사용한다.

다음의 예를 실제로 만들어보자.

  1. 컨테이너 뷰 안에 3개의 뷰가 들어있다.
  2. 빨간뷰와 파란뷰는 같은 너비이며 양쪽끝에는 표준 마진, 가운데는 8만큼의 공간을 띄운다.
  3. 초록색은 마진을 제외한 전체폭이며, 가운대로 정렬된다. 높이는 빨간색 뷰와 같은 높이를 주며, 컨테이너 내부 마진을 제외하고 8만큼의 여백을 갖는다.

위 조건을 코드로 표현하면 아래와 같다.

let containerView = UIView(frame: CGRect(x:0, y:0, width: 300, heigh: 400))
containerView.backgroundColor = UIColor.white

//: 뷰 세 개를 만들고 오토레이아웃을 적용할 준비를 한다.

let redView = UIView()
redView.backgroundColor = UIColor.red
redView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(redView)

let blueView = UIView()
blueView.backgroundColor = UIColor.blue
blueView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(blueView)

let greenView = UIView()
greenView.backgroundColor = UIColor.green
greenView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(greenView)

//: 컨테이너뷰로부터 표준 마진을 사용한다.
let margins = containerView.layoutMarginsGuide

//: `redView`의 위치는 위, 왼쪽, 폭, 높이를 결정해준다. 
redView.topAnchor.constraint(equalTo: margins.topAnchor).isActive = true
redView.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
redView.widthAnchor.constraint(equalTo: margins.widthAnchor, 
    multiplier: 0.5, constant: -4.0).isActive = true
readView.heightAnchor.constraint(equalTo: margins.heightAnchor,
    multiplier: 0.5, constraint: -4.0).isActive = true

//: `blueView`는 좀 더 쉬운데 `redView`를 따라가기만 하면 된다. 
blueView.topAnchor.constraint(equalTo: redView.topAnchor).isActive = true
blueView.widthAnchor.constraint(equalTo: redView.widthAnchor).isActive = true
blueView.heightAnchor.constraint(equalTo: redView.heightAnchor).isActive = true
blueView.leadingAnchor.constraint(equalTo: redView.trailingAnchor, constant: 8).isActive = true

//: `greenView`는 `redView`의 아래쪽으로 전체 폭을 사용한다. 
greenView.topAnchor.constraint(equalTo: redView.bottomAnchor, constant: 8).isActive = true
greenView.conterXAnchor.constraint(equalTo: containerView.centerXAnchor).isActive = true
greenView.bottomAnchor.containerView(equalTo: margins.bottomAnchor, constraint: -8).isActive = true

  1. Layout Contraints 를 사용하기

앵커를 사용하는 1의 문법들은 모두 조금 더 쓰기 쉬운 인터페이스를 이용해서 제약 요소값들을 생성하는 데, 이를 직접 이용하는 방법이 있다. 이 방법은 매우 무식하며 가독성도 떨어지고 그만큼 실수하기도 쉽다. 따라서 매우 추천하지는 않는다.

NSLayoutConstraint(item: myView, attribute: .leading, relatedBy: .Equal, toItem: view, attribute: .leadingMargin, multiplier: 1.0, constant: 0.0).isActive = true
  1. 비주얼 포맷 사용하기

비주얼 포맷 언어는 마치 아스키 아트처럼 보이는 문자열을 이용해서 제약요소값들을 생성하는 방식이다.

  • 오토레이아웃의 디버깅은 콘솔을 통해 비주얼 포맷을 사용하여 출력한다. 따라서 디버깅시에 사용되는 포맷과 생성에 사용하는 포맷이 일치한다.
  • 비주얼 포맷을 사용하면 여러 제약 요소를 한 번에 만들 수 있다.
  • 유효한 제약요소들만이 만들어진다. (단, 모든 필요한 제약요소가 다 만들어지는 것은 아니다.)
  • 완전성보다는 좋은 시각화에 집중한 방식이다. 따라서 일부 제약요소는 이 방식으로 만들 수 없다.
  • 단 포맷은 컴파일 타임에 체크할 수 없다. 실행하여 확인할 수만 있다.

어쨌든 이 방식은 다음과 같은 식으로 사용된다.

let views = ["myView": myView]
let formatString = "|-[myView]-|"
let constraints = NSLayoutConstraint.constraintsWithVisualFormat(formatString, 
    options: .AlignAllTop, 
    metrics: nil, 
    views: views)

NSLayoutConstraint.activateConstraints(constraints)

비주얼 포맷은 뷰의 한쪽 끝에서 끝까지를 다 정의해야 하는 것은 아니다. 연관있는 뷰들만 포함하는 일부분을 만들 수 있다. 이렇게해서 만들어지는 결과는 Array<NSLayoutConstraint> 타입이므로 이들 결과를 합한 다음에 NSLayoutContraint.activateContraints(_:)를 이용해서 한꺼번에 활성화할 수 있다.

비주얼 포맷을 사용하여 위 세개 뷰의 위치를 정의한 코드는 다음과 같다.

let views = ["redView": redView,
             "blueView": blueView,
             "greenView": greenView]

let format1 = "V:|-[redView]-8-[greenView]-|"
let format2 = "H:|-[redView]-8-[blueView(==redView)]-|"
let format3 = "H:|-[greenView]-|"

var constraints = NSConstraint.constraints(withVisualFormat: format1,
                    options: alignAllLeft,
                    matrics: nil,
                    views: views)
constraints += NSConstraint.constraints(withVisualFormat: format2,
                    options: alignAllTop,
                    matrics: nil,
                    views: views)
constraints += NSConstraint.constraints(withVisualFormat: format3,
                    options: []
                    matrics: nil,
                    views: views)
NSConstraint.activateConstraints(constraints)

Nib 파일로부터 UI 관련 객체를 로딩하기

nib 파일에서 뷰/뷰컨트롤러를 가져오기

UIView를 상속받은 커스텀 뷰를 작성할 때, 뷰의 서브 뷰들을 일일이 동적으로 구성하는 것보다 인터페이스 빌더를 통해서 구성하는 것이 더 편한 경우가 있다. 흔히 테이블 뷰의 셀에 쓰이는 뷰가 이런 식으로 구성하기 좋다.  코드 상으로 모든 뷰의 구성 요소들을 일일이 구성하고 초기화하는 것은 너무 번거로우니, 메인 스토리보드와는 별개의 nib 파일에 뷰를 세팅하고 이를 로드하는 방식으로 좀 더 간결하고 예쁘게 처리할 수 있는 방법이 있을지 고민해보자. Nib 파일로부터 UI 관련 객체를 로딩하기 더보기

UIApplicationMain

UIApplicationMain

UIKit에서 앱의 런칭 프로세스를 처리하는 함수. UIApplication 객체를 생성하고, 앱의 기본 설정을 Info.plist 파일로부터 읽어들인다.

int UIApplicationMain(int argc, char *argv[], NSString *pricipalClassName, NSString *delegateClassName);

principalClassName : UIApplication 클래스를 서브클래싱한 경우 해당 클래스 이름을 전달한다. nil을 쓰는 경우 이 값은 UIApplication으로 고정된다.

delegateClassName : 앱 델리게이트 클래스 이름. 만약 nib 파일 내에 앱 델리게이트 객체가 정의되어 있다면 nil을 전달해야 한다.

iOS에서 SQLite3 사용하는 방법 (Objective-C)

Swift3 버전에 대한 SQLite3 사용법은 이 포스트를 참고하시라.

예전에 쓴 글이 있기는 하지만, 그냥 요리법처럼 쓴 글이기도 하거니와 소스코드에서 뭔가 글자가 빠지는 등(syntax highlighter를 안써야 겠지만 기존 글 고치기가 귀찮아…) 문제가 많아 내용을 보충해서 다시 작성.

애플은 SQLite3를 직접 인터페이스하는 것보다는 코어데이터를 사용하라고 권장하고 있고, (실제로 있다가 빠진 것인지는 알 수 없으나 그런 주장을 하는 사람들이 종종 있다) 애플 개발자 문서에서도 관련 내용을 내렸다고 한다. (하지만 이는 사실이 아닐 거라 생각한다. 왜냐면 iOS에서 SQLite3를 인터페이스 하는 부분은 전적으로 libsqlite3를 사용하는 것이고, 이에 대한 문서는 SQLite3 홈페이지에 가면 있기 때문이다) iOS에서 SQLite3 사용하는 방법 (Objective-C) 더보기

[Cocoa Touch] 뷰 트랜지션 애니메이션 구현하기

트랜지션(transition)은 뷰가 갑자기 나타내거나 없어지거는 것과 관련하여 보다 나은 사용자 경험을 제공할 수 있도록 해준다. 트랜지션 역시 뷰 애니메이션이므로 UIView의 애니메이션 메소드를 사용하거나 코어 애니메이션을 사용하여 구현하는 두 가지 방법 중 하나를 선택할 수 있다.

UIView 애니메이션으로 뷰 트랜지션 구현

UIView 애니메이션에서 지원하는 트랜지션은 크게 두 가지로 나뉜다.

  • 기존의 뷰의 서브 뷰들을 바꾸기 – 비교적 작은 뷰의 변경에 속한다.
  • 뷰를 새로운 뷰로 대체하기 – 화면상에서 변경이 일어나는 영역이 많을 때

뷰 트랜지션은 뷰 컨트롤러에 의한 트랜지션과는 다르다. 뷰 컨트롤러 트랜지션은 window에 표시되는 전체 뷰가 바뀌는 것이지만, 여기서 말하는 뷰 트랜지션은 현재 뷰의 계층 구조 내에서의 변화가 일어나는 것을 의미한다.(즉 네비게이션 컨트롤러 상의 뷰 스택에는 변경이 없다.)

뷰의 서브 뷰 변경하기

iOS4에서부터 trasitionWithView:duration:options:animations:completion: 메소드가 제공되는데, 이를 사용하면 특정 뷰의 서브 뷰에 대한 트랜지션을 쉽게 구현할 수 있다. 이 메소드에 전달되는 블럭에서는 뷰의 전환과 관련된 변경 사항만을 구현해야 한다. 코코아 터치는 성능의 향상을 위해서 변경 전과 후의 뷰의 스냅샷을 찍어 이 두 이미지를 부드럽게 전환하도록 한다. 만약 뷰의 교체외에 다른 변경 (뷰의 크기가 변경된다거나 배경색이 변경되는등)을 동시에 수행해야 하면, UIViewAnimationOptionAllowAnimatedContent 옵션을 줘야 한다. 이 옵션이 주어지면 코코아터치는 더 이상 두 스냅샷 사이를 전환하는 기능을 사용하지 않고 모든 서브 뷰에 대해 직접 애니메이션을 생성하게 된다.

위 메소드의 원형을 먼저 살펴보자.

+ (void)transitionWithView:(UIView*)view duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void(^)(void))animations completion:(void(^)(BOOL finished))completion;

다음은 각각의 파라미터에 대한 설명

  • view : 트랜지션이 발생하는 컨테이너 뷰. 이 뷰의 서브 뷰들이 전환된다.
  • duration : 트랜지션 지속시간
  • options : 애니메이션 옵션에 대한 비트마스크 값들.
  • animations : 애니메이션 블럭. 어떤 서브뷰가 없어지고 어떤 서브뷰가 추가되는지를 기술해야 한다.
  • completion : 트랜지션 종료 후 실행할 블럭.

이를 수행하는 예제는 다음과 같이 쓸 수 있다. 코드 블럭 내에서 view의 서브 뷰를 제거하고 다른 뷰를 추가한다.

[UIView transitionWithView:containerView
    duration:0.2f
    options:UIViewAnimatinoOptionTransitionFlipFromLeft
    animations:^{
        [fromView removeFromSuperview];
        [containerView addSubview:toView];
    }
    completion:NULL];

특정한 뷰를 다른 뷰로 교체하기

그외에 다음의 메소드들도 발견할 수 있는데…

+ (void)transitionFromView:(UIView *)fromView toView:(UIView *)toView duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options completion:(void(^)(BOOL finished)completion;

이 메소드는 두 뷰간의 전환을 구현하는 가장 간단한 방법이다. 만약 두 뷰 모두 뷰 계층 구조에 이미 속해 있다면, UIViewAnimationOptionSHowHideTransitionViews를 추가해줄 수 있다. 이 메소드는 기본적으로 현재 컨테이너 뷰의 계층 구조를 변경한다. (fromViewtoView로 교체한다.)

UIView 애니메이션은 기본적으로 사용자 입력을 블럭킹하는데, 이를 피하기 위해서는 UIViewAnimationOptionAllowUserInteraction을 추가한다. 다음 코드는 뷰 컨트롤러의 메인뷰(루트뷰)를 위해 두 개의 뷰를 준비해 놓고 이들을 교체하는 트랜지션 효과를 구현한 것이다. (어느 뷰를 보여주고 있느냐에 대해서 트랜지션 방향이 바뀐다.)

    -(IBAction)toggleMainView:(id)sender
{
    [UIView transitionFromView:(displayingPrimary ? primaryView : secondaryView)
    toView:(displayingPrimary ? secondaryView : primaryView)
    duration:1.0f;
    options: ( displayingPrimary ? 
    UIViewAnimationOptionTransitionFlipFromRight : UIViewAnimationOptionTransitionFlipFromLeft)
    completion:^(BOOL finished) {
        if(finished) {
            displayingPrimary = !displayingPrimary;
        }
    }];
}

실제로 위 코드를 활용하기 위해서 뷰 컨트롤러는 뷰를 로드/언로드하는 처리를 해주어야 한다. 즉 트랜지션이 일어나기 직전에 toView가 될 뷰를 로드해서 생성해주고, 교체해야 할 것이다.

코어 애니메이션으로 트랜지션 구현하기

코어 애니메이션은 (UIView 애니메이션도 내부적으로는 코어 애니메이션으로 전환된다.) 뷰와 연관되어 있는 레이어를 조작하여 애니메이션을 구현하는 고속 그래필 처리 프레임워크이다. 코어 애니메이션은 CAAnimation이라는 추상 클래스를 통해 객체화되는데 이 CAAnimation의 서브 클래스인 CATransition을 사용하면 뷰 간의 트랜지션 효과를 구현할 수 있다.

코어 애니메이션은 뷰의 뒷단에 존재하는 애니메이션 레이어(CALayer)에 의한 애니메이션이다. 뷰와 레이어는 긴밀하게 연결되어 있어서 UIView의 어떤 속성을 변경하면 레이어에도 같은 변경이 가해지고, 그 차이를 통해 애니메이션을 구현할 수 있다.

다음은 두 뷰 간의 트랜지션을 만들어 내는 예제이다.

    CATransition *transition = [CATransition animation];
    transition.startProgress = 0;
    transition.endProgress = 1.0;
    transition.type = kCATransitionPush;
    transition.subType = kCATransitionFromRight;
    transition.duration=1.0;

    [myView.layer addAnimation:transition forKey:@"transition"];
    [myView.layer addAnimation:transition forKey:@"transition"];

    myView1.hidden = YES;
    myView2.hidden = NO;

두 뷰가 트랜지션될 때에는 하나의 동일한 트랜지션 객체를 두 뷰(뷰의 레이어)가 공유하면 된다. 각각의 뷰는 뷰 계층구조에 이미 존재하고 있고 hidden 속성을 끄고 켜는 것으로 뷰에 변경을 가하면, 각각의 뷰에 연결된 CALayer에서 이 변화를 감지, 연결되어 있는 애니메이션 객체(CATransition)에서 트랜지션 애니메이션을 처리하는 형태로 구현된다. 코어 애니메이션에 대한 내용은 별도의 포스트에서 다루도록 하겠다.