Multi Nearby Interaction
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import NearbyInteraction | |
import MultipeerConnectivity | |
import Combine | |
var numberFormatter: NumberFormatter { | |
let numberFormatter = NumberFormatter() | |
numberFormatter.roundingMode = .halfEven | |
numberFormatter.maximumFractionDigits = 3 | |
numberFormatter.minimumFractionDigits = 3 | |
return numberFormatter | |
} | |
class ContentViewModel: NSObject, ObservableObject, UWBEmitterDelegate { | |
var mpc: MPCSession? | |
@Published var state = "" | |
@Published var emitter1: UWBEmitter? | |
@Published var emitter2: UWBEmitter? | |
var emitters: [UWBEmitter] { | |
var emitters = [UWBEmitter]() | |
if let emitter1 = emitter1 { | |
emitters.append(emitter1) | |
} | |
if let emitter2 = emitter2 { | |
emitters.append(emitter2) | |
} | |
return emitters | |
} | |
override init() { | |
super.init() | |
startup() | |
} | |
func startup() { | |
logger.info("ContentViewModel.startup") | |
if !emitters.isEmpty { | |
for emitter in emitters { | |
if let myToken = emitter.session.discoveryToken, let discoveryToken = emitter.discoveryToken { | |
state = "Initializing ..." | |
shareMyDiscoveryToken(myToken) | |
let config = NINearbyPeerConfiguration(peerToken: discoveryToken) | |
emitter.session.run(config) | |
} else { | |
fatalError("Unable to get self discovery token, is this session invalidated?") | |
} | |
} | |
} else { | |
state = "Discovering Peer ..." | |
startupMPC() | |
} | |
} | |
// MARK: - Private Methods | |
private func startupMPC() { | |
logger.info("ContentViewModel.startupMPC") | |
if mpc == nil { | |
mpc = MPCSession(service: "auki", identity: "com.auki.proof-of-location", maxPeers: 3) | |
mpc?.peerConnectedHandler = peerConnectedHandler | |
mpc?.peerDataHandler = peerDataHandler | |
mpc?.peerDisconnectedHandler = peerDisconnectedHandler | |
} | |
mpc?.invalidate() | |
mpc?.start() | |
} | |
private func shareMyDiscoveryToken(_ token: NIDiscoveryToken) { | |
guard let encodedData = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true) else { | |
fatalError("Unexpectedly failed to encode discovery token.") | |
} | |
logger.info("ContentViewModel.shareMyDiscoveryToken") | |
mpc?.sendDataToAllPeers(data: encodedData) | |
} | |
private func peerDidShareDiscoveryToken(peer: MCPeerID, token: NIDiscoveryToken) { | |
logger.info("ContentViewModel.peerDidShareDiscoveryToken") | |
guard let emitter = emitters.first(where: { $0.peer == peer }) else { | |
fatalError("No emitter for this peer") | |
} | |
// Run the session. | |
if emitter.peer == emitter1?.peer { | |
emitter1!.discoveryToken = token | |
} else { | |
emitter2!.discoveryToken = token | |
} | |
} | |
// MARK: - Private MPC Handlers | |
func peerConnectedHandler(peer: MCPeerID) { | |
logger.info("ContentViewModel.peerConnectedHandler") | |
// remove previous similar peers | |
var emitter = emitters.first(where: { $0.peer == peer }) | |
if emitter1 == nil { | |
emitter1 = UWBEmitter(peer: peer) | |
emitter1?.delegate = self | |
emitter = emitter1 | |
} else { | |
emitter2 = UWBEmitter(peer: peer) | |
emitter2?.delegate = self | |
emitter = emitter2 | |
} | |
guard let myToken = emitter!.session.discoveryToken else { | |
fatalError("Unexpectedly failed to initialize nearby interaction session.") | |
} | |
debugPrint("MY TOKEN: \(myToken)") | |
shareMyDiscoveryToken(myToken) | |
state = "Connected" | |
} | |
private func peerDataHandler(data: Data, peer: MCPeerID) { | |
guard let discoveryToken = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NIDiscoveryToken.self, from: data) else { | |
fatalError("Unexpectedly failed to decode discovery token.") | |
} | |
logger.info("ContentViewModel.peerDataHandler") | |
peerDidShareDiscoveryToken(peer: peer, token: discoveryToken) | |
} | |
private func peerDisconnectedHandler(peer: MCPeerID) { | |
logger.info("ContentViewModel.peerDisconnectedHandler") | |
let emitter = emitters.first(where: { $0.peer == peer }) | |
if emitter?.peer == emitter1?.peer { | |
emitter1 = nil | |
} else { | |
emitter2 = nil | |
} | |
} | |
// MARK: - UWBEmitterDelegate | |
func emitterPeerEnded(_ emitter: UWBEmitter) { | |
if emitter == emitter1 { | |
emitter1 = nil | |
} else { | |
emitter2 = nil | |
} | |
startup() | |
state = "Peer Ended" | |
} | |
func emitterPeerTimeout(_ emitter: UWBEmitter) { | |
state = "Timeout" | |
} | |
func emitterSessionWasSuspended(_ emitter: UWBEmitter) { | |
state = "Session suspended" | |
} | |
func emitterSessionSuspensionEnded(_ emitter: UWBEmitter) { | |
startup() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct Coordinates { | |
let distance: Float | |
let direction: SIMD3<Float>? | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import MultipeerConnectivity | |
struct MPCSessionConstants { | |
static let kKeyIdentity: String = "identity" | |
} | |
class MPCSession: NSObject, MCSessionDelegate, MCNearbyServiceBrowserDelegate, MCNearbyServiceAdvertiserDelegate { | |
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?) { } | |
func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { } | |
var peerDataHandler: ((Data, MCPeerID) -> Void)? | |
var peerConnectedHandler: ((MCPeerID) -> Void)? | |
var peerDisconnectedHandler: ((MCPeerID) -> Void)? | |
private let serviceString: String | |
private let mcSession: MCSession | |
private let localPeerID = MCPeerID(displayName: UIDevice.current.name) | |
private let mcAdvertiser: MCNearbyServiceAdvertiser | |
private let mcBrowser: MCNearbyServiceBrowser | |
private let identityString: String | |
private let maxNumPeers: Int | |
init(service: String, identity: String, maxPeers: Int) { | |
logger.info("MPCSession.init(service: \(service), identity: \(identity), maxPeers: \(maxPeers)") | |
serviceString = service | |
identityString = identity | |
mcSession = MCSession(peer: localPeerID, securityIdentity: nil, encryptionPreference: .required) | |
mcAdvertiser = MCNearbyServiceAdvertiser(peer: localPeerID, | |
discoveryInfo: [MPCSessionConstants.kKeyIdentity: identityString], | |
serviceType: serviceString) | |
mcBrowser = MCNearbyServiceBrowser(peer: localPeerID, serviceType: serviceString) | |
maxNumPeers = maxPeers | |
super.init() | |
mcSession.delegate = self | |
mcAdvertiser.delegate = self | |
mcBrowser.delegate = self | |
} | |
// MARK: - `MPCSession` public methods. | |
func start() { | |
logger.info("MPCSession.start") | |
mcAdvertiser.startAdvertisingPeer() | |
mcBrowser.startBrowsingForPeers() | |
} | |
func suspend() { | |
logger.info("MPCSession.suspend") | |
mcAdvertiser.stopAdvertisingPeer() | |
mcBrowser.stopBrowsingForPeers() | |
} | |
func invalidate() { | |
logger.info("MPCSession.invalidate") | |
suspend() | |
mcSession.disconnect() | |
} | |
func sendDataToAllPeers(data: Data) { | |
logger.info("MPCSession.sendDataToAllPeers(\(data))") | |
sendData(data: data, peers: mcSession.connectedPeers, mode: .reliable) | |
} | |
func sendData(data: Data, peers: [MCPeerID], mode: MCSessionSendDataMode) { | |
logger.info("MPCSession.sendData(data: \(data), peers: \(peers), mode: \(mode)") | |
do { | |
try mcSession.send(data, toPeers: peers, with: mode) | |
} catch let error { | |
NSLog("Error sending data: \(error)") | |
} | |
} | |
// MARK: - `MPCSession` private methods. | |
private func peerConnected(peerID: MCPeerID) { | |
logger.info("MPCSession.peerConnected(peerID: \(peerID))") | |
if let handler = peerConnectedHandler { | |
DispatchQueue.main.async { | |
handler(peerID) | |
} | |
} | |
if mcSession.connectedPeers.count == maxNumPeers { | |
self.suspend() | |
} | |
} | |
private func peerDisconnected(peerID: MCPeerID) { | |
logger.info("MPCSession.peerDisconnected(peerID: \(peerID)") | |
if let handler = peerDisconnectedHandler { | |
DispatchQueue.main.async { | |
handler(peerID) | |
} | |
} | |
if mcSession.connectedPeers.count < maxNumPeers { | |
self.start() | |
} | |
} | |
// MARK: - `MCSessionDelegate`. | |
internal func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { | |
logger.info("MPCSession.session(_ session: MCSession, peer peerID: \(peerID), didChange state: \(state)") | |
switch state { | |
case .connected: | |
peerConnected(peerID: peerID) | |
case .notConnected: | |
peerDisconnected(peerID: peerID) | |
case .connecting: | |
break | |
@unknown default: | |
fatalError("Unhandled MCSessionState") | |
} | |
} | |
internal func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { | |
logger.info("MPCSession.session(_ session: MCSession, didReceive data: \(data), fromPeer peerID: \(peerID)") | |
DispatchQueue.main.async { [self] in self.peerDataHandler?(data, peerID) } | |
} | |
// MARK: - `MCNearbyServiceBrowserDelegate`. | |
internal func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) { | |
logger.info("MPCSession.browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: \(peerID), withDiscoveryInfo info: \(info ?? [:])") | |
guard let identityValue = info?[MPCSessionConstants.kKeyIdentity] else { | |
return | |
} | |
if identityValue == identityString && mcSession.connectedPeers.count < maxNumPeers { | |
browser.invitePeer(peerID, to: mcSession, withContext: nil, timeout: 10) | |
} | |
} | |
// MARK: - `MCNearbyServiceAdvertiserDelegate`. | |
internal func advertiser(_ advertiser: MCNearbyServiceAdvertiser, | |
didReceiveInvitationFromPeer peerID: MCPeerID, | |
withContext context: Data?, | |
invitationHandler: @escaping (Bool, MCSession?) -> Void) { | |
logger.info("advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: \(peerID), withContext context: \(context ?? Data()), invitationHandler: @escaping (Bool, MCSession?) -> Void)") | |
// Accept the invitation only if the number of peers is less than the maximum. | |
if self.mcSession.connectedPeers.count < maxNumPeers { | |
invitationHandler(true, mcSession) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import NearbyInteraction | |
import MultipeerConnectivity | |
protocol UWBEmitterDelegate: AnyObject { | |
func emitterPeerEnded(_ emitter: UWBEmitter) | |
func emitterPeerTimeout(_ emitter: UWBEmitter) | |
func emitterSessionWasSuspended(_ emitter: UWBEmitter) | |
func emitterSessionSuspensionEnded(_ emitter: UWBEmitter) | |
} | |
class UWBEmitter: NSObject, NISessionDelegate { | |
let peer: MCPeerID | |
let session: NISession | |
var coordinate = Coordinates(distance: 0, direction: nil) | |
var discoveryToken: NIDiscoveryToken? = nil { | |
didSet { | |
guard let discoveryToken = discoveryToken else { return } | |
let config = NINearbyPeerConfiguration(peerToken: discoveryToken) | |
session.run(config) | |
} | |
} | |
weak var delegate: UWBEmitterDelegate? = nil | |
init(peer: MCPeerID, session: NISession = .init()) { | |
self.peer = peer | |
self.session = session | |
super.init() | |
self.session.delegate = self | |
} | |
// MARK: - CustomDebugStringConvertible | |
override var debugDescription: String { | |
"\(String(describing: peer)) \(String(describing: session)) \(String(describing: coordinate)) \(String(describing: discoveryToken))" | |
} | |
// MARK: - NISessionDelegate | |
func session(_ session: NISession, didUpdate nearbyObjects: [NINearbyObject]) { | |
debugPrint("session didUpdate nearbyObjects") | |
for nearbyObject in nearbyObjects { | |
guard let distance = nearbyObject.distance else { continue } | |
coordinate = Coordinates(distance: distance, direction: nearbyObject.direction) | |
logger.info("\(peer.displayName) \(coordinate)") | |
} | |
} | |
func session(_ session: NISession, didRemove nearbyObjects: [NINearbyObject], reason: NINearbyObject.RemovalReason) { | |
for nearbyObject in nearbyObjects { | |
guard nearbyObject.discoveryToken == discoveryToken else { continue } | |
switch reason { | |
case .peerEnded: | |
// The peer stopped communicating, so invalidate the session because | |
// it's finished. | |
session.invalidate() | |
delegate?.emitterPeerEnded(self) | |
case .timeout: | |
// The peer timed out, but the session is valid. | |
// If the configuration is valid, run the session again. | |
if let config = session.configuration { | |
session.run(config) | |
} | |
delegate?.emitterPeerTimeout(self) | |
default: | |
fatalError("Unknown and unhandled NINearbyObject.RemovalReason") | |
} | |
} | |
} | |
func sessionWasSuspended(_ session: NISession) { | |
delegate?.emitterSessionWasSuspended(self) | |
} | |
func sessionSuspensionEnded(_ session: NISession) { | |
if let config = session.configuration { | |
session.run(config) | |
} else { | |
delegate?.emitterSessionSuspensionEnded(self) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment