2015年10月16日

Objective-CでCSVデータを扱う

田中です。

CSVデータの読み込みを行うのにライブラリを探していたところ、以下のライブラリが便利そうでした。

CHCSVParser

使い方

#import "CHCSVParser.h"

@interface ViewController ()
<
  CHCSVParserDelegate
>

@property (nonatomic) NSMutableArray *rows;
@property (nonatomic) NSMutableArray *columns;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    NSString *fileName = @"sample";
    NSBundle *bundle = [NSBundle mainBundle];
    NSString *path = [bundle pathForResource:fileName ofType:@"csv"];
    NSURL *url = [NSURL fileURLWithPath:path];
    
    CHCSVParser *parser = [[CHCSVParser alloc] initWithContentsOfDelimitedURL:url delimiter:','];
    parser.delegate = self;

    // 出力にダブルクォートのエスケープ無し
    parser.sanitizesFields = YES;

    // パース開始
    [parser parse];
}

#pragma mark - CHCParserDelegate methods


// パース開始
- (void)parserDidBeginDocument:(CHCSVParser *)parser
{
    self.rows = [NSMutableArray new];
}

// パース完了
- (void)parserDidEndDocument:(CHCSVParser *)parser
{
    // 完了時の処理
}

// パースエラー
- (void)parser:(CHCSVParser *)parser didFailWithError:(NSError *)error
{

}

// 列の読み込み開始
- (void)parser:(CHCSVParser *)parser didBeginLine:(NSUInteger)recordNumber
{
    self.columns = [NSMutableArray new];
}

// フィールドの値読み込み
- (void)parser:(CHCSVParser *)parser didReadField:(NSString *)field atIndex:(NSInteger)fieldIndex
{
    [self.columns addObject:field];
}

// 列の読み込み終了
- (void)parser:(CHCSVParser *)parser didEndLine:(NSUInteger)lineNumber
{
    [self.rows addObject:self.columns];

    self.columns = nil;
}

@end


CSVデータを扱う場合、カンマと改行に気をつけてデータの読み込みを行えばいいですが、値の中にカンマや改行された文やダブルクォートなどがある場合、何も考えずに読み込むとそれらがエスケープされていたりカンマの読み込み位置が間違ったりしてしまいます。
その辺りすべて面倒見てくれるので便利でしたが、値の保持などは自分で作業しないといけないのが少し手間なので自分で管理クラスを作成した方が後々楽そうです。
posted by Seesaa京都スタッフ at 19:13| Comment(0) | iOS | このブログの読者になる | 更新情報をチェックする

2015年10月07日

Nearby Messages APIを試してみる

久しぶりにガンプラを買いました、森脇です。
今回はGoogleが新たに提供を開始したNeaby Messages APIについて書いていきたいと思います。

Neaby Messages APIとは

Neaby Messages APIは、Googleが提供しているAPIの一つで、近くにあるスマートフォンやタブレット端末同士でのメッセージのやり取りを可能にするAPIです。

近距離での通信といえばNFCやBluetoothでのデータ送受信が頭に浮かびますが、Neaby Messages APIの場合はそうではなく、あくまでデータの送受信はサーバーを介して行われます。ただ、その送受信の際にBluetooth、BLE、WiFi、超音波などを用いることで相手を特定しています。
上記のような仕組みになりますのでネットワーク接続は必須となりますが、送信側の端末と受信側の端末が同じネットワークに接続している必要はありません。
ちなみにNearbyのもう一つのAPI、Nearby Connections APIは同じネットワークに接続している端末間でのデータの送受信を可能にするAPIですので注意が必要です。

事前準備

それではNeaby Messages APIを使うための準備を進めていきましょう。
まずGoogle Developers ConsoleにてAPIキーを取得します。GoogleMapsなどのAPIキーを取得する場合と手順は同じです。詳しい手順はこちらに記載してあります。

次にプロジェクトにGoogle Play Servicesを追加します。
build.gradleに以下の記述を追加します。Nearby Messages APIはGoogle Play Services 7.8.0以降でのみ扱えます。今回は8.1.0を使用します。
apply plugin: 'android'
...

dependencies {
    compile 'com.google.android.gms:play-services:8.1.0'
// Nearbyだけ追加したい場合は以下の記述でも問題ない
//    compile 'com.google.android.gms:play-services-nearby:8.1.0'
}
AndroidManifestに以下の記述を追加します。
<application ...>
    <meta-data
        android:name="com.google.android.nearby.messages.API_KEY"
        android:value="先ほど作成したAPIキー" />
    <activity>
    ...
    </activity>
