제목: 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에서 나를 팔로우해달라. 매우 기분이 좋은 하루가 될것이다.



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

으로 보내주시면 됩니다.


+ Recent posts