[iOS][Swift] イベント駆動型で P2P 通信をするPeerKit

2016.06.08

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

iOSには Peer to Peer を利用し複数のデバイス間で通信ができる Multipeer Connectivity Framework があります。
今回はそのP2P通信をイベント駆動型で構築することができるPeerKitを試してみました。ライセンスはMITです。

https://github.com/jpsim/PeerKit


※ Multipeer Connectivity Frameworkについての解説記事は下記をご覧ください。
P2P 通信を手軽に実現する Multipeer Connectivity Framework を使ってみる|Developers.IO

検証環境

今回は下記環境で試しています。

Xcode 7.3.1
Swift 2.2
CocoaPods 1.0.0

準備

CocoaPodsで追加します。

use_frameworks!

target "ターゲット名" do
  pod 'PeerKit', '~> 2.0'
end

デバイスの準備

今回はP2Pのやり取りのために端末を2台(以上)用意する必要があります。

実装

PeerKitを使ってやり取りするManagerを作成します。 名前はConnectionManager.swiftとします。

ConnectionManager.swift

import Foundation
import PeerKit
import MultipeerConnectivity

struct ConnectionManager {

}

接続の作成

自動的に接続するには PeerKit.transceive("接続文字列") を呼びます。接続文字列はBonjour service typeである必要があります。(ASCII小文字、数字、ハイフンの最大15文字)
ConnectionManagerにstart()を追加し、PeerKit.transceive()を呼び出します。

ConnectionManager.swift

struct ConnectionManager {
    
    static func start() {
        PeerKit.transceive("peerkit-sample")
    }
}

ConnectionManager.start()はAppDelegateで一度だけ呼び出します。

AppDelegate.swift

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        ConnectionManager.start()
        return true
    }

コネクション一覧

接続されているコネクション一覧は PeerKit.session?connectedPeersMCPeerID の配列が取得出来ます。
接続されている端末がない場合 PeerKit.session?nil になります。
今回はConnectionManagerにMCPeerIDの配列が取得できる、 static var peers: [MCPeerID] を追加しました。

ConnectionManager.swift

struct ConnectionManager {
    
    static var peers: [MCPeerID] {
        return PeerKit.session?.connectedPeers as [MCPeerID]? ?? []
    }
    
    static func start() {
        PeerKit.transceive("peerkit-sample")
    }
}

コネクションの接続と切断の通知

任意の端末が接続された時には、PeerKit.onConnect = PeerBlock?
切断された時には、PeerKit.onDisconnect = PeerBlock? になります。 PeerBlocktypealias PeerBlock = ((myPeerID: MCPeerID, peerID: MCPeerID) -> Void) となっています。
nilをセットすると処理は解除されます。

ConnectionManager.swift

struct ConnectionManager {
    
    static var peers: [MCPeerID] {
        return PeerKit.session?.connectedPeers as [MCPeerID]? ?? []
    }
    
    static func start() {
        PeerKit.transceive("peerkit-sample")
    }
    
    static func onConnect(run: PeerBlock?) {
        PeerKit.onConnect = run
    }

    static func onDisconnect(run: PeerBlock?) {
        PeerKit.onDisconnect = run
    }
}

UIViewControllerで下記のように記載しました。
viewWillAppear で端末の接続/切断の処理を登録、 viewWillDisappear でクリアしてます。

ViewController.swift

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)

        ConnectionManager.onConnect { _ in
            print("特定の端末が接続された")
            selt.updatePeers()
        }
        
        ConnectionManager.onDisconnect { _ in
            print("特定の端末が切断された")
            self.updatePeers()
        }
    }

    override func viewWillDisappear(animated: Bool) {
        ConnectionManager.onConnect(nil)
        ConnectionManager.onDisconnect(nil)

        super.viewWillDisappear(animated)
    }
    
    private func updatePeers() {
        // TODO: コネクション一覧が更新された場合の処理を実装する
        print(ConnectionManager.peers)
    }
}

onConnectとonDisconnectは選択画面など、コネクションの増減をリアルタイムで更新する必要のある場面で実装すればと良いと思います。

(ここまでの内容を)実行すると...

上記までを実装したものを端末2台に入れて、片方をXCodeにつないで実行すると、下記ログが表示されます。接続されているMCPeerIDが取得できました。

ConnectionManager_swift_と_backend___casp_Slack
MCPeerIDのdisplayNameにはデバイス名が入ります。

イベントの作成

コネクションが出来るようになったら次はイベントを作成します。
入力したテキストを送信ボタンが押されたタイミングで、コネクション中の端末全てに送るような感じのサンプルを今回は作りました。

UIの準備

まずは以下のような画面を仮で用意しました。

Main_storyboard_—_Edited

UIViewController.swift

