iOS
Swift

iOSアプリ開発のためにSwiftでクロージャを実用的に使う方法

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として?もしくは!をつけるだけでよい。()->Voidなら(()->Void)?にする。?か!かでは、?のほうが妥当だと感じるがドキュメントには引数に!をつけている例が多い。

//クロージャを引数として渡したいメソッド
func request(completion: (() -> ())?) {
    //渡されたクロージャを実行する
    completion?()
    //クロージャにも?をつけることでnilなら無視されるのでランタイムエラーにならない
}

これでクロージャとしてnilを渡してもコンパイルできるし、nilでもランタイムエラーにもならずにすっきりとコードが書けるようになる。

クロージャをメソッドの引数とし、NSErrorによってその処理の実行を判断する

クロージャの引数がある場合なのか、inが必要になる

override func viewDidLoad() {

    request({(withError error: NSError?) in 
        if !error {
            //ここで正常系の処理を
        } 
    }
}

func request(completion: ((withError: NSError?) -> ())?) {
    var error: NSError?
    //...非同期でいろいろ行ってerrorがあれば引数として渡す
    completion?(withError : error)
}

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は引数のプレースホルダ
}

省略できるが何がなんだか...という気がする(この例では短く書けることにメリットを見出す場面でないだけかもしれないけど)。

そもそも、iOSアプリは性質上イベントドリブンなプラットフォームなので、複数のクロージャを利用したい場合が多いため、それほどTrailing Closureにこだわることはないと考える。つまり「何が何でもTrailing Closureで書いてクロージャの引数で状態を切り分けるぜ」という考え方があるとしたら、それはバランスを欠いてしまう。

まとめ

  • 複数のクロージャが必要ならそれは現状通り複数で良いんじゃないの?
  • 文脈からクロージャの処理タイミングが判断できて引数無いならTrailing Closure使ってもいいね