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

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

<img src="https://user-images.githubusercontent.com/23278992/141350370-7c50cd9c-c59e-4f50-8dc2-514b89354db0.gif" width="200">

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

###

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

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

![スクリーンショット 2021-05-27 4.04.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/6102bd5b-c67d-df37-c7db-e2481808d4f0.png)

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

```swift: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 を初期化して開始します。

```swift
import MultipeerConnectivity // フレームワークをインポート
```

```swift
// 必要なプロパティ
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
```

```swift
// プロパティの初期
// 通信への参加、ピアの検索を開始
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**
（検索）近くのピアを検索する

###３、他のピアの発見、招待、データの送受信を行う
Delegate メソッドを追加して処理を行います。
MCSession、MCNearbyServiceAdvertiser、MCNearbyServiceBrowser それぞれDelegate メソッドがあります。

```swift
// 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

###４、データの送受信
**送信**
通信している全てのピアに送る場合は以下。
特定のピアに送るには、toPeersを特定のピアIDにすればいい。

```swift
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)")
    }
}
```

例として文字列を送ってみましょう。

```swift
let bonjourString = "bonjour!!"
guard let stringData = bonjourString.data(using: .ascii) else {return}
sendToAllPeers(stringData)
```

**受信**
デリゲートメソッド内で処理する。

```swift
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/6fd1408a-ab53-e539-48e1-2174a244ca43.gif)


**＜🐥ここまででデバイス間のデータの送受信ができる＞**

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

###１、MultipeerConnectivityを起動しておく
上記手順で接続までしておきます。

###２、CoreMotionで自分のデバイスを振っているかを確認する

```swift
import CoreMotion
```

```swift
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を１プラス
                shakeCount += 1
                capturedYaw = yaw
            }
        }else {
            capturedYaw = yaw
        }
    })
}
```

Timerで自分のデバイスをいま振っているか（shakeCountが増えているか）をチェックします。

```swift
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()
    })
}
```

###３、文字列で自分が振っているかどうかを相手に知らせる

MultipeerConnectivity をつかって、通信中のデバイスに、文字列で自分の状態をしらせます。

```swift
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)
    }
}
```

###４、他のピアと自分のデバイスを確認して、両方振っていたらマッチ状態にする

MCSessionのデリゲートメソッドで受信した他のピアの状態を更新します。

```swift
// 他のピアの状態

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 // 自分も他のピアも振っている状態であれば、マッチ状態にする
        }
    }
}
```

###５、他のピアの状態に応じてデータを送信する

```swift
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 // すでにプロフィールを送っている場合は、何もしない
        }
    }
}
```

###６、プロフィールを送る
プロフィールとして送るのは
・アイコン画像
・ユーザー名
・ID
です。
実際はIDやトークンだけ交換して、サーバーを介してユーザー情報を送るのだと思いますが、ここでは直接相手のデバイスに送ってみます。
**画像は、Dataタイプにすると送れます。例えば、UIImageをDataにして送るには**

```swift
let imageData = uiImage.jpegData(compressionQuality: 1)
```
これで画像と文字列をそれぞれ送ってもいいのですが、MultipeerConnectivityはCodableに準拠してJsonにエンコードしたデータも送れるので、それでUser情報を一度に送ってみます。

```swift
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
    }
}
```

###７、プロフィールを受信する
受信したデータでUIを更新します。

```swift
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)
        }    
    }
}
```

<img src="https://user-images.githubusercontent.com/23278992/141350370-7c50cd9c-c59e-4f50-8dc2-514b89354db0.gif" width="200">


🐣
****
フリーランスエンジニアです。
お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

[Twitter](https://twitter.com/JackdeS11)
[Medium](https://medium.com/@rockyshikoku)