제목: Swift: UIView Animation Syntax Sugar
클로저가 못생기게 엮어버리기 때문에..


들어보았을지도 모르겠지만, 여러분의 스위프트 코드에서 클로저는 유용하게 쓰인다. 이것은 일급(first-class) 종류이고, API의 끝에 있을때는 후행 클로저(trailing closure)로 만들 수 있다. 그리고 이제는 디폴트 @noescape이며 참조 사이클 싸움에서 엄청난 승리이다(now they’re @noescape by default which is a massive win in the fight against reference cycles).

그러나 우리는 이따금씩 한개 이상의 클로저를 전달해야하는 API들과 함께 작업하게 되는데, 한개 이상의 클로저는 클로저의 아름다운 기능을 덜 매력적인 기능으로 만들어버리기도 한다. UIView를 한번 보자,
class func animate(withDuration duration: TimeInterval,
    animations: @escaping () -> Void,
    completion: ((Bool) -> Void)? = nil)

후행 클로저
UIView.animate(withDuration: 0.3, animations: {
    // Animations
}) { finished in
    // Compeleted
}
우리는 기존의 클로저와 후행 클로저를 함께 쓸 수 있다. animations:는 여전히 그 파라미터의 타이틀을 가지지만, completion:후행 클로저로 만들어 파라미터 타이틀을 없앴다. 나는 후행 클로저가 이 타입의 문맥에서 API로부터 동떨어진 느낌을 받았다. 아마 그 이유는 API의 닫힘 괄호와 뒤에 따르는 열림 괄호의 내부 클로저 때문이라 생각된다.
}) { finished in // yuck
Note: 후행 클로저가 무엇인지 확신하기 힘들다면, 그것이 무엇이고 어떻게 쓰이는지 설명해놓은 Swift: Syntax Cheat Codes라는 글을 보자.

가독성을 위한 들여쓰기
애니메이션 클로저들이 기본 선언과 같은 선상의 들여쓰기를 하기 때문에 그것에대해 이야기해보자. 최근에 나는 함수형 프로그래밍 쿨피스(kool-aid) 많이 마셨고, 함수형 코드를 작성하는 것에대해 내가 완전히 좋아하는 방법은 뷸렛 포인트(bullet point) 형식으로, 이것은 명령을 열로 나타내는 것이다.
[0, 1, 2, 4, 5, 6]
    .sorted { $0 < $1 }
    .map { $0 * 2 }
    .forEach { print($0) }
두 클로저 API도 당연히 이렇게 된다.
Note: $0 문법이 이해가 안된다면, 그것이 무엇이고 어떻게 쓰이는지 설명해놓은 Swift: Syntax Cheat Codes라는 글을 보자.

못생긴 것을 강제로 아름답게 만들기
UIView.animate(withDuration: 0.3,
    animations: {
        // Animations
    },
    completion: { finished in
        // Compeleted
    })
나는 Xcode의 자동완성에 맞춰서 내 스스로 UIView 애니메이션 API를 이런식으로 배치하는 방법을 함수형 프로그래밍 문법에서 찾아냈고 사용해보기로 했다. 내 개인적인 의견은, 이런 배치가 이전 것보다 더 읽기 좋은데, 많이 귀찮아진다. 이 코드를 복사 붙여넣기 할때마다 들여쓰기는 헝클어지겠지만 스위프트 문제라기 보단 Xcode의 문제로 생각된다.

클로저 전달하기
let animations = {
    // Animate
}
let completion = { (finished: Bool) in
    // Completion
}

UIView.animate(withDuration: 0.3,
               animations: animations,
               completion: completion)
포스팅의 시작 부분에서 말했듯이 스위프트-토피아(스위프트 세상)에서 클로저는 일급(first-class) 종류이다. 이 의미는 클로저를 변수에 할당할수도 있거니와 당연히 전달도 할 수 있다는 뜻이다. 그러나 이 코드는 이전 코드만큼 읽기 좋은지 납득하기는 어렵고, 다른 오브젝트들이 다른 목적으로 이 클로저에 접근할 수 있어서 이 방법은 주저하게 된다. 결국엔 전자의 선택을 할것이다.

해결책
많은 프로그래머들이 그렇게 하듯, "장기적으로 시간을 절약"하고 싶은 타협 아래 현실적인 문제와 관련하여 해결책을 만들고자 한다.
UIView.Animator(duration: 0.3)
    .animations {
        // Animations
    }
    .completion { finished in
        // Completion
    }
    .animate()
위에서 볼 수 있듯, 스위프트의 함수형 프로그래밍 API에서 나온 방식에서 영감을 받는 문법과 구조이다. 우리는 두 클로저의 API를 일련의 고차함수(higher-order fuction)로 바꾸었고, 이제 우리 코드는 훨씬 더 읽기 쉬워졌으며, 우리 코드를 복사/붙여넣기 할 때 컴파일러가 들여쓰기를 도와줄 것이다.
"장기적으로 시간을 단축시킬 것이다"

Animator
UIView.Animator(duration: 0.3)
    .animations {
        // Animations
    }
    .completion { finished in
        // Completion
    }
    .animate()
우리 Animator 타입은 꽤 간단하게도 3가지 프로퍼티를 가진다. duration, 두 클로저, 한 생성자. 그리고 곧 익숙해질 몇몇 함수들이 있다. 반드시 필요한 것은 아니지만 우리 코드의 가독성을 증진하고, 우리가 구현한 후, 여러곳에서 클로저 시그니처를 변경하고자할때 에러를 줄여주는 typealias를 사용하는데, 우리 클로저의 시그니처를 미리 정의하기위해 두 typealias 선언한다.

클로저 프로퍼티들은 가변(mutable)인데, 어디선가 그것을 저장하고 인스턴스로 만들 뒤에 그 값을 바꿀 수 있다. 그러나 외부에서 변경가능한 상황을 막기위해 private로 하였다. 기존의 UIView API처럼 만들기 위해 completion은 옵셔널이지만 animations는 옵셔널이 아니다. 생성자 구현에서 컴파일러가 불평하는것을 막기 위해 클로저 프로퍼티에 디폴트값을 넣었다.
func animations(_ animations: @escaping Animations) -> Self {
    self.animations = animations
    return self
}

func completion(_ completion: @escaping Completion) -> Self {
    self.completion = completion
    return self
}
클로저 행렬 구현은 놀랍도록 간단하다. 하는 일이라곤 특정 클로저 인자를 받아서 그것을 해당 클로저 값에 넣는 것이다.

자기자신을 반환하기
멋진 점은 이 API들이 Self의 인스턴스를 반환한다는 점인데, 이 부분이 바로 마법같은 부분이다. 우리가 Self라 쓰면 행렬방식(sequence-style) API로 만들 수 있기 때문이다.

함수에서 Self를 반환할때, 그 자리에 다른 함수들이 다시 실행될 수 해준다.
let numbers =
    [0, 1, 2, 4, 5, 6]  // Returns Array
    .sorted { $0 < $1 } // Returns Array
    .map { $0 * 2 }     // Returns Array
그러나 행렬에서 마지막 함수가 오브젝트를 반환하면 반드시 어디 변수에 할당하여야한다. 위의 numbers 상수에 할당한 이유이다.

마지막 함수가 Void를 반환하면 실행시에 아무것도 할당하지 않아도 된다.

[0, 1, 2, 4, 5, 6]         // Returns Array
    .sorted { $0 < $1 }    // Returns Array
    .map { $0 * 2 }        // Returns Array
    .forEach { print($0) } // Returns Void

애니메이션하기
func animate() {
    UIView.animate(withDuration: duration,
        animations: animations,
        completion: completion)
}
나의 수많은 방법처럼, 원래있던 API를 감싸는 것으로 깔끔하게 끝난다. 그러나 이것이 나쁜 방법은 아니다. 스위프트는 우리를 '생각하는자(thinker)', '고쳐쓰는자(tinkerer)'라 생각하고 있으며, 우리에게 제공된 툴을 다시 생각해보고 다시 가공하는 프로그래머로서 가능하게 해준다고 나는 확고히 믿고있다.

UIView를 익스텐션하기
extension UIView {
    class Animator { ... }
}
마지막으로 두가지 이유로서 우리 Animator 클래스를 잡아다가 UIView의 익스텐션에 놓는다. 그 이유는 첫째로 UIView의 네임스페이스를 원하기 때문이다. 따라서 우리가 만든 API에 문맥을 만들어준다. 두번째로는 기능이 UIView와 직접적으로 연관되어 홀로 클래스로 존재하는 것은 의미가 없게된다.

옵션
UIView.Animator(duration: 0.3, delay: 0, options: [.autoreverse])
UIView.SpringAnimator(duration: 0.3, delay: 0.2, damping: 0.2, velocity: 0.2, options: [.autoreverse, .curveEaseIn])
애니메이션 API와 작업할때, 여러 옵션 선택사항이 있으니 문서를 확인해보자. 함수에서 디폴트값과 클래스 상속을 통해, SpringAnimator 클래스만큼 Animator는 여러분이 일반적으로 사용할 수 있는 많은 애니메이션 타입을 이제 커버한다.

언제나처럼 여러분이 확인해볼 수 있게 GitHub에 플레이그라운드를 만들어 놓았고, Xcode가 없는 사람들을 위해  Gist에도 담아두었다.

오늘 읽은 글이 마음에 든다면 나의 다른 글 도 확인해보고, 혹은 여러분 프로젝트에 이 방식을 적용시켜보고자 한다면, 나에게 트윗 해주거나 Twitter에서 나를 팔로우해달라. 매우 기분이 좋은 하루가 될것이다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
번역자 : 위 글은 2년전(2014년)에 쓰여진 글임에도 번역한 이유는, 이 개념을 알면 수월하고 모르면 해맬 수 있기 때문입니다. 그렇게 대단한 이야기는 아닐지 모르지만 부모와 자식 뷰 간의 터치 이벤트를 주고 받는 일이 있다면 꼭 한번 읽어보길 추천합니다. 


Hit-Test은 한 점(터치 포인트와 같은)이 화면에 그려진 그래픽적인 오브젝트(UIView와 같은)를 관통하는지 결정하는 프로세스이다. 가장 앞쪽에 올라와 있는 view가 무엇인지 알아내기 위해 iOS에서는 Hit-Test라는 것을 사용한다.  Hit-Test는 역방향 우선순위 깊이우선 탐색(Reverse Pre-Order Depth-First Travesal) 알고리즘을 이용하여 view 계층을 조사하도록 구현되어 있다.

Hit-Test가 어떻게 동작하는지 설명하기 전에,  Hit-Test이 언제 호출되는지부터 알아보자. 아래 다이어그램은 손가락이 화면에 터치되는 순간부터 다시 들어올리는데까지 터치 과정의 큼직큼직한 플로우(high-level flow)를 보여준다.


위 다이어그램에서 보여주듯, 화면을 터치하는 매 시간마다  Hit-Test가 호출된다.  Hit-Test 이전 시점에는 어떤 view나 gesture recognizer가 UIEvent 객체를 받는데, 터치가 어느 지점에서 됬는지 그 event에 표현된다.

Note : 알 수 없는 이유로 Hit-Test가 한번에 여러번 호출된다. 이미 결정된 Hit-Test view도 비슷한 현상이 일어난다.

Hit-Test가 끝나고 나면 터치 포인트 아래 최상단 view가 결정된다. 그  Hit-Test view는 터치 이벤트 순서(began, moved, ended, cancelled)의 모든 UITouch 객체와 연관되어있다. 추가적으로  Hit-Test view에서 view나 조상 view들에 붙은 어떤 gesture recognizer들도 위의 UITouch와 연관되어있다. 그러면  Hit-Test view는 터치 이벤트를 순서대로 받기 시작한다. 한가지 염두하고 있어야 할 것은 손가락을 움직여  Hit-Test view의 범위(bounds)를 넘어 다른 view로 움직여도  Hit-Test는 터치 이벤트 순서가 끝날 때까지 모든 touch들을 받고 있을 것이다.

" 그 Touch 객체는 후에 터치가 view 바깥으로 움직여 나갔더래도 그 라이프 타임 동안에는  Hit-Test view와 연관되어있다."

앞에서 말했듯  Hit-Test은 역방향 우선순위(Reverse Pre-Order)의 깊이우선 탐색(Depth-First Travesal)을 사용한다. (먼저 root 노드를 탐색하고 그 다음 높은 index에서 낮은 index 순서로 자식 노드를 탐색한다.) 이런 종류의 탐색은 반복적인 탐색을 줄여주고, 터치포인트가 포함된 view를 내림차순으로 깊이 우선으로 검색하다가, 결과물을 찾으면 찾는 과정을 멈춘다. 이것이 가능한 이유는 subview가 항상 superview보다 위에서 그려지고(render), sibling(형제) view는 subviews 배열 안에서 index가 낮은 sibling view 보다 위에 그려지기 때문이다. 따라서 특정 포인트에 여러 view가 겹쳐져 있으면, 가장 깊은 view 중에 가장 오른쪽 view가 최상단 view가 될 것이다.

"시각적으로 subview의 요소는 subview의 parent view의 모든것을 가리거나 일부분을 가린다. 각 superview는 순서를 가지는 배열로 그것의 subview들을 가지는데, 그 순서는 각 subview의 상태에 따라 배열에 영향을 준다. 만약 두 sibling subview가 서로 겹쳐있다면 마지막에 추가된 subview가 다른 subview 위쪽에 나타나 있을 것이다."

아래 다이어그램은 view 계층 tree에 관한 예제이고, 화면에 그린 UI에 매칭시켜 놓은 것이다. 왼쪽에서 오른쪽으로 정렬된 tree branch들은 subviews 배열의 순서를 의미한다.


위에서 볼 수 있듯이, View A의 자식인 View A.2와 View B의 자식인 View B.1은 서로 겹친다. 그러나 "View B"의 subview index가 "View A"보다 크므로 View B와 그 subview들은 View A와 그것의 subview들보다 위에 그려진다. 그러므로  사용자의 손가락이 View B.1과 View A.2가 겹치는 부분을 터치하여 Hit-Test를 하게되면 View B.1이 반환된다. 

Depth-First Travesal in Reverse Pre-Order 방식을 적용함으로써 가장 깊은 내림차순으로 터치 포인트가 포함된 view를 찾으면 탐색을 멈춘다.


이 탐색 알고리즘은 UIWindow에 hitTest:withEvent: 메시지를 보냄으로서 시작되는데, UIWindow는 view 계층에서  root view이다. 이 메소드로부터 반환되는 값(view)은 터치 포인트를 포함하는 최상단 view이다.

아래 플로우 차트가  Hit-Test 로직을 설명한다.


그리고 아래 코드는 원래 hitTest:withEvent: 메소드를 실제 구현해본 것이다.


hitTest:withEvent: 메소드는 먼저 터치를 받을 수 있는지부터 확인한다.

view가 터치를 받을 수 있다면:
  • view가 hidden 이 아니여야한다.
    self.hidden = NO;
  • view의 userInteraction이 enable이여야한다.
    self.userInteractionEnable == YES;
  • view의 alpha가 0.01보다 커야한다.
    self.alpha > 0.01
  • view가 포인트를 포함해야한다.
    pointInside:withEvent == YES
그러고 view가 터치 받는 것을 허락하면, 이 메소드는 어떤 어떤 하나가 nil을 반환하기 전까지 높은 곳에서 낮은 곳으로 그것의 각 subview에 hitTest:withEvent: 메시지를 보냄으로서 receiver의 subtree를 탐색한다. 그 subview들에의해 반환된 첫번째 nil이 아닌 값은 터치 포인트를 포함하는 최상단 뷰이고 receiver에의해 반환된다. 만약 모든 receiver의 subview들이 nil을 반환하거나 그 receiver가 suview가 없으면 자기 자신을 반환한다.

다르게말하면, view가 touch를 받지 못하도록 되있을 때는 이 메소드가 더이상 receiver의 subtree를 탐색하지 않고 nil을 반환한다. 그러므로 이  Hit-Test 작업에서는 모든 view들의 계층을 다 탐색하지 않아도 된다는 것이다.

hitTest:withEvent:를 override 하여 사용한 일반적인 유스케이스
한 view가 터치 이벤트 매 순간마다 다른 view에 리다이렉트될 수 있도록 터치 이벤트를 다룰 경우 hitTest:withEvent: 메소드를 override 하면 된다.

" Hit-Test가 호출되기 전에 터치 이벤트 순서 중 첫번째 터치 이벤트가 그것의 receiver에게 보내기 때문에, 이벤트들을 리다이렉트하기위해 hitTest:withEvent:를 override 하는 것은 그 이벤트 순서의 모든 터치 이벤트를 리다이렉트 하게 될 것이다."

View의 터치 면적 넓히기
hitTest:withEvent: 메소드를 override 할 수 있는 또하나의 경우는 view의 터치 범위가 그것의 실범위(bounds)보다 커야할 때 이다. 예를들어, 아래 그림은 20X20 크기의 UIView를 나타낸다. 이 크기는 실제 손가락으로 touch 받기엔 너무 작다. 따라서 그것의 터치 면적을 hitTest:withEvent: 메소드를 override하여 각 방향마다 10px씩 증가시킬 수 있다.



Note : 이 view에대해 정확하게  Hit-Test 하기위해, parent view의 영역(bounds)은 child view의 원하는 터치영역을 포함하고 있거나, 원하는 터치 영역을 포함하기 위해 그것의 hitTest:withEvent: 메소드를 overrid 해야한다.

아래에 터치 이벤트들을 view들에 통과시키기
종종 해당 view의 터치 이벤트를 무시하고 그 view의 아래에 통과시켜야 할 때가 있다. 예를들어, 앱 위에 투명하게 전체적으로 view가 덮혀올라간 경우를 생각해보자. 이 오버레이 는 평범하게 터치를 받을 수 있는 control들과 button들의 subview들을 가질 것 이다. 그러나 어디에서나 오버레이를 터치하면 그 오버레이 아래의 view들에게 터치 이벤트를 넘겨줄 수 있다. 이 동작을 완료하기 위해서는 오버레이에서 터치포인트를 포함하는 그것의 subview들 중 하나를 반환하거나 nil을 반환하기 위해 hitTest:withEvent:를 override할 수 있다. 오버레이 위에 터치가 된 경우에도 마찬가지이다.


subview에 터치이벤트 보내기
또 다른 경우는 parent view의 모든 터치 이벤트를 child view로 보낼때이다. child view가 parent view의 일부분만을 차지하지만 그 parent에서 발생하는 모든 터치들에 반응해야할때 이 동작이 반드시 필요하다. 예를들어, 이미지들이 회전목마처럼 구현된 UI를 생각해보자. parent view로는 UIView, 그 위에 child view로는 UIScrollView를 가지고 이 view는 pagingEnable를 YES로 clipsToBounds를 No로 설정한다. 그 위에 이미지들을 올려 구성한다.


UIScrollView의 내부만 터치를 받는것이 아니라 외부에도 터치를 받고 싶은데, 그 scroll view의 parent view 범위 내로 제한하고 싶을때, 아래와 같이 그 parent의 hitTest:withEvent: 메소드를 override 함으로서 가능하다.





WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,


-(void) setMaskTo:(UIView*)view byRoundingCorners:(UIRectCorner)corners cornerRadius:(CGFloat)cornerRadius{
    UIBezierPath* rounded = [UIBezierPath bezierPathWithRoundedRect:view.bounds
                                                  byRoundingCorners:corners
                                                        cornerRadii:CGSizeMake(cornerRadius, cornerRadius)];
    
    CAShapeLayer* shape = [[CAShapeLayer alloc] init];
    [shape setPath:rounded.CGPath];
    view.layer.mask = shape;
}


view.frame = CGRectMake...하고나서
[self setMaskTo:view byRoundingCorners:...];
을 호출해주어야지 정상적으로 라운딩된 뷰를 볼 수 있다. 


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

1. 일단 뷰를 생성해 둔다.

// view 생성
UIView *view = [[UIView alloc] init];
view.backgroundColor = [UIColor colorWithWhite:0.f alpha:1.f];
view.frame = CGRectMake(100, 100, 100, 100);
[self.view addSubview:view];

x, y : 100, 100
w, h : 100, 100
배경색 : 검정 


2. 테두리

// 테두리
view.layer.borderColor = [UIColor colorWithRed:1.f green:0.f blue:0.f alpha:1.f].CGColor;
view.layer.borderWidth = 1.f;

빨간색의 너비가 1인 테두리를 만든다


3. 둥근 모서리

// 둥근 모서리
view.layer.cornerRadius = 10.f;

 

아래와 같이 둥근 모서리를 이용한 원 형태의 뷰도 만들 수 있다.

UIImageView *iv = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"profile.jpg"]];
iv.frame = CGRectMake(100, 100, 100, 100);
[self.view addSubview:iv];


iv.clipsToBounds = YES;
iv.layer.cornerRadius = iv.frame.size.width/2.f;



4. 스케일 변경 및 애니메이션

// 2배로 크게
view.transform = CGAffineTransformMakeScale(2.0, 2.0);

각 너비와 높이의 스케일 값을 넣는다
애니메이션 효과를 원한다면 아래와 같이 하면 된다.

// 애니메이션
[UIView animateWithDuration:3.f animations:^{
    view.transform = CGAffineTransformMakeScale(2.0, 2.0);
}];



5. 각도 변경 및 애니메이션

// 180도 회전
view.transform = CGAffineTransformMakeRotation(M_PI);

M_PI는 미리 정의된 3.141592... 파이값이다. 저 값만 바꿔주면 되며 
애니메이션 효과를 원한다면 아래와 같이 하면 된다.

// 애니메이션
[UIView animateWithDuration:3.f animations:^{
    view.transform = CGAffineTransformMakeRotation(M_PI);
}];





CALayer

The CALayer class manages image-based content and allows you to perform animations on that content. Layers are often used to provide the backing store for views but can also be used without a view to display content. A layer’s main job is to manage the visual content that you provide but the layer itself has visual attributes that can be set, such as a background color, border, and shadow. In addition to managing visual content, the layer also maintains information about the geometry of its content (such as its position, size, and transform) that is used to present that content onscreen. Modifying the properties of the layer is how you initiate animations on the layer’s content or geometry. A layer object encapsulates the duration and pacing of a layer and its animations by adopting the CAMediaTiming protocol, which defines the layer’s timing information.

CALayer 클래스는 컨텐츠를 기반으로한 이미지를 관리하거나 컨텐츠의 애니메이션 기능을 수행하는 역활을 한다. 레이어는 보통 뷰들의 역할을 보완하는데 사용되기도 하지만 뷰 없이도 콘텐츠를 나타내는데 사용될 수 있다. 레이어의 주된 기능은 당신이 제공한 콘텐츠를 보여주는 역할을 관리하는 것이다. 그러나 레이어 그 자신이 배경색이나 테두리, 그림자와 같은 시각적인 속성을 가지고 있다. 추가적으로 시각적으로 콘텐츠를 관리하기 위해, 레이어는 화면에 보여지고 있는 기하학적인 정보들(좌표, 크기, transform등등)을 가지고있다. 레이어의 속성을 변화시키는 것은 콘텐츠의 레이어나 기하학적으로 애니메이션을 시작할 수 잇는 방법이다. 레이어 오브젝트는 레이어의 지속시간이나 페이스를 담고있고 이것은 CAMediaTiming를 적용시킴으로써 레이어에 정의된 시간적인 정보에 따라 애니메이션 시킬 수 있다.




If the layer object was created by a view, the view typically assigns itself as the layer’s delegate automatically, and you should not change that relationship. For layers you create yourself, you can assign a delegate object and use that object to provide the contents of the layer dynamically and perform other tasks. A layer may also have a layout manager object (assigned to the layoutManager property) to manage the layout of subviews separately. 

