カテゴリー
iOS Swift

iOS & Swift 102 callbackとdelegate

処理の中には、一定の時間がかかるものがあり(インターネット越しにあるAPIサーバーからレスポンスを待つ、など)、その処理が終わった後にプログラムに「これをやっといてね」と「頼み」たいことがよくあります。一般的にはそれらの処理を行う関数をcallback関数と呼びますが、Swiftではclosureを使ったパターン、delegateを使ったパターンがよく見られます。

まず closureで実装した例を説明します。

// PlaySound.swift

class PlaySound {

  // [1] 処理に1秒かかる作業を10回実行しているシミュレーション。1秒ごとにcallbackを呼ぶ。callbackはclosure型で指定
  public func play(callback: (Int) -> Void) {
    for i in 0..<10 {
      Thread.sleep(forTimeInterval: 1)
      callback(i)
    }
  }
}

// ViewController.swift

class ViewController: UIViewController {
 // PlaySoundクラスのインスタンス
  let playSound = PlaySound()

  override func viewDidLoad() {
    super.viewDidLoad()
    // [2] インスタンスメソッドをコールする時に関数を渡している
    playSound.play(callback: printNum)
  }
  
  // コールバック関数の実装をここで記述出来る
  private func printNum(num: Int) {
    print(num)
  }
}

[1] 関数 play() はclosure型の引数を取ります。

closureとは、名前の無い関数のことで、以下のように( ) -> ( )のような形で書くことができます。最初の( )にはそのclosureの引数を、->(矢印)の後の( )には返り値を記述します。

( ) はtuple(タプル)という型で、複数の異なる型を入れられます。

ここで面白いのは、play( )関数の引数として渡すclosureの型は指定しますが、closureでどのような処理をするのかは、play( )関数を定義する時には決めなくてもよく、その関数を呼ぶ時に決められる、ということです。

[2] 上記の例ではplay( )に渡すためのprintNum(num: Int)という関数をViewControllerクラスで定義し、play(callback: printNum)という形で渡しています。

(Int) -> Void というclosureの型と printNum(num: Int)は合っている必要があります。

func printNum(num: Int) -> Void { // 返り値がない場合 -> Voidは省略可

}
// closureの書き方の例

// 引数なし、返り値なし
() -> ()
() -> Void

// 引数はInt型、返り値はなし
(Int) -> ()
(Int) -> Void

// 引数はIntとFloat、返り値はIntとBool
(Int, Float) -> (Int, Bool)

次に、Delegateパターンの説明をしますが、その前にProtocolについて理解する必要があります。

Protocolとは、この処理を担当するなら、これらの関数(や変数)は備えといてね、という「規約」を決めるものです。

// PlaySound.swift

// [1] Protocolを定義
protocol PlaySoundDelegate {
  func printNum(num: Int)
}

class PlaySound {

 // [1] Protocol型の変数を宣言
  var playDelegate: PlaySoundDelegate!

  public func play() {
    for i in 0..<10 {
      Thread.sleep(forTimeInterval: 1)
   // ここでProtocolの関数を使う
      playSoundDelegate.printNum(num: i)
    }
  }
}

// ViewController.swift

class ViewController: UIViewController,
                        PlaySoundDelegate // [2]-b   Protocolに準拠
{

  let playSound = PlaySound()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    // [2]-a   delegateをself(ViewControllerクラス)に指定
    playSound.playSoundDelegate = self
    playSound.play()
  
  }
  // [2]-c   Protocolの関数を定義
  func printNum(num: Int) {
    print(num)
  }
}

[1] delegateは「任せる」という意味です。PlaySoundクラスの色々な処理が完了したら、その次に任せたい処理(関数)をProtocolに記述します。Protocolには関数の宣言だけをし、定義はProtocolに準拠するクラスに記述します。

[2] 上記の例ではViewControllerクラスをPlaySoundDelegate Protocolに準拠させることにします。

[2]-a playSound.playSoundDelegate = self はViewControllerクラスがdelegateを担当しますよ、と指定しています。

[2]-b その際、クラスがProtocolに準拠していないとエラーになります。

[2]-c また、Protocolの必須関数を定義していないとエラーになります。

どちらの実装方法でも同じようなことは実現出来るので、まずは一つのやり方にじっくり慣れると、より理解が深まると思います。