この記事は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 でリプライを送るか、 フォローしてください。励みになります。
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.