만약 뷰에 의해 레이어 오브젝트가 생성되었다면, 뷰는 일반적으로 레이어 델리게이트에 자동으로 지정되있을 것이고 당신은 이 지정을 바꿀 수 없다. 당신이 직접 만든 레이어의 한해서, delegate 오브젝트에 어싸인 할 수 있고 그 델리게이트 오브젝트는 콘텐츠 레이어의 동적인 행동이나 다른 일들을 제공하는 것들을 사용할 수 있다. 레이어는 또한 차별적으로 subview들의 레이아웃을 관리하기 위한 (
layoutManager 속성에 어싸인된) 레이아웃 메니저 오브젝을 가진다.







WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

1. 블럭 코딩
나는 애니메이션 효과를 쓸 때는 블럭코딩을 주로 사용하는 편인데, 보기도 편하고 사용하기 좋다.
사용법은 아래와 같이 사용하면 된다.

[UIView animateWithDuration:3.f animations:^{
    // 애니메이션 ..
}];


2. CGRect 변경 (좌표 및 사이즈) 애니메이션

view.frame = CGRectMake(0, 0, 100, 100);

[UIView animateWithDuration:3.f animations:^{
    view.frame = CGRectMake(100, 100, 50, 50);
}];



3. 투명도 변경 애니메이션

