ゆずとみかんといちご

ゆるゆる書きます

Nearby Messages by Swift

この記事は 学生エンジニア Advent Calendar 2015 17日目の記事です.

こんにちは.

一昨日引越しして荷解きと収納が嫌になったので普段は書かないブログを積極的に書いていこうと思います.

今日は Google の Nearby Messages API についてです.

developers.google.com

この API は今年の Google Play Services 7.8 リリースの際に追加された Nearby API の中の1つで, 音を使って他の端末と情報共有をするための API です.iOS 版はまだ β版らしいですが iOS 7.0 以降で使えるみたいです.

Nearby には今回紹介する Nearby Messages APINearby Connections API があります.Messages は文字通り複数の端末とメッセージを共有するために,Connections はこちらも文字通り複数の端末に接続するために用いられます.Nearby API を使うときは端末の Wi-Fi, Bluetooth 共にオンである必要があります.

既に試してみたというブログや,この API を使った Android アプリの紹介があったりするのでそちらもどうぞ.

Android - Nearby Messages APIでチャットみたいなのを作ってみる - Qiita

Nearby Message API を試してみた - 質量

Nearby API であなたを取り巻く世界とつながる - Google Developer Japan Blog

さて,これらは全て Android の記事ですので,今回は iOS で試してみたいと思います.というか,以前ハッカソンで初めて使ったのでそのときのことを書きます.

(雑な記事です.紹介したブログのような親切さは欠片ほどもありませんので予めご了承ください)

API の有効化

こちらはほぼ Android と同じ手順です.Android 版は先ほど紹介した記事にもありますし,iOS 版は英語だったら公式に載っているので読んでください.

developers.google.com

ざっくり日本語で説明すると,

  1. 最新の Xcode をインストール (6.3 以降なら OK っぽいです)
  2. CocoaPods をインストール
  3. Nearby API を CocoaPods で入れる
  4. Google アカウント作成
  5. API Key の取得
  6. プログラム中で message manager object を作成

となります.

1, 2 の Xcode や CocoaPods, 3 の pod install あたりは iOS 開発者なら問題ないと思います.また,4 の Google アカウント作成もだいたいの方は既に (いくつか …???) 持っていらっしゃると思います.(ちなみに今流行りの Carthage にも入っているかは分からないです.知見共有お願い致します)

5 以降,Google Developers Console を使って API を有効化します. 公式に書いてあるそのままですが,

  • API Key の取得
    1. プロジェクト作成 ... (a)
    2. Nearby API の有効化
    3. Client key の作成

という手順を踏みます.

各項目の細かな手順は公式に書いてありますし,割と親切な GUI なので迷うことはないと思います.Google Developers Console を初めて使った私ですら大丈夫でしたので…

iOSAndroid

ハッカソンで使ったときは私が iOS, もう一人のエンジニアが Android 担当で Nearby Messages を使って情報をやり取りしていました. 公式の手順5の下に ★ Note とあるように,iOS, Android 両方使いたいときは,(a) で同じプロジェクトを使ってください.当時は Android 側で作ったプロジェクトに iOS の project name と bundle identifier を追加して API Key をもらっていたような気がします.

Bridging-Header

公式通り Objective-C で書いている方はここは飛ばしてください.Swift で書いている方は,Nearby APIObjective-C なので Bridging-Header を作ります (Bridging-Header を作ったことがある方もここは飛ばしてくださって大丈夫です).

YourProjectName-Briding-Header.h に

#ifndef YourProjectName_Bridging_Header_h
#define YourProjectName_Bridging_Header_h
#import <GNSMessages.h>
#endif

を書いて,

  • TARGETS
    • Swift Compiler - Code Generation

を設定します.

Create Message Manager Object

手順 6 でようやっとプログラムに何か書きます.Message Manager Object (GNSMessageManager) の初期化です.Message Manager Object は Publication, Subscription を管理するためのクラスです.たぶんどこで初期化してもよいのですが,ハッカソン時はアプリ起動時から pub / sub を行いたかったので AppDelegete.swift 内に書きました (本当は Nearby Messages 用のクラスを別に作って AppDelegate 内でインスタンス作るとかの方がよいのでしょうけど,ハッカソンなので設計などは許してください).

具体的には

let nearbyAPIKey = "Your Nearby API Key"
var messageManager: GNSMessageManager?

