Multipeer ConnectivityによるiPad間通信で早押しボタンを作った


概要

2チーム対抗クイズイベントのために、早押しボタンのiOSアプリを簡単に実装しました。
二台のiPadを用いて、各チームがiPadを早押しボタンとして使います。

iOS端末間のP2P通信を可能にするフレームワーク、Multipeer Connectivityを用いています。
回答ボタンを押した時の時間を端末間で通信し、正確なタイミング判定を実現しました。

画面に出ている数字はスコアカウンターです。

仕組み

Multipeer ConnectivityはBluetooth等を用いて通信しています。
通信のメッセージが届くのにラグがあるため、二つの起こりうるパターンが考えられます。
それぞれのパターンで先に押した方を判定するために、以下のような通信が行われます。

1. 一方がもう一方より圧倒的に早く押した場合

(以下の「勝ち」「負け」は、どちらが先に押したかの判定を意味します。)
早く押された方の端末をAとした場合、以下の図のようになります。
1. Aは、Bにボタンが押された時の時間を伝えます。
2. Bの方はボタンがまだ押されていないので、Bは負けを画面上に表示します。
3. しかし、この時点ではA端末側は自分が勝ったかどうかは分かりません。そのため、Bは負けを表示した後に、Aに「Bが負けたこと」を伝えます。
4. Aは、そのメッセージを受け取ると自身が勝ったことが分かり、画面上に勝ちを表示します。

2. 二チームのタイミングが僅差だった場合

Aのメッセージが発信されてから届くまでの間に、Bが押された場合は、以下のようになります。
1. Aが押されると、先ほどと同様にBに押された時の時間を送信します。
2. それをBが受け取る前にBも押され、Bが押された時の時間をAに送信します。
3. その後、BにAからの通信が届きます。Bは、自身が押された時間とAが押された時間を比較します。すると、Bは自身が負けたことが分かり、画面上に負けを表示します。
4. 同様に、AにBからの通信が届くと、時間を比較することで自身が勝ったことが分かり、画面上に勝ちを表示します。

実装

全体はGithubに置いておきます。
https://github.com/blu3mo/QuizButton
以下は、MCService.swiftの一部です。

早押しボタンが押された時の送信/受信まわりの実装は以下のようになっています。
メッセージはData型にエンコードしてやりとりしています。

MCService.swift
extension MCService: MCSessionDelegate {

    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
        // Debug用
        switch state {
        case .connected:
            print("connected: \(peerID.displayName)")
        case .connecting:
            print("connecting: \(peerID.displayName)")
        case .notConnected:
            print("not connected: \(peerID.displayName)")
            //self.state = .open
        }
    }

    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
        let decodedString = String(data: data, encoding: .utf8)

        var message: Message
        switch decodedString {
        case "youwin":
            message = .youWin
        default: //date
            let date = dateFromString(string: decodedString!, format: dateStringFormat)
            message = .triedDate(date)
        }

        model.reactMessage(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, at localURL: URL?, withError error: Error?) {

    }

}

extension MCService: JudgeModelConnectionOutput {

    var isConnected: Bool {
        get {
            return (mcSession.connectedPeers.count == 0)
        }
    }

    func sendMessage(message: Message) {
        var sendingString: String = ""

        switch message {
        case .youWin:
            sendingString = "youwin"
        case .triedDate(let date):
            sendingString = stringFromDate(date: date, format: dateStringFormat)
        }

        do {
            try mcSession.send(sendingString.data(using: .utf8)!, toPeers: mcSession.connectedPeers, with: .reliable)
        } catch {
            print("sending error") //TODO
        }
    }

    func advertise() {
        let mcAdvertiserAssistant = MCAdvertiserAssistant(serviceType: serviceTypeId, discoveryInfo: nil, session: self.mcSession)
        mcAdvertiserAssistant.start()
    }

    func getBrowser() -> UIViewController {
        let mcBrowser = MCBrowserViewController(serviceType: serviceTypeId, session: mcSession)
        mcBrowser.delegate = self
        mcBrowser.maximumNumberOfPeers = 1
        mcBrowser.minimumNumberOfPeers = 1

        return mcBrowser
    }

}

詳しい仕様については、Multipeer Connectivityの公式Documentationを確認してください。

運用

実際のイベントでは2台のiPadを観客に向けて設置し、マウスをそれぞれに接続しました。
(最近iPadでもマウスが使えるようになりました)
回答者がマウスをクリックすると、ボタンが押された判定となります。

備考

・Qiita初投稿&素人なので、改善点等あったら教えてください🙏
・今回は二端末間の通信となっていますが、接続数の制限を解除すれば同
じ仕組みで三台以上も問題ないはずです。
・Multipeer Connectivityについては、不安定であるという意見も存在します。
 MultipeerConnectivityって使えるの? by @YearCentury

参考

iOS Swift Tutorial: Transfer Data with the Multipeer Connectivity Framework
https://www.youtube.com/watch?v=H5c4vo6p5Fg

ActionSheet Popover on iPad in Swift
https://medium.com/@nickmeehan/actionsheet-popover-on-ipad-in-swift-5768dfa82094