class ViewController: UIViewController {
    
    @IBOutlet weak var inputTextField: UITextField!
    @IBOutlet weak var sendButton: UIButton!
    @IBOutlet weak var logTextView: UITextView!

    @IBAction func didTapSendButton(sender: AnyObject) {
        // TODO: 送信ボタンが押された時の処理
    }
    ... 以下、略

イベントの定義

PeerKitでは必要に応じて任意のイベントを定義する必要があります。
今回は単純にMessageというイベントを定義しました。

ConnectionManager.swift

enum Event: String {
    case Message = "Message"
}

struct ConnectionManager {

    ... 以下、略

イベントを送る

上記で定義したMessageイベントを送れるようにします。ConnectionManagerにイベントを送れるようにするsendMessageEventを定義します。

ConnectionManager.swift

    static func sendMessageEvent(message: String, from: String = PeerKit.myName, toPeers peers: [MCPeerID]? = PeerKit.session?.connectedPeers as [MCPeerID]?) {
        let anyObject = ["message": message, "from": from]
        PeerKit.sendEvent(Event.Message.rawValue, object: anyObject, toPeers: peers)
    }
}

送る処理としてボタンが押されたタイミングで、inputTextFieldのテキストを送る処理を追加しました。

UIViewController.swift

class ViewController: UIViewController {
    
    @IBOutlet weak var inputTextField: UITextField!
    @IBOutlet weak var sendButton: UIButton!
    @IBOutlet weak var logTextView: UITextView!

    @IBAction func didTapSendButton(sender: AnyObject) {
        // 送信ボタンが押された時の処理
        if let message = inputTextField.text where message != "" {
            ConnectionManager.sendMessageEvent(message)
            inputTextField.text = ""
        }
    }
    ... 以下、略

イベントを受け取る

他の端末から送られたデータを受け取れるようにします。

ConnectionManager.swift

    static func onEvent(event: Event, run: ObjectBlock?) {
        if let run = run {
            PeerKit.eventBlocks[event.rawValue] = run
        } else {
            PeerKit.eventBlocks.removeValueForKey(event.rawValue)
        }
    }

eventは受け取るイベント名、runにはそのイベントを実行した際のBlock処理を設定します。

そして受け取り側も実装します。Messageを受け取ったら、logTextViewに表示するようにしています。

ViewController.swift

    override func viewWillAppear(animated: Bool) {
        
        super.viewWillAppear(animated)

        ConnectionManager.onConnect { _ in
            print("特定の端末が接続された")
            selt.updatePeers()
        }
        
        ConnectionManager.onDisconnect { _ in
            print("特定の端末が切断された")
            self.updatePeers()
        }
        
        ConnectionManager.onEvent(.Message) { [unowned self] _, zobject in
            self.receiveMassage(object)
        }
    }
    
    private func receiveMassage(object: AnyObject?) {
        guard let data = object else {
            return
        }
        if let message = data["message"] as? String, from = data["from"] as? String{
            printMessage(message, from)
        }
    }

    private func printMessage(message: String, _ from: String) {
        logTextView.text = textView.text + from + ":" + message + "\n"
    }
}

※ 本当はobjectはデータ定義をした方が良いと思いますが、今回はサンプルのため省略してます。

デモ

実行すると下記のような感じになります。

イメージ図

実機で動かすと下記のイメージです。(見辛くてすみません。。。)

デモ

※ 自分が送ったメッセージもログに表示するように少し処理に修正を加えてます。

まとめ

長くなってしまったので、各処理をまとめてみます。

PeerKit.transceive(serviceType: String) 接続を開始させる
PeerKit.stopTransceiving() 接続を停止させる
PeerKit.onConnect = PeerBlock? 特定の端末が接続された時に呼び出すPeerBlockを定義する
PeerKit.onDisconnect = PeerBlock? 特定の端末が切断された時に呼び出すPeerBlockを定義する
PeerKit.eventBlocks["イベント名"] = ObjectBlock? 指定したイベント時に呼び出すObjectBlockを定義する
PeerKit.eventBlocks.removeValueForKey("イベント名") 指定したイベント時に呼び出すObjectBlockを削除する
PeerKit.sendEvent(event: String, object: AnyObject? = nil, toPeers peers: [MCPeerID]? = session?.connectedPeers) toPeersに設定したピアに対して、指定したイベントを実行します。

今回のサンプルではイベントを1つしか定義してませんでしたが、用途に応じて定義すると良いと思います。
例えば、Request(リクエスト)、Approve(承認)、Reject(却下)のイベントを定義して、Requestを受け取ったら送ってきた端末(pia)に対して、ApproveかRejectを返したりすれば許可/却下のフローが出来上がります。

参考

jpsim/CardsAgainst