view.frame = CGRectMake(100, 100, 100, 100);

[UIView animateWithDuration:3.f animations:^{
    view.alpha = 0;
}];







WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

키보드 올라오는 Delegate받기

- (void)viewDidAppear:(BOOL)animated {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self selector:@selector(keyboardOnScreen:) name:UIKeyboardWillShowNotification object:nil];
    [center addObserver:self selector:@selector(keyboardHideScreen:) name:UIKeyboardWillHideNotification object:nil];
}
- (void)viewDidDisappear:(BOOL)animated {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center removeObserver:self name:UIKeyboardWillShowNotification object:nil];
    [center removeObserver:self name:UIKeyboardWillHideNotification object:nil];
}

#pragma mark - 키보드
-(void)keyboardOnScreen:(NSNotification *)notification {
    //...
}
-(void)keyboardHideScreen:(NSNotification *)notification {
    //...
}



- (void) viewDidAppear:(BOOL)animated ; 는 화면이 나타나기 바로직전에 호출되는 함수이고

- (void) viewDidDisappear:(BOOL)animated ; 는 화면이 없어지기 바로 직전에 호출되는 함수이다.

viewDidAppear에 "키보드 올라오면 말해주세요" 용도의 노티피케이션을 설정하고

viewDidDisappear에 "아까 등록한 노티피케이션을 지워주세요"라고 설정한다.




