Multipeer Connectivityを使用して共有できるメモアプリ作成してみた

## Multipeer Connectivityとは

iOS、iPadOS、および macOS で、近くにいるデバイス間で簡単にデータを共有したり、通信を行ったりするためのフレームワークです。Bluetooth、Wi-Fi、インターネット経由などの複数の接続方法を使用して、デバイス同士が直接通信できます。特に、同じネットワーク内や物理的に近い場所にいる複数のデバイス間で、セッションを作成してデータを送受信することができます。

https://developer.apple.com/documentation/multipeerconnectivity

イメージとしては下のような画像になります。従来であればサーバーを介する必要がありますが、デバイス同士で通信が可能になります。
![P2P.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796389/ff970a14-308a-e5c1-798c-12d1239c7680.png)

## ユースケース

インターネット接続ができない場所、または不安定な場所で他の端末とデータの共有を行いたいシチュエーションが考えられます。またそのような場所でなくとも、単に他の端末とデータの共有を行いたい場合にも有効であると考えています。

・地下や海上、上空など
・災害時など
・セキュリティやプライバシーの観点から、インターネット接続が制限される場所

## 実装を雰囲気で掴む
実装について簡単に紹介します。実装についてはさまざまな記事やサンプルがあるので、詳しくはそちらを参考にしてください。今回は学習のためコメントを追加したものになります。

### 通信するまで
端末同士の通信を確立するまでの実装です。Data型をやりとりすることになります。

```swift:MultiPeerSession.swift
import MultipeerConnectivity

class MultiPeerSession {
  // 1. MCPeerIDの作成
  let peerID = MCPeerID(displayName: UIDevice.current.name)
  // 2. MCSessionの作成
  let session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .required)
  session.delegate = self
  // 3. アドバタイズ（自分を他のデバイスに表示。）
  let advertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: "example-service")
  advertiser.delegate = self
  advertiser.startAdvertisingPeer()
  // 4. 他のデバイスを探す（アドバタイズしているデバイスを探す。）
  let browser = MCNearbyServiceBrowser(peer: peerID, serviceType: "example-service")
  browser.delegate = self
  browser.startBrowsingForPeers()
}

//　MCSessionDelegateの実装
extension MultiPeerSession: MCSessionDelegate {
    // 接続状態が変わったときに呼ばれる
    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
        switch state {
        case .connected:
            print("\(peerID.displayName) connected")
        case .notConnected:
            print("\(peerID.displayName) disconnected")
        case .connecting:
            print("\(peerID.displayName) connecting...")
        @unknown default:
            break
        }
    }
    // データを受信したときに呼ばれる
    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
        // 受信したデータの処理
        guard let message = String(data: data, encoding: .utf8) else {
            print("メッセージ取得失敗:(")
            return
        }
        print("Received message: \(message)")
    }
    // その他のデリゲートメソッド
    // バイトストリーム？開始時に呼ばれる
    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) {}
    // データの受信を終了した時に呼ばれる
    func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, atURL localURL: URL?, error: Error?) {}
}

// MCNearbyServiceBrowserDelegateの実装
extension MultiPeerSession: MCNearbyServiceBrowserDelegate {
    // 他のデバイスを探すがエラーにより開始できなかった時に呼ばれる
    func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error) {
    }
    // ブラウズしている他端末を見つけた時に呼ばれる。
    func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) {
    }
    //　他端末と接続が切れた時に呼ばれる
    func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
    }
}
extension MultiPeerSession: MCNearbyServiceAdvertiserDelegate {
    // アドバタイズ（自分を他のデバイスに表示。）がエラーにより開始できなかった時に呼ばれる。
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error) {
    }
    // 「他のデバイスを探す」をしている他端末から招待が届いた時に呼ばれる
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        self.alertMessage = "\(peerID) wants to Connect :)"
        // trueで通信が開始される。
        invitationHandler(true, session)
    }
}

```

### Data型を送信する
例えば文字列のデータを送信したい場合は、以下のようになります。
```swift
let session = MultiPeerSession()
let messageData = Data("This is message".utf8)
session.send(messageData)
```

### 通信を終了する
通信を終了する処理です。自己流なので間違いがあるかもしれません。
```swift
func disConnect(){
    // 他の端末への接続を切断
    for peer in session.connectedPeers {
      session.cancelConnectPeer(peer)
    }
    // セッションを切断。
    session.disconnect()
    // アドバタイズ（自分を他のデバイスに表示。）を終了する。
    advertiser.stopAdvertisingPeer()
    // 他のデバイスを探す（アドバタイズしているデバイスを探す。）を終了する。
    browser.stopBrowsingForPeers()
}

```

## 今回作成したメモアプリ
今回Multipeer Connectivityを使用して共有できるメモアプリを作成してみました。
シンプルにチャットアプリとかでもよかったのですが、下記3点から共有できるメモアプリを作成してみました。

・試しにPencilKitを使用したメモアプリを作成していたこと
・作成していたメモアプリはStoryBoardを使用していたので、SwiftUIに書き換えたかったこと
・Multipeer ConnectivityはData型でやり取りするので、試しに作成していたメモアプリでも共有できると考えたこと


### PencilKitについて
Appleが提供するフレームワークで、iOSとiPadOSのアプリケーションに手書きのメモや図形を描く機能を簡単に追加できます。標準アプリのメモアプリで使用できる機能をアプリに組み込めるというイメージです。

PencilKitについて詳細は割愛します。

https://developer.apple.com/documentation/pencilkit

### 作成したメモアプリの動作
今回はシミュレーターを2つ並べて、作成した共有できるメモアプリの動作を確認しました。
描画ツールの操作が終了したタイミングで描画データを送信しています。今回はシミュレーター同士での確認になるため同一ネットワークでの確認になります。

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796389/7e28b130-9175-85c4-17e2-3eb1d3116c00.gif)


## まとめ
今回はMultipeer Connectivityを使用して、共有できるメモアプリ作成してみました。
Multipeer Connectivityを使用することで、P2Pで他の端末と通信することが可能になります。
作成した共有できるメモアプリはシミュレーター上で動作を確認しましたが、実際の端末での動作は確認出来ていないので、実際に複数の端末で動作を確認したいと考えています。

## 参考記事
https://www.slideshare.net/slideshow/multipeer-connectivity-2/250232687

https://www.ralfebert.com/ios-app-development/multipeer-connectivity/