メモ2ブログ

メモtoウェブログ。旧ブログはこちら。 http://sakebook.blogspot.jp/

iOSでのコールバック処理の3つの書き方(Swift)

最近iOSのアプリ開発ばかりしています。

通信や非同期での処理、ユーザアクションなど、様々な箇所でコールバックを受けたい場合があって、それぞれ適した使いどころがあると思ったのでまとめます。

方法は3つ

  • Protocol
  • Closure
  • NSNotification

一つずつ説明していきます。

Protocol

一つは、Protocolを用いたDelegateパターンです。

次のように記述します。

protocol TaskDelegate {
    func complete(result: AnyObject)
    func failed(error: NSError)
}

final class Task {
    
    var delegate: TaskDelegate?

    ...
    func someTask() {
        ...
        someTaskResult(someResult, error: error)
    }

    ...
    func someTaskResult(result: AnyObject, error: NSerror) {
        if error == nil {
            self.delegate?.complete(result)
        } else {
            self.delegate?.failed(error)
        }
    }
}

このクラスを次のように使います。

final class SomeViewController: UIViewController {
    ...
    func viewDidLoad() {
        super.viewDidload()
        let task = Task()
        task.delegate = self
        task.someTask()
    }
    ...
}

// MARK: - TaskDelegate
extension SomeViewController.swift: TaskDelegate {
    func complete(result: AnyObject) {
        // 処理
    }

    func failed(error: NSError) {
        // 処理
    }
}

使うタイミング

この方法は、同じクラス内で処理を分けることができます。 他に参照したいプロパティがある時や、タイミングは問わないから何かしら非同期で処理を行い、その結果を受け取りたい時に使えます。

実装時のポイントはdelegateOptionalにしておくことです。そうすることで、実行時に落ちることを避けられます。

var delegate: TaskDelegate?

extensionで記述していますが、次の書き方と同じです。

final class SomeViewController.swift: UIViewController, TaskeDelegate {
    ...
    func viewDidLoad() {
        ...
    }
    ...
    func complete(result: AnyObject) {
        // 処理
    }

    func failed(error: NSError) {
        // 処理
    }
}

Closure

Closureを用いてコールバック処理を行います。

final class ClosureAlert {
    
    class func showAlert(parentViewController: UIViewController, title: String, message: String, completion: ((Bool) -> Void)?) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertControllerStyle.Alert)
        
        let yesAction = UIAlertAction(title: "見る", style: UIAlertActionStyle.Default, handler: {
            (action:UIAlertAction!) -> Void in
            // 引数にメソッドが使われてれば実行する
            if let completion = completion {
                // yesなのでtrue
                completion(true)
            }
        })
        
        let noAction = UIAlertAction(title: "見ない", style: UIAlertActionStyle.Default, handler: {
            (action:UIAlertAction!) -> Void in
            // 引数にメソッドが使われてれば実行する
            if let completion = completion {
                // noなのでfalse
                completion(false)
            }
        })
        
        alert.addAction(yesAction)
        alert.addAction(noAction)
        parentViewController.presentViewController(alert, animated: true, completion: nil)
    }
}

このクラスを次のように使います。

final class SomeViewController: UIViewController {
    ...
    func someMethod(){

        // クリック時に呼ばれるメソッドを定義
        let completeAction: (Bool) -> Void = {
            (isPositive) -> Void in
            if isPositive {
                // okの処理
            } else {
                // ngの処理
            }
        }
        
        // 実行するのはアラート選択時なのでcompleteActionに`()`はつけない。
        ClosureAlert.showAlert(self, title: "最新の記事", message: "注目です!",
            completion: completeAction
        )
    }

使うタイミング

続けて記述できるので、処理をまとめることができます。 メソッド自体を渡すことができるので、必要な値を詰めてしまえば他のプロパティを必要とせずに、処理を完結させることができます。

次のように直接書くこともできます。

final class SomeViewController.swift: UIViewController {
    ...
    func someMethod(){

        ClosureAlert.showAlert(self, title: "最新の記事", message: "注目です!",
            completion: { (isPositive) -> Void in
            if isPositive {
                // okの処理
            } else {
                // ngの処理
            }

        )
    }

Protocolとの使い分けは、Alert表示やLogの送信など、投げっぱなしで済む処理を書く際に使うのが良いと思います。また、同一スコープ内で処理を書きたい場合などにも使えます。

NSNotification

最後の一つは、NONotificationを用いたObserverによるコールバックです。

次のように記述します。

final class SomeViewController.swift: UIViewController {
    
    ...
    override func viewDidLoad() {
        super.viewDidLoad()
        // バックグラウンドから復帰した際のObserver
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "comeback:", name: UIApplicationWillEnterForegroundNotification, object: nil)
    }

    ...
    internal func comeback(notification: NSNotification) {
    }
    ...
}

これでアプリがバックグラウンドから起動した時にcomebackが呼ばれます。

自前ではハンドリングできないものなどは、用意されてるものがいくつか存在します。

  • 画面の復帰
  • バックグラウンドへいくとき
  • 全画面動画の全画面表示終了時
  • etc..

これはソース上でも離れた位置で記述できるので、結合度を低く保てます。

使うタイミング

いくつかの画面がある時に、ユーザアクションやOSの制御に起因するトリガーによって、現在見えている画面以外に対して何か変更を行いたい時などに使えます。 よくある「ある画面に遷移して戻ってきた時」の処理を書けます。

final class SomeViewController: UIViewController {
    ...
    // 画面遷移する際にNotificationを登録
    override func viewDidDisappear(animated: Bool) {
        super.viewDidDisappear(animated)
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "comeback:", name: "NOTIFICATION_COMEBACK", object: nil)
    }
    ...
    // 画面遷移してから戻ってくる時に呼ばれる
    internal func comeback(notification: NSNotification) {
       
        // 通知を解除する
        NSNotificationCenter.defaultCenter().removeObserver(self, name: "NOTIFICATION_COMEBACK", object: nil)
        // 何かしらの処理
    }
}
final class NextViewController: UIViewController {
    ...
    // 画面遷移する際に登録したNotificationをへ通知
    override func viewDidDisappear(animated: Bool) {
        super.viewDidDisappear(animated)
        NSNotificationCenter.defaultCenter().postNotificationName("NOTIFICATION_COMEBACK", object: nil)
    }
    ...
}

実装時の注意は、removeしないとメモリリークの可能性があります。通知が完了した際は速やかにremoveしましょう。

あまりないですが、同じNotificationに対して複数addした場合は、はじめに登録したものから順に呼ばれます。

UIApplicationで通知可能なイベント一覧はこちらにあります。

まとめ

個人的には次のように使おうと思います。

  • Protocol
    • コールバックのタイミングは問わないときや処理だけ分けたいとき
  • Closure
    • 同一スコープ内で処理を済ませたいとき
  • NSNotification
    • 関連のないクラス同士で処理を行いたいときや自前で実装できないタイミングのイベントに対して処理を行いたいとき

他に「私はこういう風に使う」「こういうやり方もあるよ」などがあれば教えて下さい。 今回紹介した3つの書き方を組み込んだサンプルをGitHubに置きました。

サンプル

参考

プロトコルとデリゲートのとても簡単なサンプルについて / Qiita

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

SwiftでNSNotificationCenterを使う / Qiita

UIApplication Class Reference / iOS Developer Library