func initMessageManager () {
    messageManager = GNSMessageManager(APIKey: nearbyAPIKey)
}

でいいと思います.思います,というのは,私のソースコードにはそう書いてないからです.公式もそうなっていますが,今後,マイク使用の許可,Wi-FiBluetooth がオンになっているかなどのトラッキングをするのでここはもうちょっと膨らみます (なので最初から initMessageManager を作っておきました).

アプリの初回起動でこんなダイアログがでてきて,Nearby API を使うのに必要なマイク,Wi-FiBluetooth の使用許可を求めてきます.

gyazo.com

ちなみに messageManager は Optional にしていますが,今流行りの - [要出典] lazy var でもいけるんじゃないでしょうか.var + ! ?? 知らない子ですね.

Pub / Sub

いよいよメッセージの送信,受信をしてみましょう.

ユーザ設定と Permission のトラッキング

その前に,先ほど書いたユーザへの Nearby API 使用のための様々な許可 etc. のトラッキングを設定していきます.

ユーザ設定のトラッキング

公式 だと 'Tracking user settings that affect Nearby' という部分をやります.先ほどの initMessageManager では マイク,Wi-FiBluetooth 使用の Permission 確認ダイアログが出てきただけでした.ここでユーザがこれらの使用を許可しなかったり,Bluetooth がオフになっているときにアラート (アラートじゃなくてもいいのですけど) を表示してユーザに Nearby API が使えないことを知らせることができます.次のサンプルではとりあえず print しているだけです.適宜,何かしらの UI を作ってください.

func initMessageManeger () {
    messageManager = GNSMessageManager(APIKey: nearbyAPIKey) {
            (params: GNSMessageManagerParams!) -> Void in

            // This is called when microphone permission is enabled or disabled by the user.
            params.microphonePermissionErrorHandler = { hasError in
                if (hasError) {
                    print("Nearby works better if microphone use is allowed")
                }
            }

            // This is called when Bluetooth permission is enabled or disabled by the user.
            params.bluetoothPermissionErrorHandler = { hasError in
                if (hasError) {
                    print("Nearby works better if Bluetooth use is allowed")
                }
            }

            // This is called when Bluetooth is powered on or off by the user.
            params.bluetoothPowerErrorHandler = { hasError in
                if (hasError) {
                    print("Nearby works better if Bluetooth is turned on")
                }
            }
        }
}

ちなみにこれは

GNSMessageManager(APIKey: nearbyAPIKey,
                    paramsBlock: {
                        (params: GNSMessageManagerParams!) -> Void in
                        ...
                        ...
                        })

の Trailing Closure です.でもここまで長いと Handler を別に定義してあげるのがよさそうですね…あと抽象化したい…

Permission のトラッキング

公式 の 'Tracking the Nearby permission state' のところです.ユーザが Nearby API の使用を許可した / しなかったときに何かしらのアクションを起こすことができます.Permission Tracking には GNSPermission というクラスを使います.GNSPermission.isGranted() で 許可したかどうかを取ってくることができ,GNSPermission.setGranted (granted: Bool) で permission を set します.

次のサンプルは,画面の左上にボタンを作り (i.e. ツールバーの left button),ボタンを押すことで Nearby を許可したりしなかったりを切り替えるものです.

var nearbyPermission: GNSPermission?
// messageViewController: ツールバーを設置する画面 (ViewContoller)

func setupNearbyPermission () {
        let changedHandler: Bool -> Void = {
            [unowned self] granted in
                let answer = String(format: "%@ Nearby", granted ? "Deny" : "Allow")
                self.messageViewController.leftBarButton =
                    UIBarButtonItem(title: answer,
                                    style: UIBarButtonItemStyle.Plain,
                                    target: self,
                                    action: "toggleNearbyPermission")
        }
        
        nearbyPermission = GNSPermission(changedHandler: changedHandler)
    }
    
// Toggles the permission state of Nearby.
func toggleNearbyPermission() {
    GNSPermission.setGranted(!GNSPermission.isGranted())
    }

公式にも注意書きがありますが,ユーザの意図に反して permission を set しないでくださいね.

Publication

長かった…ここまで本当に長かったです…ブログ書くのって本当に体力使いますよね……

公式だと Publishing a message のところです.

