サーバーなしでデバイスだけで直接で通信できます。
#ピア通信に必要な最小手順
この手順で他のデバイスとデータの送受信ができます。
###1、Local Network (Bonjour services)をアプリに追加
Info.plistにLocal Network Usage DescriprtionとBonjour servicesを追加します。
Info.plist のBonjour services に自分のサービス名をStringで記述します。
このサービス名が、そのアプリのやりとりの識別子になります。
Bonjourサービス名はASCII小文字、数字、およびハイフン15文字以内である必要があります。
仮に、"my-mc-service"とします。
<key>NSBonjourServices</key>
<array>
<string>_my-mc-service._tcp</string> // サービス名とtcpの前に_(アンダーバー)を入れます
</array>
Bonjour は Apple が開発したデバイス通信プロトコル。
ローカルエリアネットワーク上のデバイスを設定いらずで発見してこんにちは(ボンジュール)できる。通信には、Wi-Fi、peer-to-peer Wi-Fi、Bluetoothをつかえる。
###2、通信セッション、サービスへの参加、ピアの検索を開始
MCSession、MCNearbyServiceAdvertiser、MCNearbyServiceBrowser を初期化して開始します。
import MultipeerConnectivity // フレームワークをインポート
// 必要なプロパティ
static let serviceType = "my-mc-service" // Bonjour service の名前
let myPeerID = MCPeerID(displayName: UIDevice.current.name) // 自分のピアID
var session: MCSession!
var serviceAdvertiser: MCNearbyServiceAdvertiser!
var serviceBrowser: MCNearbyServiceBrowser!
var connectedPeers: [MCPeerID] {
return session.connectedPeers
} // セッションで通信している他のピアのID
// プロパティの初期
// 通信への参加、ピアの検索を開始
session = MCSession(peer: myPeerID, securityIdentity: nil, encryptionPreference: .required)
session.delegate = self
serviceAdvertiser = MCNearbyServiceAdvertiser(peer: myPeerID, discoveryInfo: nil, serviceType: MultipeerSession.serviceType)
serviceAdvertiser.delegate = self
serviceAdvertiser.startAdvertisingPeer() // サービスへの参加意思を周りに伝えることを開始
serviceBrowser = MCNearbyServiceBrowser(peer: myPeerID, serviceType: MultipeerSession.serviceType)
serviceBrowser.delegate = self
serviceBrowser.startBrowsingForPeers() // 近くのピアの検索を開始
それぞれの役割:
MCSession:
(送受信)通信を管理する
MCNearbyServiceAdvertiser
(参加)このデバイスは、このサービスに参加する意思があるよ、と近くのピアに伝える
MCNearbyServiceBrowser
(検索)近くのピアを検索する
###3、他のピアの発見、招待、データの送受信を行う
Delegate メソッドを追加して処理を行います。
MCSession、MCNearbyServiceAdvertiser、MCNearbyServiceBrowser それぞれDelegate メソッドがあります。
// MCSessionDelegate
func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
// 近くのピアの状態が変化したとき呼ばれる
}
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
// Data を受信したとき呼ばれる
}
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
// バイトストリームを受信しはじめたとき呼ばれる
}
func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
// リソース(ファイルURLもしくはHTTP URL から送信される)の受信を開始したとき呼ばれる
}
func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
// リソースの受信を完了したとき呼ばれる
}
// MCNearbyServiceBrowserDelegate
public func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) {
// 近くのピアを発見したとき呼ばれる
// 新しいピアを招待する
browser.invitePeer(peerID, to: session, withContext: nil, timeout: 10)
// withContext:Data で招待に関する情報を送れる
// timeout:TimeInterval 招待したピアが参加するのを待つ秒数
print(peerID)
}
public func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
// 近くのピアが失われたとき呼ばれる
}
//MCNearbyServiceAdvertiserDelegate
/// 招待を受ける
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
// 他のピアに招待された時に呼ばれる
invitationHandler(true, self.session) // sessionにて招待を受ける(True)
print(peerID)
messageLabel.text = "Connected\nwith \(peerID.displayName)"
}
<🐥ここまでの手順で、近くのピアと通信接続ができる>
接続したピアのDisplayNameを取得できる。
MCPeerID: 0x2823ac7a0 DisplayName = **のiPhone
###4、データの送受信
送信
通信している全てのピアに送る場合は以下。
特定のピアに送るには、toPeersを特定のピアIDにすればいい。
func sendToAllPeers(_ data: Data) {
do {
try session.send(data, toPeers: session.connectedPeers, with: .reliable) // reliable(待ち行列化や再送信でデータの配信を保証する) unreliable(データの配信を保証せず、ドロップの可能性がある)
} catch {
print("error sending data to peers: \(error.localizedDescription)")
}
}
例として文字列を送ってみましょう。
let bonjourString = "bonjour!!"
guard let stringData = bonjourString.data(using: .ascii) else {return}
sendToAllPeers(stringData)
受信
デリゲートメソッド内で処理する。
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
guard let string = String(data: data, encoding: .ascii) else {return}
print(string)
DispatchQueue.main.async {
self.messageLabel.text = "\(string)\n from \(peerID.displayName)"
}
}
"bonjour!!"
動画下側がiPhoneの画面、上が上がiPodです。(iPhoneの画面上半分でiPodを撮って、下半分でconnect状態を映してスクリーンキャプチャ。。。別のカメラでデバイス二つ撮影すると綺麗に映らないから、苦肉の策です。あしからず)
<🐥ここまででデバイス間のデータの送受信ができる>
#デバイスふってプロフィールを共有するやつ
###1、MultipeerConnectivityを起動しておく
上記手順で接続までしておきます。
###2、CoreMotionで自分のデバイスを振っているかを確認する
import CoreMotion
let motionManager = CMMotionManager()
var capturedYaw:Double?
var shakeCount:Int = 0
func getShaking(){
capturedYaw = nil
guard motionManager.isDeviceMotionAvailable else { return }
motionManager.deviceMotionUpdateInterval = 1 / 100
// デバイスの動きの取得を開始
motionManager.startDeviceMotionUpdates(to: OperationQueue.current!, withHandler: { [unowned self] (motion, error) in
guard let motion = motion, error == nil else { return }
let yaw = motion.attitude.yaw // デバイスのz軸の回転角度を取得
if let recentCapturedYaw = capturedYaw {
if abs(recentCapturedYaw - yaw) > 1 { // もしz軸の回転が60度を超えたら、shakeCountを1プラス
shakeCount += 1
capturedYaw = yaw
}
}else {
capturedYaw = yaw
}
})
}
Timerで自分のデバイスをいま振っているか(shakeCountが増えているか)をチェックします。
var timer:Timer?
var shaking = false
func checkShakingAndSendData() {
timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: true, block: { [unowned self] Timer in
if shakeCount > 6 {
shakeCount = 0
shaking = true
} else {
shaking = false
}
// 自分が振っているか確認したあと、他のピアの状態を確認して、データを送信します。後で説明します。
checkMatching()
sendDataByStates()
})
}
###3、文字列で自分が振っているかどうかを相手に知らせる
MultipeerConnectivity をつかって、通信中のデバイスに、文字列で自分の状態をしらせます。
func sendShakingState(peerID:MCPeerID, shaking:Bool){
var shakingString = "notShaking"
if shaking {
shakingString = "shaking"
}
guard let stringData = shakingString.data(using: .ascii) else {return}
do {
try session.send(stringData, toPeers: [peerID], with: .reliable)
}catch let error{
print(error)
}
}
###4、他のピアと自分のデバイスを確認して、両方振っていたらマッチ状態にする
MCSessionのデリゲートメソッドで受信した他のピアの状態を更新します。
// 他のピアの状態
enum PeerState {
case notShaking
case shaking
case matched
case sent
}
var peersStates:[MCPeerID:PeerState] = [:]
// MCSessionのデリゲートメソッド内でデータを受信する
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
if let string = String(data: data, encoding: .ascii) {
switch string {
case "shaking":
peersStates[peerID] = .shaking // "shaking" を受信したら、送信元のピアIDにつけたstateを.shakingにアップデート
case "notShaking":
peersStates[peerID] = .notShaking // "notShaking" を受信したら、送信元のピアIDにつけたstateを.notShakingにアップデート
default:
break
}
}
}
func checkMatching(){
guard shaking else { return }
for (peerID, peerState) in peersStates {
if peerState == .shaking {
peersStates[peerID] = .matched // 自分も他のピアも振っている状態であれば、マッチ状態にする
}
}
}
###5、他のピアの状態に応じてデータを送信する
func sendDataByStates(){
for (peerID,peerState) in peersStates {
switch peerState {
case .notShaking,.shaking:
sendShakingState(peerID: peerID, shaking: shaking) // まだマッチしていない状態の時は、自分のデバイスを振っているかどうかを送る
case .matched:
sendProfile(peerID: peerID) // マッチ状態の時は、プロフィールを送る
case .sent:
break // すでにプロフィールを送っている場合は、何もしない
}
}
}
###6、プロフィールを送る
プロフィールとして送るのは
・アイコン画像
・ユーザー名
・ID
です。
実際はIDやトークンだけ交換して、サーバーを介してユーザー情報を送るのだと思いますが、ここでは直接相手のデバイスに送ってみます。
画像は、Dataタイプにすると送れます。例えば、UIImageをDataにして送るには
let imageData = uiImage.jpegData(compressionQuality: 1)
これで画像と文字列をそれぞれ送ってもいいのですが、MultipeerConnectivityはCodableに準拠してJsonにエンコードしたデータも送れるので、それでUser情報を一度に送ってみます。
struct User: Codable { // Jsonにエンコード・Jsonからデコード可能な User struct
let userImage:Data
let userName:String
let userID:String
}
let user = User(userImage:imageData,
userName:"Daisuke",
userID:"elephant")
func sendProfile(peerID:MCPeerID){
var error:Error?
let encoder = JSONEncoder()
do {
let myUserData = try encoder.encode(user)
try session.send(myUserData, toPeers: [peerID], with: .reliable)
} catch let err {
print(err)
error = err
}
if error == nil { // errorがなければ送れているとみなして対象のピアの状態を「送信ずみ」にする
peersStates[peerID] = .sent
}
}
###7、プロフィールを受信する
受信したデータでUIを更新します。
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
let decoder = JSONDecoder()
if let receivedUser = try? decoder.decode(User.self, from: data) {
var userImage:UIImage
if let uiImage = UIImage(data: receivedUser.userImage) {
userImage = uiImage
} else {
userImage = defaultImage
}
DispatchQueue.main.async {
self.profileImageView.image = uiImage
self.userNameLabel.text = receivedUser.userName
}
// 未送信であれば、こちらからもユーザーデータを送ります。
if peersStates[peerID] != .sent {
sendProfile(peerID: peerID)
}
}
}
🐣
フリーランスエンジニアです。
お仕事のご相談こちらまで
rockyshikoku@gmail.com
Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。