#pragma mark - 키보드
-(void)keyboardOnScreen:(NSNotification *)notification {
    NSDictionary *info  = notification.userInfo;
    NSValue      *value = info[UIKeyboardFrameEndUserInfoKey];
    
    CGRect rawFrame      = [value CGRectValue];
    CGRect keyboardFrame = [self.view convertRect:rawFrame fromView:nil]; /*키보드 프레임*/

    NSDictionary *userInfo = notification.userInfo;
    NSNumber *durationValue = userInfo[UIKeyboardAnimationDurationUserInfoKey];  /*키보드가 올라오는 동안의 시간*/
    NSTimeInterval animationDuration = durationValue.doubleValue;
    NSNumber *curveValue = userInfo[UIKeyboardAnimationCurveUserInfoKey];       /*키보드 애니메이션 옵션 효과 (ease)*/
    UIViewAnimationCurve animationCurve = curveValue.intValue;
    
    [UIView animateWithDuration:animationDuration delay:0.f options:animationCurve<<16 animations:^{
        // ...
    } completion:nil];
}

-(void)keyboardHideScreen:(NSNotification *)notification {
    NSDictionary *userInfo = notification.userInfo;
    NSNumber *durationValue = userInfo[UIKeyboardAnimationDurationUserInfoKey];
    NSTimeInterval animationDuration = durationValue.doubleValue;
    NSNumber *curveValue = userInfo[UIKeyboardAnimationCurveUserInfoKey];
    UIViewAnimationCurve animationCurve = curveValue.intValue;


    
    [UIView animateWithDuration:animationDuration delay:0.f options:animationCurve<<16 animations:^{
        // ...
    } completion:nil];
}


