Katashin .info

Objective-C でデリゲートメソッド内でコールバックできるようにする

Objective-C では非同期処理の完了後の処理をデリゲートメソッドで行うものがあります。デリゲートメソッドで処理するのは、単純なシステムなら問題ありませんが、システムが複雑になってくると、1つのデリゲートメソッド内に多くの条件分岐ができて、コードが読みにくくなる場合があります。こういった時にはデリゲートメソッドの代わりにコールバック関数を用いることで、コードがすっきりします。既に存在するライブラリが、非同期処理完了後の処理をデリゲートメソッドで実行するようにしている場合は、デリゲートメソッド内でコールバック関数を呼ぶようなラッパークラスを書く必要があります。コールバック関数については以前の投稿を読むと分かるかもしれません (Objective-C でコールバックを持つメソッドを実装する方法について)。

非同期処理完了後の処理をデリゲートメソッドで行うと以下のようになります。例として、WebSocket ライブラリの square/SocketRocket を用いています。今回は説明を簡単にするために、クライアントが送信したメッセージを受け取ったら、即座に同一のメッセージを返すサーバーを仮定します。

// メッセージ送信
- (void)sendMessage:(NSString *)message {
  [_socket send:message];
}

// メッセージ受信 (SocketRocket のデリゲートメソッド)
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {
  // メッセージ受信後の処理を書く
  NSLog(@"%@", message);
}

メッセージ受信後に1つの処理のみを行うのであれば上記で充分ですが、場合によって異なる種類の処理を行いたい場合は工夫をする必要があります。例えば、サーバーから受け取るメッセージの種類によって処理を分けたい場合は、サーバーから受け取ったメッセージ内に、type のようなパラメータを加えて、デリゲートメソッド内で条件分岐をするということが考えられます。また、上記の sendMessage メソッドを呼び出すクラスが複数あり、クラス毎にメッセージ受信後の処理が違うというような場合は、デリゲートメソッドのみでは対応できません。こういった場合は、NSNotificationCenter を使った通知で条件分岐をするとうまくいきます。また、sendMessage メソッドの引数でコールバック関数のポインタを渡すようにし、デリゲートメソッド内でそのコールバック関数を呼ぶことでも対応できます。今回は、デリゲートメソッド内でこのコールバック関数を呼ぶ方法を説明します。

デリゲートメソッド内でコールバック関数を呼ぶには、インスタンス変数にコールバック関数を保存する必要があります。また、非同期処理を行うメソッドは並列で実行される可能性があるため、コールバック関数は複数保存可能で、かつ、デリゲートメソッド内で対応するコールバック関数を判別可能であるべきです。これらは以下の様なコードで実現できます。ここでも、サーバーはクライアントから受け取ったメッセージを即座にそのまま返すと仮定します。また、NSDictionary と JSON の変換部分は擬似コードです。インスタンス変数の初期化部分も適当なので、自分で実装する時は書き換えることをおすすめします。

// インスタンス変数
@property NSMutableDictionary *callbacks;
@property int *callbackID;
// 初期化
- (void)initialize {
  _callbacks = [NSMutableDictionary dictionary];
  _callbackID = 0;
}

// メッセージ送信
- (void)sendMessage:(NSString *)message completion:(void (^)(NSString *))completion {
  // メッセージに _callbackID を含める
  NSDictionary *dic = @{@"message": message, @"id": [NSNumber numberWithInt: _callbackID]};

  // _callbackID をキーとして、コールバック関数をインスタンス変数に保存
  [_callbacks setObject:completion forKey:[NSNumber numberWithInt:_callbackID]];
  _callbackID++;
  [_socket send:[dic json]]; // NSDictionary を JSON に変換して送信 (擬似コード)
}

// メッセージ受信 (SocketRocket のデリゲートメソッド)
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {
  NSDictionary *dic = [message dictionary];
  // JSON を NSDictionary に変換 (擬似コード)
  // ID に対応するコールバック関数を取得
  void (^callback)(NSString *) = _callbacks[dic[@"id"]];

  // コールバック関数を呼ぶ
  callback(dic[@"message"]);
}

上記のコードのように、コールバック関数毎に一意な ID を割り当てることができ、デリゲートメソッド内でこの ID を取得することができれば、デリゲートメソッド内でコールバック関数を呼ぶことができます。今回の例では、インスタンス変数に NSMutableDictionary 型の変数 (callbacks) と int 型の変数 (callbackID) を用意してこれを行いました。新しいコールバック関数が与えられる度に、現在の callbackID の値をキーとして、コールバック関数を callbacks 内に保存しています (14行目)。また、サーバーへ送信するメッセージ内に callbackID の値を含めています (10-11行目)。サーバーはメッセージをそのまま返すので、メッセージに含めた callbackID の値をデリゲートメソッド内で取得することができます (25行目)。取得した callbackID を用いてコールバック関数を取得し、実行すれば完了です (25-28行目)。

今回はサーバーがメッセージをそのまま返すものだったのでこのままでは役に立ちませんが、あるパラメータに与えられた値のみはそのまま返すようなサーバーにすることで実用的になります。また、最終的にデリゲートメソッド内でコールバック関数を取得できれば良いので、考えれば色々なパターンで実装できそうです。