</application>
これで準備は完了です。それでは以下に簡単な実装の流れを書いていきます。

Google API Clientに接続

Nearby Messages APIを使うには、まずGoogle API Clientを作成する必要があります。
この際、接続に成功した場合と失敗した場合のコールバックをそれぞれ追加することができます。
mGoogleApiClient = new GoogleApiClient.Builder(this)
     .addApi(Nearby.MESSAGES_API)
     .addConnectionCallbacks(this)
     .addOnConnectionFailedListener(this)
     .build();
Google API Clientへの接続に成功した場合の処理を以下のように記述します。ここではNearby Messages APIを使うにあたってユーザーからパーミッションの許可を得ているかどうかを確認しています。そして許可を得ていることが確認できた場合、メッセージを受信できる状態に移行します。
@Override
public void onConnected(Bundle bundle) {
    Nearby.Messages.getPermissionStatus(mGoogleApiClient)
            .setResultCallback(new ErrorCheckingCallback("getPermissionStatus", new Runnable() {
                @Override
                public void run() {
                    // メッセージの受信
                    subscribe();
                }
            }));
}
こちらを参考にNearby Messages APIを使用した際の共通のコールバックとして以下のように定義しています。パーミッションエラーが出た場合のみユーザーにパーミッションの許可を促すダイアログを表示します。
private class ErrorCheckingCallback implements ResultCallback<Status> {
    private final String method;
    private final Runnable runOnSuccess;

    private ErrorCheckingCallback(String method) {
        this(method, null);
    }

    private ErrorCheckingCallback(String method, @Nullable Runnable runOnSuccess) {
        this.method = method;
        this.runOnSuccess = runOnSuccess;
    }

    @Override
    public void onResult(@NonNull Status status) {
        if (status.isSuccess()) {
            Log.i(TAG, method + " succeeded.");
            if (runOnSuccess != null) {
                runOnSuccess.run();
            }
        } else {
            // Currently, the only resolvable error is that the device is not opted
            // in to Nearby. Starting the resolution displays an opt-in dialog.
            if (status.hasResolution()) {
                if (!mResolvingError) {
                    try {
                        // パーミッション確認ダイアログの表示
                        status.startResolutionForResult(MainActivity.this,
                                REQUEST_RESOLVE_ERROR);
                        mResolvingError = true;
                    } catch (IntentSender.SendIntentException e) {
                        Log.d(TAG, method + " failed with exception: " + e);
                    }
                } else {
                    // This will be encountered on initial startup because we do
                    // both publish and subscribe together.  So having a toast while
                    // resolving dialog is in progress is confusing, so just log it.
                    Log.i(TAG, method + " failed with status: " + status
                            + " while resolving error.");
                }
            } else {
                Log.d(TAG, method + " failed with : " + status
                        + " resolving error: " + mResolvingError);
            }
        }
    }
}
パーミッション確認ダイアログの結果はonActivityResultで受け取ります。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_RESOLVE_ERROR) {
        mResolvingError = false;
        // User was presented with the Nearby opt-in dialog and pressed "Allow".
        if (resultCode == Activity.RESULT_OK) {
            // メッセージの受信
            subscribe();
        } else {
            Toast.makeText(this, "Failed to resolve error with code " + resultCode,
                    Toast.LENGTH_LONG).show();
        }
    }
}
またNearby Messages APIはバッテリーの消耗が激しいため、必ずライフサイクルに応じて接続を解除するようにしてください。
@Override
protected void onStart() {
    super.onStart();
    // 復帰時に再接続
    if (!mGoogleApiClient.isConnected()) {
        mGoogleApiClient.connect();
    }
}

@Override
protected void onStop() {
    // バッテリー消費を抑えるために必ず接続を解除する
    if (mGoogleApiClient.isConnected() && !isChangingConfigurations()) {
        unsubscribe();
        unpublish();
        mGoogleApiClient.disconnect();
    }
    super.onStop();
}

メッセージの送受信

まず送受信するメッセージはバイト配列でやり取りされます。データサイズは3KB以内が推奨とされていて、画像や動画などの大きなファイルのやり取りは想定されていません。 送信の場合、PublishOptionsでメッセージの送受信が有効な距離や送信状態を維持する時間などをStrategyにて設定することができます。またGoogle Play Services8.1.0以降から送信状態が終了した際のコールバックを設定することができます。
/**
 * メッセージの送信
 */
