タグ別アーカイブ: Objective-C

XCode で動作環境に応じて API の URL などの設定を変更する

OSX, iOS アプリでも Rails の RAILS_ENV のように、動作環境に応じて値を変えたいという時があります。 この記事では、アプリから利用する Web API の URL を動作環境に応じて切り替えるのを例に、そのやり方を説明します。執筆時の開発環境は下記のとおりです。

  • XCode: Version 7.2
  • Swift: version 2.1.1

1. Build Configuration を作成する

PROJECT -> Info -> Configurations に、動作環境の分だけビルド設定を追加します。例えば、ローカル環境を対象としたビルド設定を追加したいときは、Debug Local と Release Local を追加します。追加するときは、それぞれ、元からある Debug と Release をコピーすると良いでしょう。

Build Configurations

2. Scheme を作成、編集する

Manage Schemes から、動作環境の分だけ Scheme を作成します。また、作成した Scheme それぞれをダブルクリックで編集し、ビルド設定を 1. で作成したものに変更します。例えば、ローカル環境用の Scheme として、AppLocal を作成したら、そのビルド設定は Debug Local および Release Local に設定しておきます。

Scheme

Manage Schemes

Build Configuration の変更

3. コンパイラの設定を変更する

Scheme の作成後は、ソース中で環境ごとに条件分岐ができるように、コンパイラの設定を変更します。PROJECT -> Build Settings 内をいじります。

Objective-C を使う場合は Apple LLVM 7.0 – Preprocessing -> Preprocessor Macros の値を変えます。各動作環境が判別できるように、適当な変数を定義すると良いです。今回の例だと、Debug Local と Release Local に LOCAL=1 を設定します。

Objective-C のコンパイラ設定の変更

Swift を使う場合は Swift Compiler – Custom Flags -> Other Swift Flags の値を変えます。 -D <flag> の形式で <flag>true にできるようなので、そのように書きます。今回の例だと -D LOCAL を追加すると良いです。また、複数の値を設定する場合は -D DEBUG -D LOCAL のように書くことができます。

Swift の方にはなぜか DEBUG の値が設定されていないため、ついでに設定しておくと良いと思います。

Swift のコンパイラ設定の変更

4. Conditional compilation statement を書く

Conditional compilation statement で条件分岐させて、開発環境ごとに異なる値を変数に入れてやります。

Objective-C の場合は以下のように書きます。

#ifdef LOCAL
  NSString * const kAPIBase = @"http://localhost.example.com/api/";
#else
  NSString * const kAPIBase = @"http://production.example.com/api/";
#endif

Swift の場合は以下のように書きます。

#if LOCAL
  let kAPIBase = "http://localhost.example.com/api/"
#else
  let kAPIBase = "http://production.example.com/api/"
#endif

参考

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行目)。

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

Objective-C でコールバックを持つメソッドを実装する方法について

非同期に結果が返ってくる処理を書く場合、Objective-C では @Protocol を定義して、デリゲートメソッド内で結果をもらうのが良いのかもしれませんが、JavaScript を書いてるとコールバックで結果を取得したいと考えてしまいます。この記事では、Objective-C でコールバック付きのメソッドを作る方法を解説します。(コールバックって呼んでいいのかわかりませんが)

メソッドの書き方

サーバーからデータを取得し、そのデータを UIImage として返すメソッドを例とします。以下がメソッドの簡単な書き方です。

// メソッド定義
- (void)requestImageWithURL:(NSURL *)url completion:(void (^)(UIImage *image, NSError *error))completionHandler;

 

// メソッド実装
- (void)requestImageWithURL:(NSURL *)url completion:(void (^)(UIImage *, NSError *))completionHandler
{
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        if (connectionError) {
            completionHandler(nil, connectionError);
            return;
        }
        UIImage *image = [UIImage imageWithData:data];
        completionHandler(image, nil);
    }];
}

メソッドの定義

定義部分のポイントは completionHandler の書き方です。completionHandler の型に関数のようなものが書いてあります。これは関数のポインタを表しています。void が戻り値を表しており、^ がポインタであることを表しており、UIImage *image, NSError *error が引数を表しています。

このようにメソッドの引数に関数のポインタを渡すことにより、定義したメソッド内で、この関数を実行することができるようになります。この関数をメソッド内の非同期処理が終了したタイミングで呼び出せば、コールバック関数のように使うことができます。

メソッドの実装

非同期処理が終了したタイミングで仮引数の  completionHandler を実行しています。コールバック関数でメソッドの値を返す時は、コールバック関数の引数に返したい値を与えます。これにより、呼び出し元の関数の、実引数として与えられた関数内でメソッドが返す値にアクセスすることができます。

注意しなければならないのは completionHandler はObjective-C のメソッド呼び出しではなく、C言語の関数呼び出しのように書かなければならないということです。

おわりに

コールバックの概念は理解しづらいですが、理解できるといろいろと書けるようになるので、書き方は覚えておいて損はないと思います。関数のポインタはまだまだ色々と使い道がありそうです。