#概要
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