GNSMessageManagerインスタンスmessageManager に送りたいメッセージを入れて GNSPublication クラスのインスタンス (以下では publication) に渡してあげます. publicationnil ではない間ずっとメッセージは送信し続けられます.逆に,publicationnil にすると送信は止まります.以下はメッセージとして JSON を送るようにしています (公式ではメールアドレスですね).ちなみに JSON を扱うのに SwiftyJSON を使っています.

var publication: GNSPublication?

func startPublication () {
        if let mgr = self.messageManager {
            let json = JSON(someDictionary)
            do {
                // JSON.rawData() が exception を出すようになっているので try します
                let message = GNSMessage(content: try json.rawData())
                
                // ここで publication にメッセージが入った時点で publish が開始される
                publication = mgr.publicationWithMessage(message)
            }
            catch {
                // 本来はちゃんとなんらかの対処をしてあげてください
                print("json.rawData() throwed an exception")
            }
        }
        else {
            // 本来は (ry
            print("messageManager is nil")
        }
    }
    
    func stopPublication () {
        publication = nil
    }

Subscription

次はメッセージの受信です.

こちらも Publication のときと同様,messageManager でハンドラなどを設定して GNSSubscription クラスのインスタンス (以下では subscription) に渡してあげます.subscriptionnil ではない間ずっと受信できるよう待機しています.

受信する際,subscript にはなんらかのメッセージを受信した際に呼ばれるハンドラと,メッセージを失ったときに呼ばれるハンドラの2つの GNSMessageFoundHandler が渡されます.

func startSubscript () {
    let messageFoundHandler = {
        [unowned self] (message: GNSMessage!) in
        let messageStr = String(data: message.content, encoding: NSUTF8StringEncoding)
        if let data = messageStr?.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false) {
            let content = JSON(data: data)
            ...
            ...
        }
        else {
            // 本来は (ry
            print("data is nil")
        }
    }
        
    let messageLostHandler = {
        [unowned self](message: GNSMessage!) in
        if let content = String(data: message.content, encoding: NSUTF8StringEncoding) {
            // 本来は (ry
            print("Lost message: \(content)")
        }
        else {
            // 本来は (ry
            print("cannot convert message.content to String")
        }
    }
        
    subscription = messageManager.subscriptionWithMessageFoundHandler(messageFoundHandler,
        messageLostHandler: messageLostHandler)
}

func stopSubscription () {
    subscription = nil
}

使ってみて

ハッカソンのときは私ももう一人も Nearby を使うのは初めてでしたが割とすんなり使えて,しかもちょっと大きめの JSON をやり取りすることができたので,今後なにかをサクッと共有したいときには使っていけるなぁと思いました.ちょっと新しい技術使ってる感があって (あとこなしちゃん -後述 がいたのもあって) ハッカソンはとても楽しかったです.

ちょっとよく分かってないのですが,GNSPublicationGNSSubscriptionnil になると送受信が止まるというのはどうなんでしょうね? 公式の Android 版 をみると

@Override
protected void onStop() {
    if (mGoogleApiClient.isConnected()) {
        // Clean up when the user leaves the activity.
        Nearby.Messages.unpublish(mGoogleApiClient, mMessage)
                .setResultCallback(new ErrorCheckingCallback("unpublish()"));
        Nearby.Messages.unsubscribe(mGoogleApiClient, mMessageListener)
                .setResultCallback(new ErrorCheckingCallback("unsubscribe()"));
    }
    mGoogleApiClient.disconnect();
    super.onStop();
}

こんな感じで unpublish なるものが存在していて,こちらのほうがよいのではないかなと思うのです (というか Android 版めっちゃ親切にいろいろ書いてありますね…!!).

(GNSPublicationGNSSubscriptionインスタンスGNSMessageManager から設定を渡されるとき以外出てくることはないので,そのうち意図的にではなく implicit に nil になってしまうこととか…ないですかね… GC 的な…ない?)

ソースコード

あるけどまだどこにも載せてないですすみません. ハッカソンのときは受信したメッセージを元に Konashi にコマンドを送ったりしていたので今回のお話にはちょっと余計なコードが入っているのと,ちょっと汚かったのでリファクタリングしてたのですがよく考えたら端末を1台しか持っていなくて動作確認ができない,というのが理由です.本当にすみません.(Android 用のも書いて動作確認してみるかなぁ…)

次回は kmd_09 さんです.