どうも広報担当です.今回は新作アプリの紹介ではなく,フレームワークの解説記事です.
iOSのとあるフレームワークと1年間戦ってきたので,その成果と記録を記事として残しておきます.
同様の解説でいくつか良質な記事がネット上に転がっていますが,Objective-Cで内容が古かったりするので,せっかくなので自分への備忘録も兼ねて改めて書いておこうかなと思った次第です.
はじめに
このフレームワークで何ができるのか
近距離に存在するiOS端末間のP2P通信を可能にします.サーバいらずでネット環境も必要なし.扱えるデータの種類は,NSDataを送れるので実質なんでもありです.
一応,同時に接続できる最大の台数は8台まで,ということになっています.
ここでの”一応”というのは8台以上接続する方法が実は存在するためです(次の記事で書きます).
中身の通信方式
内部で使用されている通信方式は3種類.
- Bluetooth通信
- アクセスポイントを経由するWi-Fi通信
- 端末間での直接のWi-Fi通信
どの方式を用いるかをプログラム内で明示することはできないようです.通信方式の決定は端末本体の設定に依存しています.
大まかな接続の流れ
接続にあたって,それぞれの端末は,他端末を探索して招待する役割と,自身の存在を広報して招待を受ける役割に分かれます.
ここでは説明を分かりやすくするために,それぞれの役割を果たす端末を,ホスト,クライアントと呼ぶことにします.
接続のためにそれぞれの端末は,おおまかにこのようなステップを踏みます.
- クライアントが自端末の広報を開始
- ホストがクライアントを発見して招待
- 招待を受けたクライアントがそれに対して返答(yes/no)
- yesの場合にホストは接続処理を実行
ここからは,これらのステップを踏んで接続を行うために必要なAPIについて説明します.
Multipeer Connectivityの構成
ここからはMultipeer Connectivityの中身の話です.いくつかコードを出して説明しますが,環境はXCode7.2とswift2.1です.
ま,そのまま使えるコードはあまり出せそうにありませんけどね.
とりあえず関連するクラスの一覧はこちら.
- MCPeerID
- MCSession
- MCNearbyServiceBrowser
- MCNearbyServiceAdvertiser
- MCAdvertiserAssistant
- MCBrowserViewController
で,これに合わせてデリゲートがいくつかあります.
- MCSessionDelegate
- MCNearbyServiceBrowserDelegate
- MCNearbyServiceAdvertiserDelegate
- MCAdvertiserAssistantDelegate
- MCBrowserViewControllerDelegate
全部を実装する必要がある訳ではなく,用途に応じていくつか使い分けるものがあります.
MCNearbyServiceBrowserとMCBrowserViewController,MCNearbyServiceAdvertiserとMCAdvertiserAssistantがこれにあたります.
MCBrowserViewControllerとMCAdvertiserAssistantについてはあまり触っていないため説明がどうしても薄くなってしまいそうですがそこはご勘弁を…
MCPeerID
接続しているそれぞれの端末を区別するためのIDのようなもの.自端末のIDは自端末側で生成してあげる必要があります.
・displayname
端末の表示名を表すプロパティです.イニシャライザで指定してやる必要があります.
一度生成してからのdisplayNameの変更は不可.
1 2 |
//MCPeerIDの生成 var myPeerID : MCPeerID = MCPeerID(displayName: UIDevice.currentDevice().name) |
コードはこんな感じ.ここからはMCPeerIDが度々出てきます.
MCSession
Multipeer Connectivityにおける全ての接続している端末との通信は,このクラスのインスタンスを介して行われます.
データの送受信もこれ.接続済端末の出入りの検知もこれ.
ここではとりあえず,インスタンスの生成のところだけ触れておきます.
1 2 3 |
//MCSessionの生成 var session : MCSession = MCSession(peer : myPeerID) session.delegate = self |
MCNearbyServiceAdvertiser
自端末の存在をアドバタイズ(広報?)するためのもの.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//イニシャライザ var advertiser : MCNearbyServiceAdvertiser = MCNearbyServiceAdvertiser(peer: myPeerID, discoveryInfo: nil, serviceType: serviceType) advertiser.delegate = self //アドバタイズの開始 advertiser.startAdvertisingPeer() //アドバタイズの終了 advertiser.stopAdvertisingPeer() //------------ //デリゲート //------------ //他端末から招待を受けた時に呼ばれる(実装必須) func advertiser(advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: NSData?, invitationHandler: (Bool, MCSession) -> Void) { //招待への返答(true/false) invitationHandler(true,session) } //アドバタイズが開始できなかった場合に呼ばれる,エラー処理があればここ func advertiser(advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: NSError) { } |
イニシャライザでは2つ指定してやるべきものがあります.
・discoveryInfo
端末を探索しているホストに対して追加で提示できる情報です.(NSDictionary)
ホストはこの情報の中身を発見時に受け取ることによって,招待を行うかどうかを決定するなど,細かい制御を行うことが出来ます.
とくに提示する内容がなければ,nilで可.
・serviceType
これはNSSting型のプロパティで,サービスを一意に特定するために用いられます.アプリ毎に固有の識別子を持たせることによって,全く別のアプリと通信してしまうことを防ぎます.
他端末から招待を受けた時に呼ばれるデリゲートでは,招待を受けるのに合わせて,いくつか情報を受け取ります.
・peerID
どの端末から招待を受けたかどうかをMCPeerIDで受け取っています.
・withContext
次のところで説明しますが,招待する側は発見した端末に対して招待するとき,追加で情報を提示することが可能になっています.この情報をNSNearbyServiceAdvertiserでは,withContextとしてNSData型で受け取ります.
で,これを受けて返事をするのが
invitationHandler(true,session)
となるわけです.ひとつ目の引数で接続するかどうかをtrue/falseで返事します.
ここで,先に作っておいたsessionを渡してやる必要があります.
falseの場合はnilでも前は大丈夫だったんですが,何故か最新バージョンではsessionを渡してやる必要があるようです.
別に使わないんだしいいじゃん…と思うけど,エラーがうるさいのでやむなし.
NSNearbyServiceBrowser
他端末を探索して,招待を行うためのもの.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//イニシャライザでは自端末のMCPeerIDとserviceTypeを指定しておく. var browser : MCNearbyServiceBrowser = MCNearbyServiceBrowser(peer: myPeerID, serviceType: serviceType) browser.delegate = self //探索開始 browser.startBrowsingForPeers() //探索終了 browser.stopBrowsingForPeers() //------------ //デリゲート //------------ //他端末の発見時に呼ばれる func browser(browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) { browser.invitePeer(peerID, toSession: session, withContext: nil, timeout: self.timeout) } //端末を見失った時に呼ばれる func browser(browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { } //探索を開始出来ない場合に呼ばれる,エラー処理がアレばここ func browser(browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: NSError) { } |
ここで大事なところは,端末の発見時に呼ばれるデリゲートの部分ですね.端末を発見した時には,幾つかの情報を受け取ります.
・peerID
どの端末を発見したかという情報です(MCPeerID型).
・withDiscoveryInfo
先述したMCNearbyServiceAdvertiserで設定しているdiscoveryInfoを,ここで受け取っています.(NSDictionary型)
これを受けて端末の招待を行います.
実際に招待をしているのは
browser.invitePeer(peerID, toSession: session, withContext: nil, timeout: self.timeout)
の部分.
それぞれの引数の意味は以下の通り.
・peerID
招待する端末のMCPeerIDを渡す.受け取ったpeerIDをそのまま渡せばOKです.
・toSession
どのSessionに対して招待を行うかを,先に作っておいたMCSessionのインスタンスを渡すことで知らせます.
招待する側とされる側でsessionを交換しあうことで接続が確立されるわけですね.
・withContext
相手に対して追加で提示する情報です.こちらはwithContextと違い,NSDictionaryではなくNSDataを送信することが可能です.
何も送ることがない場合はnilで可.
・timeout
招待に対して返答がなかった場合,タイムアウトすることで処理されるわけですが,その時間の長さの設定です.
MCSessionふたたび
ここまで説明した内容で,とりあえず端末が接続されるとこまではなんとかなりました.ここからはデータの送受信についてです.
接続さえ終わってしまえば後はもうほとんどがSessionに対する操作になります.
Multipeer Connectivityにおけるデータ送信の手段は3つに分けられます.
- NSDataを用いる送信
- NSURLを用いる送信
- ストリーミングによる送信
では,それぞれについて説明します.
ここからは,若干オリジナルのコードも入っていますがその辺りは適当に読み替えてください.
NSDataによる送信
1 2 3 4 5 6 7 8 |
//NSDataの送信 func sendData (data : NSData) throws { try session.sendData(data, toPeers: self.session.connectedPeers, withMode: MCSessionSendDataMode.Reliable) } //デリゲート //NSDataを受信した時に呼ばれる func session(session: MCSession, didReceiveData data: NSData, fromPeer peerID: MCPeerID) {} |
データの送信には,sendDataを用います.送信するデータの実体,送信先,送信モードの3つについて決めてやる必要があります.データの実体については,送信したいNSDataをそのまま渡せばOK.送信先はMCPeerIDの配列として渡すことから,複数の端末を同時に送信先に指定してやることも可能です.ここではsessionのプロパティであるconnectedPeersを用いることで,sessionに接続中の全ての端末についてMCPeerIDの配列を取得しています.3つ目の送信モードについては,ReliableとUnreliableの2つのうちどちらかを選択します.必要に応じてどちらかを選択しましょう.
データの受信した場合は,どのSessionで受信したか,どんなデータを受信したか,どの端末から受信したかについて取得しています.
NSURLによる送信
1 2 3 4 5 6 7 8 9 10 11 |
//NSURLからの送信 func sendResourceAtURL( resourceURL: NSURL, withName: String, peer: MCPeerID, withCompletionHandler: ((NSError?) -> Void)? ){ self.session.sendResourceAtURL(resourceURL, withName: withName, toPeer: peer, withCompletionHandler: withCompletionHandler) } //デリゲート //NSURLからデータを受信開始した func session(session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, withProgress progress: NSProgress) {} //NSURLからのデータを受信終了した func session(session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, atURL localURL: NSURL, withError error: NSError?) {} |
カメラロールだとか,iCloud DriveだとかからNSURLを引っ張ってきてデータを送信する場合はここから.割と重いデータを扱う場合はこちらのほうがよさ気.プログレスも見れますし.
送信するときは,送りたいデータのNSURL,その名前(任意),送信先(一度に1台)を指定して送信を開始します.
受信開始時と受信終了時にはデリゲートが呼ばれます.受信開始時は名前と送信している端末のpeerIDしか見れませんが,受信が完了すると,NSURLが取得できるようになります.
ストリーミングによる送信
1 2 3 4 5 6 7 8 |
//ストリーミングで送信 func startStreamWithName( streamName : String , peer : MCPeerID) throws -> NSOutputStream{ try session.startStreamWithName( streamName, toPeer: peer) } //デリゲート //ストリーミングで受信 func session(session: MCSession, didReceiveStream stream: NSInputStream, withName streamName: String, fromPeer peerID: MCPeerID) {} |
正直このあたりは触ってないのであまりわかりません!とりあえず,ストリームの名前と相手を決めたらNSOutputStreamが帰ってくるから,それを使ってストリーミング通信すればいいのでは…
参考にさせていただいた記事とか
Multipeer Connectivity Framework Reference
Apple公式のリファレンスです.当然ながら内容は一番詳しいです.この一年のバイブル的存在.
第 3 回・iOSでMultipeerConnectivityを実装してみよう!
すごく分かりやすく網羅している日本語の記事です.Multipeer Connectivityが実装されたての2013年の記事でコードはObjective-Cで説明されていますが,とても分かりやすくまとめられています.図表がしっかりしててみやすいなぁ(しみじみ)
[iOS 7] P2P 通信を手軽に実現する Multipeer Connectivity Framework を使ってみる
こちらも日本語の記事です.これも2013年の記事ということでコードは上の記事と同様にObjective-Cです.
さいごに
一通り書いてみて内容が多くなったので,基本編と応用編に分けて記事にすることにしました.
基本となる端末同士の接続とデータの送受信を行えるようにする部分について説明しました.
全体的にかなり雑になってしまった感がありますね.後半は特に顕著な気がしています.その辺含めて,また日をあらためてこの記事は加筆修正する予定です.
それでは皆様,今度は試行錯誤のMultipeer Connectivity応用編でお会いしましょう!