Skip to content

Instantly share code, notes, and snippets.

@StevenMasini
Created August 23, 2021 06:35
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save StevenMasini/30b0da0f6335b40bca662ced80808c63 to your computer and use it in GitHub Desktop.
Save StevenMasini/30b0da0f6335b40bca662ced80808c63 to your computer and use it in GitHub Desktop.
Multi Nearby Interaction
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()
}
}
struct Coordinates {
let distance: Float
let direction: SIMD3<Float>?
}
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)
}
}
}
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