MultipeerConnectivityでiOSの端末間通信（P2P通信）を実装する方法

#目次
# [はじめに](#1-はじめに)
端末間通信(P2P)をするアプリを開発する際にめちゃくちゃ苦労したので、今後はこの機能をスムーズに開発するために概要から実際のサンプルコードをここに残します。
この記事を見ればMultipeerConnectivityライブラリを使っての端末間通信のアプリを開発できるようになると思います。
この記事では近距離通信の機能を実装していきます

# [この記事の対象者](#2-この記事の対象者)
・BluetoothやWi-Fiを使って端末間の通信アプリを開発したい方
・MultipeerConnectivityを使って何らかの通信アプリを開発したい方
・MultipeerConnectivityのサンプルコードを使いたい方
# [接続手順](#3-接続手順)
１、端末間通信をサポートするMultipeerConnectivityライブラリを導入
２、[MCPeerID](https://developer.apple.com/documentation/multipeerconnectivity/mcpeerid)を発行(端末を識別する一意のもの)
３、[MCSession](https://developer.apple.com/documentation/multipeerconnectivity/mcsession)を生成
４、接続時の招待(端末検索)側と受ける側の初期設定
招待(端末検索)する側　
 [MCNearbyServiceBrowser](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyservicebrowser)もしくは[MCBrowserViewController](https://developer.apple.com/documentation/multipeerconnectivity/mcbrowserviewcontroller)で接続できる端末を探す。
招待を受ける側　
[MCNearbyServiceAdvertiser](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyserviceadvertiser)もしくは[MCAdvertiserAssistant](https://developer.apple.com/documentation/multipeerconnectivity/mcadvertiserassistant)でアドバタイズする。
他の端末から探知できるようにする
５、招待(端末検索)側で探知したPeerに対して招待
６、招待された側で許可すると接続が確立
７、MCSessionを使ってデータの送受信
# [サンプルコードを用いて順に解説](#4-サンプルコードを用いて順に解説)
## １、MultipeerConnectivityライブラリ導入
MultipeerConnectivityはiOS標準ライブラリになるので宣言するだけで導入できる
```swift
import MultipeerConnectivity 
```
### infoの設定
①Bonjour services
・info.plistのKeyに『Bonjour services』を追加
・その下のitemを２つ追加
・そのitemのValueに『_SampleApp._tcp』と『_SampleApp._udp』と記述
②App Transport Security Settings
・info.plistのKeyに『App Transport Security Settings』を追加
・その下に『Allow Arbitrary Loads』を追加
・そのValueをYesに変更
③Privacy - Bluetooth Always Usage Description
・アプリ名のターゲットの『info』タブをタップ
・Custom iOS Target PropertiesのKeyに『Privacy - Bluetooth Always Usage Description』を追加
・そのValueに端末間通信を使用する理由を記述

### 招待(端末検索)する側
作成したクラスにMCNearbyServiceBrowserDelegateとMCSessionDelegateを継承させる
```swift
class SelectConnectViewController: UIViewController, MCNearbyServiceBrowserDelegate,MCSessionDelegate{
```
### 招待される側
作成したクラスにMCNearbyServiceAdvertiserDelegateとMCSessionDelegateを継承させる
```swift
class SelectConnectViewController: UIViewController, MCNearbyServiceAdvertiserDelegate,MCSessionDelegate{
```
## ２、MCPeerIDと３、MCSessionの生成
招待(端末検索)する側も招待される側も[MCSession](https://developer.apple.com/documentation/multipeerconnectivity/mcsession)を発行し、[MCSession](https://developer.apple.com/documentation/multipeerconnectivity/mcsession)を生成する
```swift
//Sessionの生成
var session : MCSession!

override func viewDidLoad() {
        super.viewDidLoad()
        //MCPeerID発行
        self.peerID = MCPeerID(displayName: UIDevice.current.name)
}
```
## ４、接続時の招待側と受ける側の初期設定
### 招待（端末検索）する側のクラス
①MCNearbyServiceBrowserやMCBrowserViewControllerを生成する
②infoで追加した『_SampleApp._tcp』と『_SampleApp._udp』のSampleAppをserviceTypeに代入
③そのDelegate宣言をしてそのクラスを使えるようにする
④各Delegate メソッドを追加する（自動で生成される）
```swift
class InviteViewController: UIViewController, MCNearbyServiceBrowserDelegate,MCSessionDelegate{
    //①MCNearbyServiceBrowserやMCBrowserViewControllerを生成する
    var browser : MCBrowserViewController!
    var nearbyBrowser : MCNearbyServiceBrowser!
    var session : MCSession!
    var peerID: MCPeerID!
    serviceType = "SampleApp"
override func viewDidLoad() {
        super.viewDidLoad()
        //②そのDelegate宣言をしてそのクラスを使えるようにする
        //nearbyBrowserを初期化
        self.nearbyBrowser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType)
        nearbyBrowser.delegate = self
}
//③この下に各Delegate メソッドを追加する（自動で生成される）

```
### 招待される側のクラス
①MCNearbyServiceAdvertiserDelegateを生成する
②infoで追加した『_SampleApp._tcp』と『_SampleApp._udp』のSampleAppをserviceTypeに代入
③そのDelegate宣言をしてそのクラスを使えるようにする
④各Delegate メソッドを追加する（自動で生成される）
```swift
class InvitedViewController: UIViewController, MCNearbyServiceAdvertiserDelegate,MCSessionDelegate{
    //① MCNearbyServiceAdvertiserDelegateを生成する
    var advertiser : MCNearbyServiceAdvertiser!
    var session : MCSession!
    var peerID: MCPeerID!
    serviceType = "SampleApp"
override func viewDidLoad() {
        super.viewDidLoad()
        //②そのDelegate宣言をしてそのクラスを使えるようにする
        advertiser.delegate = self
        //advertiserを初期化
        advertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
        
}
//③この下に各Delegate メソッドを追加する（自動で生成される）
```
## ５、招待側で探知したPeerIDに対して招待
### 招待する側のクラス
#### 端末検索開始
MCBrowserViewControllerやMCNearbyServiceBrowserで検索した端末を表示し選択する
＊自分のPeerIDも検出してしまうので条件文分岐などできちんと区別しましょう
```swift
 //端末を検索開始
 nearbyBrowser.startBrowsingForPeers()
```
### 招待される側のクラス
#### アドバタイズ開始
自分のPeerIDを近くの端末に発信する
```swift
 //アドバタイズ開始
  self.advertiser.startAdvertisingPeer()
 //アドバタイズを終了する場合
 //self.advertiser.stopAdvertisingPeer()
```
## ６、招待された側で許可すると接続が確立
### 招待される側のクラス
MCNearbyServiceAdvertiserのデリゲートメソッドで招待を受けた時のアラートを設定
```swift
//接続先の端末を選択した時の処理
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        let alert = UIAlertController(title: "接続を許可しますか？", message: "許可すると通信が可能になります", preferredStyle: .alert)
        
        let delete = UIAlertAction(title: "許可", style: .default, handler: { (action) -> Void in
            //招待を受諾の処理
            invitationHandler(true, self.session)
            print("許可")
        })
        
        let cancel = UIAlertAction(title: "拒否", style: .cancel, handler: { (action) -> Void in
            invitationHandler(false, self.session)
            print("拒否")
        })
        
        alert.addAction(delete)
        alert.addAction(cancel)
        
        self.present(alert, animated: true, completion: nil)
    }
```
## ７、MCSessionを使ってデータの送受信

### データの送信
#### ①DataCoderクラスを作成（エンコード、デコード）
```swift
import Foundation

class DataCoder: NSObject, NSCoding {
    
    var canvas : Data!              // キャンバスデータ
    var drawData : [Data?]!         // 描画情報(配列)
    var lessonImageData : [Data]!   // 画像(配列)
    var studentID: Int!             // 生徒ID
    init(canvas : Data, seigyoNum : Int, drawData : [Data?], lessonImageData : [Data], studentID : Int ) {
        self.canvas = canvas
        self.drawData = drawData
        self.lessonImageData = lessonImageData
        self.studentID = studentID
    }
    
    // デコード
    required init?(coder aDecoder: NSCoder) {
        self.canvas = aDecoder.decodeObject(forKey: "data") as! Data
        self.drawData = NSKeyedUnarchiver.unarchiveObject(with: draw) as? [Data?]
        self.lessonImageData = NSKeyedUnarchiver.unarchiveObject(with: lessonData) as? [Data]
        self.studentID = aDecoder.decodeObject(forKey: "studentID") as! 
        
    }
    
    // エンコード
    func encode(with aCoder: NSCoder) {
        aCoder.encode(self.canvas, forKey: "data")
        let data1 = NSKeyedArchiver.archivedData(withRootObject: self.drawData)
        aCoder.encode(data1, forKey: "drawData"
        let data2 = NSKeyedArchiver.archivedData(withRootObject: self.lessonImageData)
        aCoder.encode(data2, forKey: "lessonImageData")
        aCoder.encode(self.studentID, forKey: "studentID")
    }
    
}

```

#### ②送信したいデータをエンコード（画像、描画情報、音声など）
画像や描画情報なども一度データ型に変換してからNSDataにエンコードすれば送信できます

```swift
var data : DataCoder!= DataCoder(canvas: canvas.drawing.dataRepresentation(), drawData: [],lessonImageData: [], studentID: studentID)

// エンコードされたデータをencDataに代入
let encData = NSKeyedArchiver.archivedData(withRootObject: data) // CommonData→NSData
```
#### ③MCSessionのsendメソッドで送信
```swift
do {
//エンコードされたデータをconnectedStudentPeerIDに送信
      try session!.send(encData as Data, toPeers: [connectedStudentPeerID], with: MCSessionSendDataMode.unreliable)
} catch {
      print(error)
}
```
### データの受信
#### ①接続先からデータ受信時に呼ばれるメソッド
```Swift
 // 相手からNSDataが送られてきたとき
   func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID)  {
}
```
#### ②受信データをデコード（非同期）
```swift
 DispatchQueue.main.async { [self] in
      // デコード
      let decode = NSKeyedUnarchiver.unarchiveObject(with: data) as! DataCoder
      // studentID
      var studentID : Int= decode.studentID
      // 画像配列
      var imageFataArray : [Data]= decode.lessonImageData
      // drawData
      drawDataArray : [Data]= decode. drawData
}
```

### MCBrowserViewController
端末検索する際に簡易的な検索画面を表示してくれる便利なViewcontroller。
あまりカスタムはできないが、特に何か処理をかく必要はない

```swift
// 端末検索で発見した端末を表示するかどうかを設定できる（optional）
func browserViewController(_ browserViewController: MCBrowserViewController, shouldPresentNearbyPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) -> Bool
}
// Doneボタン押下時に呼ばれる（必須）
func browserViewControllerDidFinish(_ browserViewController: MCBrowserViewController)
}

// Cancelボタン押下時に呼ばれる（必須）
func browserViewControllerWasCancelled(_ browserViewController: MCBrowserViewController)
}
```

### MCNearbyServiceBrowser
端末検索(近くにいた人と接続)する際に独自のUIでその画面を表示したい時に使う
＊簡易的に実装するなら、MCBrowserViewControllerの方が便利

```swift
//MCNearbyServiceBrowserのデリゲート
//端末検索でpeerIDを発見した時の処理(このpeerIDは条件分岐等で使える)
func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
        print("foundPeer")
}
//端末検索や接続時にpeerIDが消えた時の処理
func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
        print("lostPeer")
}
// 検索開始失敗に呼ばれる（optional）
func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error)
　　　　　print("エラー")
}
```


```swift
# デリゲートクラスの詳細
//sessionのデリゲートでtrueがデフォルト
func session(_ session: MCSession, didReceiveCertificate certificate: [Any]?, fromPeer peerID: MCPeerID, certificateHandler: @escaping (Bool) -> Void) {
         certificateHandler(true)
}
//sessionのデリゲート
//ファイルの受信終了時の処理
func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
        print("")
}
//sessionのデリゲート
//ファイルの受信開始時の処理
func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
        print("")
}
//sessionのデリゲート
//接続等を検知する処理
func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
        switch state {
           // 接続完了
        case .connected:
            print("\(peerID.displayName)と接続完了")
            // 接続中
        case .connecting:
            print("\(peerID.displayName)と接続中")
            
            // 接続していない
        case .notConnected:
            print("\(peerID.displayName)と切断")
            
        @unknown default:
            print("その他")
        }
}
//sessionのデリゲート
//データの受信時（data: Dataのdataが受信したデータ）
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
        print("")
}
// バイトストリーム接続でのデータ受信処理
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
        print("")
}
}
```


# [おわりに](#5-おわりに)
今回初めての技術記事の投稿となります。
初めて実務で端末間通信を実装することになり過去の素晴らしい記事を何個見て、公式も見て
やっと実装できたので、皆さんの参考になればと思い投稿しました。
今後はiOSアプリについて順次投稿していきますのでよろしくお願いします。
最後まで見て頂きありがとうございました。

参考にした記事
・[第 3 回・iOSでMultipeerConnectivityを実装してみよう！](https://gaprot.jp/2013/11/27/multipeer-connectivity/)
・[Multipeer ConnectivityでのP2P通信（Swift）](https://qiita.com/am10/items/e56c2bc6eaab75bc9c8c)