iOS7將GameKit中的藍牙模塊單獨出的一個Multipeer Connectivity Framework,通過發現附近的設備用wifi或藍牙進行p2p連接。
概念和Class結構
- session,比如一個共同的聊天室就可以稱為一個session,class為
MCSession
- peerID,
MCPeerID
代表一個session內的設備ID,類似聊天室的ID號 - Advertiser,告訴附近的設備可以加入自己的session進行通訊,class有兩個,
MCAdvertiserAssistant
是自動的Adertiser管理類,另一個是MCNearbyServiceAdvertiser
需要自己實現代理方法 - Browser,尋找并加入附近的Advertiser,類似Advertiser也有兩個class,
MCNearbyServiceBrowser
同理MCNearbyServiceAdvertiser
,MCBrowserViewController
類似MCAdvertiserAssistant
是一個有默認視圖的browser。
實現multipeer有兩種方法,比如官方例子使用的是MCAdvertiserAssistant
和MCBrowserViewController
的組合,這種方法的優點是簡單,缺點是自帶的UI太丑,也不好明白具體的流程,所以本文選擇自己實現代理的方法。
session里peer可以同時是advertiser和browser,比如聊天室,每個人都可以發起或者加入。但peer也可以分開做advertiser和browser,這就類似client和server的模式。由于將advertiser和browser寫在一個工程會造成混淆,所以我選擇第二種模式來實現一個簡單的發送照片的demo,一個設備作為發送者拍照之后發送給session里的其他用戶。
Adertiser
我決定將發送者作為Adertiser來實現,稱為Server。其實作為browser也是可以的。首先創建一個single view的工程,然后在storyboard里加入一個顯示連接狀態的status
label,再加入一個發送圖片的button,并加入對應的IBOutlet(見本文最后的代碼)。
完成UI部分之后,首先需要import frameworkimport MultipeerConnectivity
,然后建立Adertiser。
let serviceType: String = "bs-mc"
var peerID: MCPeerID!
var session: MCSession!
var advertiser: MCNearbyServiceAdvertiser!
let displayName = UIDevice.currentDevice().name
self.peerID = MCPeerID(displayName: displayName)
self.session = MCSession(peer: self.peerID)
self.session.delegate = self
self.advertiser = MCNearbyServiceAdvertiser(peer: self.peerID, discoveryInfo: nil, serviceType: self.serviceType)
self.advertiser.delegate = self
self.advertiser.startAdvertisingPeer()
serviceType
用來標識哪些人能加入session,這個字符串不能超過15個字符,橫桿最多只能有一個。當有browser申請加入時就會調用下面的代理方法。
func advertiser(advertiser: MCNearbyServiceAdvertiser!, didReceiveInvitationFromPeer peerID:MCPeerID!, withContext context: NSData!, invitationHandler: ((Bool, MCSession!) -> Void)!) {
println("advertiser receive invitation from peer \(peerID.displayName)")
let alertController = UIAlertController(title: "Permission", message: "\(peerID.displayName) wants to know your position", preferredStyle: .Alert)
let okAction = UIAlertAction(title: "OK", style: .Default) { (action) -> Void in
invitationHandler(true, self.session)
}
let noAction = UIAlertAction(title: "NO", style: .Cancel) { (action) -> Void in
invitationHandler(false, nil)
}
alertController.addAction(okAction)
alertController.addAction(noAction)
self.presentViewController(alertController, animated: true, completion: nil)
}
當有browser申請加入session時該方法調用,這里會彈出一個alert,根據選擇來回調invitationHandler
并傳入對應的值,這樣browser就會接收到同意或拒絕。
MCSessionDelegate
的很多方法都是required,但這里只使用了session(_:peer:didChangeState:)
用于改變status
label。
func session(session: MCSession!, peer peerID: MCPeerID!, didChangeState state: MCSessionState) {
switch state {
case .Connected:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.status.text = "connected"
})
case .Connecting:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.status.text = "connecting..."
})
case .NotConnected:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.status.text = "connect fail"
})
default:
println("error")
}
}
Browser
圖片的接受者作為Browser去發現Advertiser的session,稱為Client。跟創建Advertiser一樣,Browser也需要peerID、session。
let serviceType: String = "bs-mc"
var peerID: MCPeerID!
var session: MCSession!
var browser: MCNearbyServiceBrowser!
let displayName = UIDevice.currentDevice().name
self.peerID = MCPeerID(displayName: displayName)
self.session = MCSession(peer: self.peerID)
self.session.delegate = self
self.browser = MCNearbyServiceBrowser(peer: self.peerID, serviceType: self.serviceType)
self.browser.delegate = self
self.browser.startBrowsingForPeers()
serviceType
需要跟advertiser相同,當Browser發現附近的advertiser之后就會調用方法browser(_:foundPeer:withDiscoveryInfo:)
。
func browser(browser: MCNearbyServiceBrowser!, foundPeer peerID: MCPeerID!, withDiscoveryInfo info: [NSObject : AnyObject]!) {
println("browser found peear \(peerID.displayName)")
browser.invitePeer(peerID, toSession: self.session, withContext: nil, timeout: 0)
}
iOS8之后browser會在找到advertiser之后自動申請加入session,可以去掉browser.invitePeer
,詳見Choosing an inviter when using Multipeer Connectivity。
數據傳輸
當Server和Client建立起連接之后就可以互相發送數據了。首先Server拍照,然后將照片作為數據發送出去。
func imagePickerController(picker: UIImagePickerController!, didFinishPickingImage image: UIImage!, editingInfo: [NSObject : AnyObject]!) {
picker.dismissViewControllerAnimated(true, completion: nil)
if let data = UIImagePNGRepresentation(image) {
var error: NSError?
self.session.sendData(data, toPeers: self.session.connectedPeers, withMode: .Unreliable,
error: &error)
}
}
當Client接受到數據后就會調用方法session(_:didReceiveData:fromPeer:)
。
func session(session: MCSession!, didReceiveData data: NSData!, fromPeer peerID: MCPeerID!) {
if let image = UIImage(data: data) {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.imageView.image = image
})
}
}
類似的也可以發送流數據和資源文件,startStreamWithName
和sendResourceAtURL
。
Demo代碼已放到Github:MultipeerConnectivityDemo