概要
2チーム対抗クイズイベントのために、早押しボタンのiOSアプリを簡単に実装しました。
二台のiPadを用いて、各チームがiPadを早押しボタンとして使います。
iOS端末間のP2P通信を可能にするフレームワーク、Multipeer Connectivityを用いています。
回答ボタンを押した時の時間を端末間で通信し、正確なタイミング判定を実現しました。
画面に出ている数字はスコアカウンターです。
— Bluemo (@blu3mo) March 2, 2020
仕組み
Multipeer ConnectivityはBluetooth等を用いて通信しています。
通信のメッセージが届くのにラグがあるため、二つの起こりうるパターンが考えられます。
それぞれのパターンで先に押した方を判定するために、以下のような通信が行われます。
1. 一方がもう一方より圧倒的に早く押した場合
(以下の「勝ち」「負け」は、どちらが先に押したかの判定を意味します。)
早く押された方の端末をAとした場合、以下の図のようになります。
- Aは、Bにボタンが押された時の時間を伝えます。
 - Bの方はボタンがまだ押されていないので、Bは負けを画面上に表示します。
 - しかし、この時点ではA端末側は自分が勝ったかどうかは分かりません。そのため、Bは負けを表示した後に、Aに「Bが負けたこと」を伝えます。
 - Aは、そのメッセージを受け取ると自身が勝ったことが分かり、画面上に勝ちを表示します。
 
2. 二チームのタイミングが僅差だった場合
Aのメッセージが発信されてから届くまでの間に、Bが押された場合は、以下のようになります。
- Aが押されると、先ほどと同様にBに押された時の時間を送信します。
 - それをBが受け取る前にBも押され、Bが押された時の時間をAに送信します。
 - その後、BにAからの通信が届きます。Bは、自身が押された時間とAが押された時間を比較します。すると、Bは自身が負けたことが分かり、画面上に負けを表示します。
 - 同様に、AにBからの通信が届くと、時間を比較することで自身が勝ったことが分かり、画面上に勝ちを表示します。
 
実装
全体はGithubに置いておきます。
https://github.com/blu3mo/QuizButton
以下は、MCService.swiftの一部です。
早押しボタンが押された時の送信/受信まわりの実装は以下のようになっています。
メッセージはData型にエンコードしてやりとりしています。
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