Swift: UIViewアニメーションのシンタックスシュガー

この記事はMediumからの転載です。

クロージャを組み合わせた良くない例

初めて聞くかもしれませんが、クロージャはSwiftで活躍する素晴らしいツールです。彼らは第一級オブジェクトで、引数の最後ではTrailing Closureという書き方ができ、現在はデフォルト@noescape となり、循環参照についてきにすることが少なくなりました。

しかし、複数のクロージャを含むAPIを使用しなければならない場面があり、この美しい言語機能の魅力をかなりそこなっています。例えばUIViewのAPIですね。

class func animate(withDuration duration: TimeInterval,            
    animations: @escaping () -> Void,          
    completion: ((Bool) -> Void)? = nil)

Trailing Closure

UIView.animate(withDuration: 0.3, animations: {
    // Animations
}) { finished in
    // Compelted
}

普通のクロージャとTrailing Closureを混ぜています。 animations: はパラメータ名を持っていますが、 completion: からはパラメータ名がなくなっており、Trailing Closureとなっています。 また、この種のメソッドでは、Trailing ClosureがAPIから切り離されているように感じますが、これはAPIの閉じカッコと内側のクロージャの後にカッコが続くためです。

}) { finished in // yuck

Note: Trailing Closureやその使い方については、別の記事でも説明しています。 Swift: Syntax Cheat Codes

読みやすさのためのインデント

Animation Closureのデフォルトのインデントについては、宣言と同じレベルにあるので、よく話題になります。最近、関数型プログラミングをとても気に入っています。関数的なコードを書くことにおいて、私が大好きなのは、一連のコマンドを箇条書きでリストにする方法です。

記事の更新情報を受け取る

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

どうして先程の2つのClosureもこういう風に書けないのでしょうか?

Note: $0というSyntaxについては、他の記事で解説しています。 Swift: Syntax Cheat Codes

無理やり美しくする

UIView.animate(withDuration: 0.3,
    animations: {
        // Animations
    },
    completion: { finished in
        // Compeleted
    })

関数プログラミングの構文から着想を得たり、Xcodeのオートコンプリートと戦ったり、インデントをうまく利用したりして、UIViewのアニメーションAPIをこのようにレイアウトしました。これまでよりもはるかに読みやすい形でコードをレイアウトしていますが、これもまた愛の労力によるものです。このコードをコピペする度にインデントがつぶれてしまいますが、スクロールよりXcodeの問題の方が多いようですね。でもこれがSwiftですよね?

Closureを渡す

let animations = {
    // Animate
}
let completion = { (finished: Bool) in
    // Completion
}
UIView.animate(withDuration: 0.3,
               animations: animations,
               completion: completion)

この投稿の始めに、ClosureがSwift楽園の第一級オブジェクトであると言いました。これは、変数に代入して、他のものに渡せることを意味しています。このコードが有効であっても、さっきの例と同じように読み込まれるとは思えません。また、ひとつの目的のために、他のオブジェクトがアクセスしてこれを使えてしまうことにためらっています。どうしても選べというのなら、前者を選びます。

解決方法

ほとんどのプログラマーがそうしていたように、「長い目で見て時間を節約する」という約束のもとに、比較的汎用的な問題解決策を作り出すことに専念しました。

UIView.Animator(duration: 0.3)
    .animations { 
        // Animations
    }
    .completion { finished in
        // Completion
    }
    .animate()

ご覧のとおり、構文と構造は、Swiftの関数型プログラミングAPIを使って学んだことに触発されています。高階関数のシーケンスのために2つのClosure APIに変更しました。そして今、コードはより読みやすくなり、新しい行を書いて古いものをコピペしているときには、コンパイラが私たちのために戦っています。

「長い目で見れば、時間の節約になっているのです!」

Animator

class Animator {
    typealias Animations = () -> Void
    typealias Completion = (Bool) -> Void
    private var animations: Animations
    private var completion: Completion?
    private let duration: TimeInterval
    init(duration: TimeInterval) {
        self.animations = {}
        self.completion = nil
        self.duration = duration
    }
...

Animator 型は非常にシンプルなものです。3つのプロパティがあります。Durationと2つのClosure、イニシャライザと、いくつかの関数があります。2つ typealias 定義をしており、クロージャを事前に定義しています。これは必須ではありませんが、コードの可読性を向上させ、エラーの発生箇所を減らします。あちこちでシグネチャを変更することになったとしてもです。

Closureのプロパティはmutableです。なぜなら、それらをどこかに格納する必要があり、インスタンス化した後に値を変更するつもりだからです。ですが、外部からの突然変異を避けたいのでprivateにしています。 completion は公式のUIViewのAPIに似ていますが、 animations はそうではありません。イニシャライザの実装では、Closureのプロパティにデフォルト値を定義しているため、コンパイラは警告を出していません。

func animations(_ animations: @escaping Animations) -> Self {
    self.animations = animations
    return self
}
func completion(_ completion: @escaping Completion) -> Self {
    self.completion = completion
    return self
}

Closureのシーケンスの実装は驚くほど簡単です。特定のClosureの引数を指定し、対応するClosureをセットして渡すだけです。

Selfを返す

クールなのは、これらのAPIがselfのインスタンスを返すことです。魔法です。 selfを返すので、シーケンススタイルのAPIを作成できます。

関数にselfを返すと、同じ実行で、他の関数をそれに対して実行することができます。

let numbers = 
    [0, 1, 2, 4, 5, 6]  // Returns Array
    .sorted { $0 < $0 } // Returns Array
    .map { $0 * 2 }     // Returns Array

しかし、シーケンス内の最後の関数がオブジェクトを返す場合は、コンパイラが動作できるよう、何かの変数に格納する必要があります。numbersに格納したのはそういうことです。

最後の関数がVoidを返す場合は、実行のために格納する必要はありません。

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

Animating

func animate() {
    UIView.animate(withDuration: duration, 
        animations: animations, 
        completion: completion)
}

私の他の多くのアイディアのように、このように整理されたものは、既存のAPIのシンプルなラッパーとなっていますが、これは全く悪いことではありません。Swiftは、思想家、汚れ者、プログラマーとしての私達が、提供されたツールを再度想像して作り直すことを許すような方法で作られたものだと信じています。

UIViewの拡張

extension UIView {
    class Animator { ...

最後に、 Animator クラスを UIView のExtensionの中においてみます。このようにするのには、いくつかの理由があります。まず、 UIView の名前空間を使えることです。このようにすることで、つくったAPIにコンテキストを与えられます。2つ目に、機能性が UIView と直接関連することです。これによって、スタンドアロンのクラスとしての意味が無くなります。

Options

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

AnimationのAPIと同時に動作するいくつかのオプションがあります。 ドキュメント をお読みください。関数や継承クラスにおけるデフォルト値の力で、SpringAnimatorというAnimatorは、普段使うであろうアニメーションAPIの種類をほとんど網羅しています。

いつものように、GitHubに playgrounds を用意しています。Xcodeがなくても Gist をみてください。

この記事が気に入ったら、他の 記事 も見てください。また、もしこのアプローチをプロジェクトに採用していただけるなら、 Twitter でリプライを送るか、 フォローしてください。励みになります。


Andyy & the Playgrounds logo

Playgroundsはオーストラリアで開催されるSwiftやAppleのプラットフォーム、そして開発者のためのカンファレンスです。2017/2/23・24にメルボルンで開催されます。詳しくはWebサイトもしくは、Twitter@playgroundsconでご覧いただけます。

About the content

This content has been published here with the express permission of the author.


Andyy Hope

Andyy is the lead iOS Engineer for Punters in Melbourne, Australia. He’s constantly studying the language and finding new ways to challenge the status quo. You can read more of his work on Medium or follow him on Twitter @andyyhope.

He’s also the proud organiser of the Playgrounds conference in Melbourne! Check them out on Twitter @playgroundscon.

4 design patterns for a RESTless mobile integration »

close