// ... 부분에 애니메이션 코드를 넣어주면 키보드에서와 동일한 애니메이션 ease가 연출된다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,


나는 UIKit보다 Cocos2d-iPhone을 먼저 접했다. 애니메이션에 대한 욕심이 있었기때문에.. 그치만 Cocos2d는 게임 엔진에 최적화된 라이브러리다. 유틸리티 앱을 만드는 내 입장에서는 여간 부담스럽지 않을 수 없었고.. 점점 Cocos2d를 없애나가는 방법을 찾기 시작했다.

오 그런데 신기하게도 왠만한 애니메이션은 UIKit 프레임워크에서 지원을 해줬던 것이다.

그 와중에 UIKit에서 쉽게 구현하기 힘든 기능이 EaseIn, EaseOut 기능인데, 이건 어떤 사람이 잘 만들어 놓은 라이브러리가 있다. 그걸 가져다 쓰면 된다.


Github : UIView-EasingFunctions


사용법
[UIView animateWithDuration:.6 animations:^{    
    [view setEasingFunction:ElasticEaseOutforKeyPath:@"center"];    
    view.center=CGPointMake(160,415);
}completion:^(BOOL finished){        
    [view removeEasingFunctionForKeyPath:@"center"];
}];


혹시 블럭 코딩을 잘 모르겠으면 구글에 꼭 검색해서 대충 감 잡고 사용해보시길. 난 기초가 부족하니 기초설명은 하지 않겠음.

[MyClass callBlock:^{
    // ...
}];

이렇게 생긴걸 블럭(block)이라 한다


► EaseIn, EaseOut은 점점 빠르게 혹은 점점 느리게 이런 효과를 말하는거다.

아래 다양한 Ease효과를 그래프로 그려놓은 이미지 참조




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,