iOSアプリを作る場合、blocksを使い非同期処理後にViewControllerなどでその処理の実行内容から任意の処理を実行する場合があった。Swiftではクロージャ(closures)を使い同じような処理を行うための実用的な方法を書いておく。
シンプルなクロージャの記述
まずクロージャの記述
{ (parameters) -> return type in
statements
}
クロージャをメソッドに渡す方法
override func viewDidLoad() {
//クロージャ
let example = {
println("exampleじゃけん");
}
request(example)
request({ println("そのまま渡すこともできるけえの") })
}
//クロージャを引数として渡したいメソッド
func request(completion: () -> ()) {
//渡されたクロージャを実行する
completion()
//しかしこれはクロージャcompletionがnilだとランタイムエラーでクラッシュするし
//そもそもこのメソッドにnilを渡そうとするとコンパイルできないはず
}
よくあるパターンとして処理の完了後にクロージャを渡したい場合があるが、逆に完了後に何もしたくない場合もある。そういう場合に実装の中身がカラのクロージャを渡すのではなくnilを渡したい場合もあると思う
クロージャの引数をOptionalにする
クロージャを渡さず、nilを渡したい場合は変数と同じでOptional Valueとして?もしくは!をつけるだけでよい。
//クロージャを引数として渡したいメソッド
func request(completion: (() -> ())?) {
//渡されたクロージャを実行する
completion?()
//クロージャにも?をつけることでnilなら無視されるのでランタイムエラーにならない
//しかしnilの場合にcompletion!()にするとクラッシュする
}
これでクロージャとしてnilを渡してもコンパイルできるし、nilでもランタイムエラーにもならずにすっきりとコードが書けるようになる。
しかしこれはおすすめできない。次のようにデフォルト引数でからのクロージャを呼び出せるようにする方が無駄が少ないし合理的でしょう。
クロージャの引数にデフォルト引数を指定する
//クロージャを引数として渡したいメソッド
func request(completion: () -> () = {}) {
//渡されたクロージャを実行する。Optionalかどうかなんて気にしない
completion()
//クロージャがからの場合は何も起こらない
}
Trailing Closure
クロージャを引数にするメソッドで、クロージャを最後の引数とする場合に限りメソッドの外に出すことが出来る。
クロージャ自身に引数がない場合の例
例としてUIKitの中でも最も使われるだろうUIViewControllerのモーダル表示メソッドを例にする。まずメソッドの定義は次にようになっている。
func presentViewController(viewControllerToPresent: UIViewController!, animated flag: Bool, completion: (() -> ())!)
まずTrailing Closureをしない場合次のようになる(selfはviewController)
self.presentViewController(composeViewController, animated: true, completion: {
println("画面を表示した")
})
Trailing Closureを行いメソッドの外にクロージャを出すと次のようになる
self.presentViewController(composeViewController, animated: true) {
println("画面を表示した")
}
Trailing Closureはキーワード引数であるcompletion:を省略する必要があるようだ。Trailing Closureによってこの場合のコードの書きやすさは若干上がっている。このクロージャが何の処理を行うためにあるかをキーワード引数名を頼りに推測することが少し難しくなっているのも事実で無理にTrailing Closureを使う理由はない。
クロージャ自身に引数がある場合の例
他にもTrailing Closureを使った場合のデメリットがある。次の例は、UIActionSheetやUIAlertViewを置き換える新しいクラスであるUIAlertActionを使って、クロージャ自身に引数がある場合の例を示す。まずはTrailing Closureを用いない方法
let action = UIAlertAction(title:"action1", style: UIAlertActionStyle.Default, handler:{(action : UIAlertAction?) in
//ここでアクションシート/アラートビューで選ばれ時の処理を書く
})
これをTrailing Closureを使った方法に書き換える
let assets = UIAlertAction(title: "assets", style: UIAlertActionStyle.Default) { (action: UIAlertAction?) in
//ここでアクションシート/アラートビューで選ばれ時の処理を書く
}
引数はプレースホルダにすることで仮引数の宣言部分を省略できる
let action = UIAlertAction(title: "action2", style: UIAlertActionStyle.Default) {
//ここでアクションシート/アラートビューで選ばれ時の処理を書く
println($0) //$0は引数のプレースホルダ
}
(省略できるが何がなんだか...という気がする)
まとめ
- 複数のクロージャが必要ならそれは現状通り複数で良いんじゃないの?
- 文脈からクロージャの処理タイミングが判断できて引数無いならTrailing Closure使ってもいいね
クロージャを関数から使う場合のクロージャ引数省略でエラーになる話
クロージャを使っていて、(parameters)
-> in
を省略するとコンパイルエラーになることがある。
エラー内容は「contextual type for closure argument list expects 1 argument, which cannot be implicitly ignored」。これは引数(ここでは1)を暗黙的に無視しているということの様子。
例えば次のような関数があった場合に。
func request(completion: (String) -> ()) {
completion("<request>")
}
次のように利用するとエラーになる。
これを次のように引数をショートハンドで利用したらエラーではない。明示的に省略しているからだろう。
request() {
print("foo: \($0)")
}
明示的に省略する場合は良くて、暗黙的に省略している場合がなぜ悪かというのは、最初に示したrequest
関数と同名でクロージャの引数がない関数を用意してみると分かる。
// completion: (String) -> () を completion: () -> () にした関数を別途用意
func request(completion: () -> ()) {
completion()
}
そうすると先程のコンパイルエラーだったコードはエラーではなくなる。つまり、関数を増やしてしまうと、呼び出し元がどちらを示しているかわからなくなるからコンパイルエラーで事前に防いでるのではないだろうか。