private void publish() {
    // GoogleApiClientに接続しているか確認
    if (!isApiClientConnected()) {
        return;
    }
    PublishOptions options = new PublishOptions.Builder()
            .setStrategy(PUB_SUB_STRATEGY)
            .setCallback(new PublishCallback() {
                @Override
                public void onExpired() {
                    super.onExpired();
                    // 送信状態が終了した時のコールバック
                    Log.d(TAG, "publish onExpired");
                }
            })
            .build();

    Nearby.Messages.publish(mGoogleApiClient, mMessage, options)
            .setResultCallback(new ErrorCheckingCallback("publish"));
}

/**
 * メッセージの送信解除
 */
private void unpublish() {
    // GoogleApiClientに接続しているか確認
    if (!isApiClientConnected()) {
        return;
    }
    Nearby.Messages.unpublish(mGoogleApiClient, mMessage)
            .setResultCallback(new ErrorCheckingCallback("unpublish"));
}
受信の場合は、あらかじめメッセージを受信した時の処理としてMessageListenerを定義しておきます。
mMessageListener = new MessageListener() {
    @Override
    public void onFound(final Message message) {
        // メッセージを受信した時の処理
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                SimpleMessage simpleMessage = SimpleMessage.fromMessage(message);
                Toast.makeText(getApplicationContext(), simpleMessage.getId(), Toast.LENGTH_LONG).show();
            }
        });
    }
};
SubscribeOptionsではPublishOptionsと同様の設定に加えて、Strategyにて受信するメッセージのフィルターを設定することができます。
/**
 * メッセージの受信
 */
private void subscribe() {
    // GoogleApiClientに接続しているか確認
    if (!isApiClientConnected()) {
        return;
    }
    SubscribeOptions options = new SubscribeOptions.Builder()
            .setStrategy(PUB_SUB_STRATEGY)
            .setCallback(new SubscribeCallback() {
                @Override
                public void onExpired() {
                    super.onExpired();
                    // 受信状態が終了した時のコールバック
                    Log.d(TAG, "subscribe onExpired");
                }
            })
            .build();

    Nearby.Messages.subscribe(mGoogleApiClient, mMessageListener, options)
            .setResultCallback(new ErrorCheckingCallback("subscribe"));
}

/**
 * メッセージの受信解除
 */
private void unsubscribe() {
    // GoogleApiClientに接続しているか確認
    if (!isApiClientConnected()) {
        return;
    }
    Nearby.Messages.unsubscribe(mGoogleApiClient, mMessageListener)
            .setResultCallback(new ErrorCheckingCallback("unsubscribe"));
}
以上です。ソースコードはこちらに置いておきます。
こちらのサンプルでは受信が成功すると相手の端末上のUUIDがToastで表示されます。
device-2015-10-07-170127.png

所感

アプリへ組み込む場面がかなり限定される機能ではありますが、今までQRコードを撮影したり、振った端末同士の位置情報を介していたID交換などがよりスムーズに行えるようになるかもしれないですね。実装自体はかなりお手軽というわけではなく、少しクセのあるAPIだとは思いますが、AndroidだけではなくiOSでも利用可能ということなので、そのあたりも含めて今後の開発に期待していきたいです。

参考

Nearby  |  Google Developers
Nearby Messages APIでチャットみたいなのを作ってみる
posted by Seesaa京都スタッフ at 17:48| Comment(0) | Android | このブログの読者になる | 更新情報をチェックする

2015年10月01日

Xcodeで行うUIテスト

iOS担当のエモトです.

今回は約3ヶ月前に公開したテストに関する記事の続きになります.前回において,User Interface Testing / Recording のことに触れましたが,当時はXcode7がbetaだったので控えましたが,ついに問題なく書ける日がやってきました.

UIテストを使ってみる

使い方は非常に簡単です.シミュレーターを起動して,赤い丸ボタンの録画ボタンを押して,実際のUI操作を録画するだけです.
uiteest.png

そして,そのテストコードを動かせば,録画された動作を自動で行います.XCTAssertを組み合わせれば,画面操作して得られた結果を判定することもできます.簡単な操作でUIテストが行えるのでとても魅力です,しかし,問題として,まだ非同期テストには対応していませんので,万能ではありません.使用する場所は限られますが,標準でUIテストをサポートしてくれたのはうれしいことですね.
posted by Seesaa京都スタッフ at 12:00| Comment(0) | iOS | このブログの読者になる | 更新情報をチェックする