10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

スマホを振って、ピア通信で友だちとプロフィールを交換する【MultipeerConnectivity】【iOS】

Last updated at Posted at 2021-11-11

サーバーなしでデバイスだけで直接で通信できます。

#ピア通信に必要な最小手順

この手順で他のデバイスとデータの送受信ができます。

###1、Local Network (Bonjour services)をアプリに追加
Info.plistにLocal Network Usage DescriprtionとBonjour servicesを追加します。

スクリーンショット 2021-05-27 4.04.33.png

Info.plist のBonjour services に自分のサービス名をStringで記述します。
このサービス名が、そのアプリのやりとりの識別子になります。
Bonjourサービス名はASCII小文字、数字、およびハイフン15文字以内である必要があります。
仮に、"my-mc-service"とします。

Info.plist
<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状態を映してスクリーンキャプチャ。。。別のカメラでデバイス二つ撮影すると綺麗に映らないから、苦肉の策です。あしからず)

Jun-04-2021 06-00-26.gif

<🐥ここまででデバイス間のデータの送受信ができる>

#デバイスふってプロフィールを共有するやつ

###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を使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
Medium

10
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?