why is it impossible for a second client to connect to the host?
import UIKit
import SwiftUI
import NearbyInteraction
import MultipeerConnectivity
import CoreBluetooth
import AVFoundation
import CoreMotion
class ViewController: UIViewController, NISessionDelegate, MCSessionDelegate,
MCNearbyServiceAdvertiserDelegate, MCNearbyServiceBrowserDelegate,
CBCentralManagerDelegate, AVAudioRecorderDelegate {
// MARK: - Properties
var niSessions: [MCPeerID: NISession] = [:]
var mcSession: MCSession?
var peerID: MCPeerID?
var mcAdvertiser: MCNearbyServiceAdvertiser?
var mcBrowser: MCNearbyServiceBrowser?
var connectedPeers: [MCPeerID: NIDiscoveryToken] = [:]
var distanceLabels: [MCPeerID: UILabel] = [:]
var centralManager: CBCentralManager?
var motionManager = MotionManager()
var isMovementDetectionEnabled = true
var taskId: UIBackgroundTaskIdentifier = .invalid
var username: String?
// Properties for sound playback
var audioEngine = AVAudioEngine()
var audioPlayerNode = AVAudioPlayerNode()
var audioFile: AVAudioFile?
var recordedSoundURL: URL? // Only one recorded sound URL
var currentTimeGap: TimeInterval = 1.0 // Initial gap
var playbackTimer: Timer?
var delayEffect = AVAudioUnitDelay()
// AVAudioRecorder properties
var audioRecorder: AVAudioRecorder?
var isRecording: Bool = false
var levelTimer: Timer?
// Boolean to distinguish host and joiner
var isHost: Bool = false
// UI Elements
var recordButton: UIButton?
// Timer to disable echo effect after a period
var echoResetTimer: Timer?
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// Initialize Central Manager for Bluetooth permissions
centralManager = CBCentralManager(delegate: self, queue: nil)
// Observe app state changes
NotificationCenter.default.addObserver(
selector: #selector(applicationWillEnterBackground),
name: UIApplication.didEnterBackgroundNotification,
NotificationCenter.default.addObserver(
selector: #selector(applicationWillEnterForeground),
name: UIApplication.willEnterForegroundNotification,
NotificationCenter.default.addObserver(
selector: #selector(applicationWillTerminate),
name: UIApplication.willTerminateNotification,
// Check if Nearby Interaction is supported
if !NISession.isSupported {
let alert = UIAlertController(
title: "Unsupported Device",
message: "This device does not support Nearby Interaction.",
self.present(alert, animated: true, completion: nil)
// Ensure that the device supports UWB (iPhone 11 or later)
let alert = UIAlertController(
title: "Unsupported Device",
message: "This device does not support Ultra-Wideband (UWB) required for Nearby Interaction.",
self.present(alert, animated: true, completion: nil)
// Request microphone permissions
AVAudioSession.sharedInstance().requestRecordPermission { granted in
DispatchQueue.main.async {
let alert = UIAlertController(
title: "Microphone Access Denied",
message: "Please enable microphone access in Settings.",
alert.addAction(UIAlertAction(title: "OK", style: .default))
self.present(alert, animated: true)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Show login interface if the username is not set
DispatchQueue.main.async {
self.showRoleSelectionInterface()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// motionManager.stopAccelerometerUpdates()
}
// MARK: - Device Compatibility Check
func isUWBSupported() -> Bool {
// Check if the device supports UWB
let supportedDeviceModels: [String] = [
“iPhone12,1”, // iPhone 11
“iPhone12,3”, // iPhone 11 Pro
“iPhone12,5”, // iPhone 11 Pro Max
“iPhone13,1”, // iPhone 12 mini
“iPhone13,2”, // iPhone 12
“iPhone13,3”, // iPhone 12 Pro
“iPhone13,4”, // iPhone 12 Pro Max
“iPhone14,4”, // iPhone 13 mini
“iPhone14,5”, // iPhone 13
“iPhone14,2”, // iPhone 13 Pro
“iPhone14,3”, // iPhone 13 Pro Max
“iPhone14,7”, // iPhone 14
“iPhone14,8”, // iPhone 14 Plus
“iPhone15,2”, // iPhone 14 Pro
“iPhone15,3”, // iPhone 14 Pro Max
// Add more models as needed
]
var systemInfo = utsname()
let machineMirror = Mirror(reflecting: systemInfo.machine)
let modelCode = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else {
return identifier + String(UnicodeScalar(UInt8(value)))
return supportedDeviceModels.contains(modelCode)
}
func enableEchoEffect() {
// Configure the delay effect parameters
delayEffect.wetDryMix = 80 // Percentage of echo effect (0 to 100)
delayEffect.delayTime = 0.5 // Delay time in seconds
delayEffect.feedback = 60 // Feedback percentage
print("Echo effect enabled")
// Reset the echo effect after a period
echoResetTimer?.invalidate()
echoResetTimer = Timer.scheduledTimer(
timeInterval: 5.0, // Duration of echo effect
selector: #selector(disableEchoEffect),
}
@objc func disableEchoEffect() {
delayEffect.wetDryMix = 0
delayEffect.feedback = 0
print(“Echo effect disabled”)
}
// MARK: - UI Setup
func showRoleSelectionInterface() {
let alertController = UIAlertController(
title: “Select Role”,
message: “Do you want to Host or Join a session?”,
preferredStyle: .alert
)
let hostAction = UIAlertAction(title: "Host", style: .default) { _ in
self.showLoginInterface()
let joinAction = UIAlertAction(title: "Join", style: .default) { _ in
self.showLoginInterface()
alertController.addAction(hostAction)
alertController.addAction(joinAction)
present(alertController, animated: true)
}
func showLoginInterface() {
let role = isHost ? “Host” : “Joiner”
let alertController = UIAlertController(
title: “Login ((role))”,
message: “Enter your username”,
preferredStyle: .alert
)
alertController.addTextField { textField in
textField.placeholder = "Username"
let loginAction = UIAlertAction(title: "Login", style: .default) { _ in
if let username = alertController.textFields?.first?.text,
self.peerID = MCPeerID(displayName: username)
self.showLoginInterface() // Retry if username is empty
alertController.addAction(loginAction)
present(alertController, animated: true)
}
func setupSession() {
guard let peerID = self.peerID else {
print(“Error: peerID is nil in setupSession()”)
return
}
encryptionPreference: .required
mcSession?.delegate = self
}
func setupUI() {
// Remove any existing views
for subview in view.subviews {
subview.removeFromSuperview()
}
let roleLabel = UILabel()
roleLabel.text = isHost ? "Hosting Session" : "Joining Session"
roleLabel.textAlignment = .center
roleLabel.frame = CGRect(
width: view.frame.width - 40,
view.addSubview(roleLabel)
// Add the round-shaped record button
recordButton = UIButton(type: .custom)
/* if let recordButton = recordButton {
recordButton.frame = CGRect(
x: (view.frame.width - 80) / 2,
y: view.frame.height - 150,
recordButton.layer.cornerRadius = 40 // Make it round
recordButton.backgroundColor = .systemGreen // Default color is green
recordButton.setTitle("Go To Music Toy Page", for: .normal)
recordButton.setTitleColor(.white, for: .normal)
action: #selector(recordButtonTouchDown),
action: #selector(recordButtonTouchUp),
for: [.touchUpInside, .touchUpOutside]
view.addSubview(recordButton)
let recordButton = UIButton(type: .custom)
recordButton.frame = CGRect(
x: (view.frame.width - 80) / 2,
y: view.frame.height - 150,
recordButton.layer.cornerRadius = 40 // Make it round
recordButton.backgroundColor = .systemGreen // Default color is green
recordButton.setTitle("Go", for: .normal)
recordButton.setTitleColor(.white, for: .normal)
recordButton.addTarget(self, action: #selector(recordButtonTapped), for: .touchUpInside)
view.addSubview(recordButton)
}
var hostingController: UIHostingController?
@objc func recordButtonTapped() {
// 打印当前的值
print(“Go to Music Toy”)
let musicToyView = MusicToyView().environmentObject(motionManager)
let hostingController = UIHostingController(rootView: musicToyView)
hostingController.modalPresentationStyle = .fullScreen // 可选,设置为全屏
present(hostingController, animated: true, completion: nil)
}
// MARK: - Recording Audio
@objc func recordButtonTouchDown() {
print(“Record button pressed”)
startRecording()
// Change button color to blue when recording
recordButton?.backgroundColor = .systemBlue
}
@objc func recordButtonTouchUp() {
print(“Record button released”)
stopRecording()
// Change button color back to green when not recording
recordButton?.backgroundColor = .systemGreen
}
func startRecording() {
guard !isRecording else { return }
let audioSession = AVAudioSession.sharedInstance()
// Set audio session category and mode
try audioSession.setCategory(
options: [.defaultToSpeaker]
try audioSession.setActive(true)
// Set input gain if possible
if audioSession.isInputGainSettable {
try audioSession.setInputGain(1.0) // Maximum gain
// Verify audio session input
let currentRoute = audioSession.currentRoute
for input in currentRoute.inputs {
print("Current input: \(input.portName)")
// Define the recording settings
let settings: [String: Any] = [
AVFormatIDKey: Int(kAudioFormatAppleLossless),
AVNumberOfChannelsKey: 1,
AVEncoderBitDepthHintKey: 16,
AVEncoderAudioQualityKey: AVAudioQuality.max.rawValue
// Create a unique filename for the recording
let filename = "recording-\(Date().timeIntervalSince1970).m4a"
let documentsDirectory = FileManager.default.urls(
let audioURL = documentsDirectory.appendingPathComponent(filename)
// Initialize the recorder
audioRecorder = try AVAudioRecorder(url: audioURL, settings: settings)
audioRecorder?.delegate = self
audioRecorder?.isMeteringEnabled = true
// Start monitoring audio levels
levelTimer = Timer.scheduledTimer(
selector: #selector(monitorAudioLevels),
print("Started recording to \(audioURL)")
print("Failed to start recording: \(error.localizedDescription)")
}
@objc func monitorAudioLevels() {
audioRecorder?.updateMeters()
if let averagePower = audioRecorder?.averagePower(forChannel: 0) {
print(“Average input power: (averagePower) dB”)
}
}
func stopRecording() {
guard isRecording else { return }
print("Stopped recording")
}
// AVAudioRecorderDelegate method
func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
if flag {
let url = recorder.url
print(“Recording saved to (url)”)
// Delete the previous recording if it exists
if let previousURL = recordedSoundURL {
deleteRecordingFile(at: previousURL)
// Update the recorded sound URL to the new recording
setupAudioPlayers() // Update the audio players with the new recording
print("Recording failed")
}
func deleteRecordingFile(at url: URL) {
do {
try FileManager.default.removeItem(at: url)
print(“Deleted recording at (url)”)
} catch {
print(“Failed to delete recording at (url): (error.localizedDescription)”)
}
}
// MARK: - Sound Playback
func setupAudioPlayers() {
// Reset the audio engine
audioEngine.stop()
audioEngine.reset()
audioEngine = AVAudioEngine()
audioPlayerNode = AVAudioPlayerNode()
delayEffect = AVAudioUnitDelay()
delayEffect.wetDryMix = 0 // Start with echo effect disabled
audioEngine.attach(audioPlayerNode)
audioEngine.attach(delayEffect)
audioEngine.connect(audioPlayerNode, to: delayEffect, format: nil)
audioEngine.connect(delayEffect, to: audioEngine.mainMixerNode, format: nil)
// Load the recorded sound file
if let url = recordedSoundURL {
audioFile = try AVAudioFile(forReading: url)
print("Loaded sound from \(url.lastPathComponent)")
print("Error loading sound from \(url.lastPathComponent): \(error)")
// Prepare and start the audio engine
print("Audio engine started")
// Set the playback volume here
audioEngine.mainMixerNode.outputVolume = 140 // Adjust volume between 0.0 and 1.0
print("Failed to start audio engine: \(error.localizedDescription)")
}
func startPlayingSounds() {
if audioFile == nil {
print(“No sounds available to play”)
return
}
// Configure audio session for playback
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .default, options: [])
try audioSession.setActive(true)
print("Failed to activate audio session for playback: \(error.localizedDescription)")
}
func stopPlayingSounds() {
playbackTimer?.invalidate()
playbackTimer = nil
audioPlayerNode.stop()
audioEngine.stop()
}
func startPlaybackTimer() {
playbackTimer?.invalidate() // Invalidate existing timer if any
playbackTimer = Timer.scheduledTimer(
timeInterval: currentTimeGap,
target: self,
selector: #selector(playNextSound),
userInfo: nil,
repeats: true
)
print(“Started playback timer with interval (currentTimeGap) seconds”)
}
@objc func playNextSound() {
guard let audioFile = audioFile else { return }
// Schedule the audio file to play without stopping previous playback
audioPlayerNode.scheduleFile(audioFile, at: nil, completionHandler: nil)
// If host, send a message to joiners
// Send a message to connected peers
if let mcSession = mcSession, mcSession.connectedPeers.count > 0 {
// Create a dictionary with the time gap
"timeGap": currentTimeGap
let data = try JSONSerialization.data(
toPeers: mcSession.connectedPeers,
print("Host sent playSound message to joiners")
print("Error sending playSound message: \(error)")
}
func playSoundFromJoiner() {
if let audioFile = audioFile {
// Schedule the audio file to play without stopping previous playback
audioPlayerNode.scheduleFile(audioFile, at: nil, completionHandler: nil)
print(“Joiner playing sound”)
} else {
print(“No sound available to play”)
}
}
func updateSoundPlayback(forDistance distance: Float) {
let newTimeGap = calculateTimeGap(forDistance: Double(distance))
if abs(newTimeGap - currentTimeGap) > 0.01 {
currentTimeGap = newTimeGap
print("Updated time gap to \(currentTimeGap) seconds for distance \(distance) meters")
startPlaybackTimer() // Restart timer with new time gap
// Optional: Adjust volume based on distance
// Uncomment the following lines to enable volume adjustment based on distance
let newVolume = calculateVolume(forDistance: Double(distance))
audioEngine.mainMixerNode.outputVolume = newVolume
print("Adjusted volume to \(newVolume) for distance \(distance) meters")
}
func calculateTimeGap(forDistance distance: Double) -> TimeInterval {
// Adjust time gap calculation as needed
// For example, map the distance to a time gap between 0.5 and 2.0 seconds
let minGap: TimeInterval = 0.5
let maxGap: TimeInterval = 2.0
let maxDistance: Double = 5.0 // Maximum expected distance
let normalizedDistance = min(distance / maxDistance, 1.0)
let timeGap = minGap + (maxGap - minGap) * normalizedDistance
}
// Optional: Calculate volume based on distance
func calculateVolume(forDistance distance: Double) -> Float {
let maxDistance: Double = 5.0 // Maximum expected distance
let normalizedDistance = min(distance / maxDistance, 1.0)
let volume = Float(1.0 - normalizedDistance) // Inverse relationship
}
// MARK: - NISessionDelegate
// MARK: - NISessionDelegate
func session(_ session: NISession, didUpdate nearbyObjects: [NINearbyObject]) {
print(“session didUpdate called”)
DispatchQueue.main.async {
for nearbyObject in nearbyObjects {
guard let distance = nearbyObject.distance else {
print(“No distance information available.”)
continue
}
//matt22221111
print("Received distance: \(distance) meters")
SharedData.tempoValue = distance
self.updateSoundPlayback(forDistance: distance)
if let peerID = self.niSessions.first(where: { $0.value == session })?.key {
if let label = self.distanceLabels[peerID] {
label.text = "\(peerID.displayName): \(String(format: "%.2f", distance)) meters"
label.text = "\(peerID.displayName): \(String(format: "%.2f", distance)) meters"
y: 100 + (self.distanceLabels.count * 40),
self.view.addSubview(label)
self.distanceLabels[peerID] = label
}
func session(_ session: NISession, didInvalidateWith error: Error) {
print(“session didInvalidateWith called with error: (error.localizedDescription)”)
if let peerID = niSessions.first(where: { $0.value == session })?.key {
let nsError = error as NSError
print(“NISession invalidated for peer (peerID.displayName) with error: (error.localizedDescription)”)
print(“Error code: (nsError.code), domain: (nsError.domain)”)
// Handle specific NIError codes
if nsError.domain == NIErrorDomain,
let niErrorCode = NIError.Code(rawValue: nsError.code) {
print("User did not allow Nearby Interaction access.")
// Prompt user to enable permissions
DispatchQueue.main.async {
let alert = UIAlertController(
title: "Permission Denied",
message: "Please enable Nearby Interaction permissions in Settings.",
alert.addAction(UIAlertAction(
if let appSettings = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(appSettings)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
self.present(alert, animated: true)
print("Session failed due to an internal error.")
// Attempt to restart the session
self.restartNISession(for: peerID)
print("An unknown error occurred.")
// Attempt to restart the session
self.restartNISession(for: peerID)
// For other errors, attempt to restart the session
self.restartNISession(for: peerID)
}
func sessionWasSuspended(_ session: NISession) {
print(“sessionWasSuspended called”)
}
func sessionSuspensionEnded(_ session: NISession) {
print(“sessionSuspensionEnded called”)
if let peerID = niSessions.first(where: { $0.value == session })?.key,
let token = connectedPeers[peerID] {
let config = NINearbyPeerConfiguration(peerToken: token)
session.run(config)
}
}
func session(_ session: NISession, didGenerateShareableConfigurationData data: Data,
for object: NINearbyObject) {
// This method is not used in this implementation
}
// MARK: - MCSessionDelegate
func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
DispatchQueue.main.async {
switch state {
case .connected:
if self.isHost {
print(“Host connected to (peerID.displayName)”)
// Create NISession for this peer
if let _ = self.createNISession(for: peerID) {
self.sendDiscoveryToken(to: peerID)
}
} else {
if session.connectedPeers.count > 1 {
print(“Joiner can only connect to one host. Disconnecting from (peerID.displayName).”)
session.cancelConnectPeer(peerID)
} else {
print(“Connected to host: (peerID.displayName)”)
// Create NISession for this peer
if let _ = self.createNISession(for: peerID) {
self.sendDiscoveryToken(to: peerID)
}
}
}
case .connecting:
print(“Connecting to (peerID.displayName)”)
case .notConnected:
print(“Disconnected from (peerID.displayName)”)
self.cleanupPeer(peerID)
if !self.isHost {
self.startBrowsing()
}
@unknown default:
fatalError(“Unknown state”)
}
}
}
func cleanupPeer(_ peerID: MCPeerID) {
DispatchQueue.main.async {
if let label = self.distanceLabels[peerID] {
label.removeFromSuperview()
self.distanceLabels.removeValue(forKey: peerID)
}
}
self.connectedPeers.removeValue(forKey: peerID)
if let session = niSessions[peerID] {
session.invalidate()
niSessions.removeValue(forKey: peerID)
}
}
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
// First, try to unarchive data as NIDiscoveryToken
do {
if let token = try NSKeyedUnarchiver.unarchivedObject(
ofClass: NIDiscoveryToken.self, from: data
) {
connectedPeers[peerID] = token
exchangeDiscoveryTokens(with: peerID, token: token)
return
}
} catch {
// Not a discovery token, proceed to check if it’s a JSON message
}
// Try to parse data as JSON message
if let message = try JSONSerialization.jsonObject(
let command = message["command"] as? String {
if command == "playSound" {
// Handle playSound command
if let timeGap = message["timeGap"] as? TimeInterval {
self.currentTimeGap = timeGap // Update the time gap
// Schedule sound playback after half the time gap
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.playSoundFromJoiner()
print("Joiner scheduled sound playback after \(delay) seconds")
print("Received invalid data from \(peerID.displayName)")
print("Failed to decode message from \(peerID.displayName): \(error.localizedDescription)")
}
// Other MCSessionDelegate methods (unused in this context)
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 session(_ session: MCSession,
didReceive stream: InputStream,
withName streamName: String,
fromPeer peerID: MCPeerID) {}
func session(_ session: MCSession,
didReceiveCertificate certificate: [Any]?,
fromPeer peerID: MCPeerID,
certificateHandler: @escaping (Bool) -> Void) {
certificateHandler(true)
}
// MARK: - MCNearbyServiceAdvertiserDelegate
func advertiser(_ advertiser: MCNearbyServiceAdvertiser,
didNotStartAdvertisingPeer error: Error) {
print(“Failed to start advertising: (error.localizedDescription)”)
}
func advertiser(_ advertiser: MCNearbyServiceAdvertiser,
didReceiveInvitationFromPeer peerID: MCPeerID,
withContext context: Data?,
invitationHandler: @escaping (Bool, MCSession?) -> Void) {
if isHost {
invitationHandler(true, mcSession)
} else {
invitationHandler(false, nil)
}
}
// MARK: - MCNearbyServiceBrowserDelegate
func browser(_ browser: MCNearbyServiceBrowser,
foundPeer peerID: MCPeerID,
withDiscoveryInfo info: [String: String]?) {
print(“Found peer: (peerID.displayName)”)
if !isHost && mcSession?.connectedPeers.count == 0 {
browser.invitePeer(
peerID,
to: mcSession!,
withContext: nil,
timeout: 30 // Increased timeout
)
}
}
func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
print(“Lost peer: (peerID.displayName)”)
}
// MARK: - NISession Management
func createNISession(for peerID: MCPeerID) -> NISession? {
// Create a new NISession for this peer
let session = NISession()
session.delegate = self
niSessions[peerID] = session
print(“Created NISession for (peerID.displayName)”)
// Wait a moment before sending the discovery token
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.sendDiscoveryToken(to: peerID)
}
func sendDiscoveryToken(to peerID: MCPeerID, retryCount: Int = 0) {
guard let session = niSessions[peerID] else {
print(“NISession not available for (peerID.displayName)”)
return
}
guard let discoveryToken = session.discoveryToken else {
if retryCount < 10 { // Increase the retry limit
print("Discovery token not yet available for \(peerID.displayName). Retrying (\(retryCount + 1))...")
// Retry after a longer delay
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.sendDiscoveryToken(to: peerID, retryCount: retryCount + 1)
print("Failed to obtain discovery token for \(peerID.displayName) after multiple attempts.")
// Handle the failure appropriately
guard let mcSession = mcSession else {
print("MCSession is not initialized.")
if !mcSession.connectedPeers.contains(peerID) {
print("Cannot send discovery token to \(peerID.displayName): Peer is not connected.")
let data = try NSKeyedArchiver.archivedData(
withRootObject: discoveryToken,
requiringSecureCoding: true
try mcSession.send(data, toPeers: [peerID], with: .reliable)
print("Sent discovery token to \(peerID.displayName)")
print("Failed to send discovery token to \(peerID.displayName): \(error.localizedDescription)")
}
func exchangeDiscoveryTokens(with peerID: MCPeerID, token: NIDiscoveryToken) {
if let session = niSessions[peerID] {
let config = NINearbyPeerConfiguration(peerToken: token)
session.run(config)
print(“Exchanged tokens and started NI session with (peerID.displayName)”)
} else {
print(“NISession not found for (peerID.displayName)”)
}
}
func restartNISession(for peerID: MCPeerID) {
// Invalidate the old session
if let oldSession = niSessions[peerID] {
oldSession.invalidate()
niSessions.removeValue(forKey: peerID)
}
if let _ = createNISession(for: peerID) {
// Wait a moment before sending the discovery token
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.sendDiscoveryToken(to: peerID)
print("Restarted NI session with \(peerID.displayName)")
}
// MARK: - App State Handling
@objc func applicationWillEnterBackground() {
// stopAccelerometerUpdates()
stopPlayingSounds()
// Begin background task if needed
taskId = UIApplication.shared.beginBackgroundTask(expirationHandler: {
UIApplication.shared.endBackgroundTask(self.taskId)
self.taskId = .invalid
})
}
@objc func applicationWillEnterForeground() {
// startAccelerometerUpdates()
// Start playing sounds
startPlayingSounds()
if taskId != .invalid {
UIApplication.shared.endBackgroundTask(taskId)
taskId = .invalid
}
}
@objc func applicationWillTerminate() {
shutdownMultiPeerStuff()
}
func shutdownMultiPeerStuff() {
stopAdvertisingAndBrowsing()
mcSession?.disconnect()
for (_, session) in niSessions {
session.invalidate()
}
niSessions.removeAll()
}
// MARK: - Hosting and Browsing
func startHosting() {
stopAdvertisingAndBrowsing() // Clean-up before starting
guard let peerID = self.peerID else {
print(“Error: peerID is nil in startHosting()”)
return
}
mcAdvertiser = MCNearbyServiceAdvertiser(
peer: peerID,
discoveryInfo: nil,
serviceType: “uwb-distance”
)
mcAdvertiser?.delegate = self
mcAdvertiser?.startAdvertisingPeer()
print(“Started hosting successfully.”)
}
func startBrowsing() {
stopAdvertisingAndBrowsing() // Clean-up before starting
guard let peerID = self.peerID else {
print(“Error: peerID is nil in startBrowsing()”)
return
}
mcBrowser = MCNearbyServiceBrowser(
peer: peerID,
serviceType: “uwb-distance”
)
mcBrowser?.delegate = self
mcBrowser?.startBrowsingForPeers()
print(“Started browsing for peers.”)
}
func stopAdvertisingAndBrowsing() {
mcAdvertiser?.stopAdvertisingPeer()
mcAdvertiser = nil
mcBrowser?.stopBrowsingForPeers()
mcBrowser = nil
}
// MARK: - CBCentralManagerDelegate
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOn:
print(“Bluetooth is On”)
case .poweredOff:
print(“Bluetooth is Off”)
case .resetting:
print(“Bluetooth is Resetting”)
case .unauthorized:
print(“Bluetooth is Unauthorized”)
case .unsupported:
print(“Bluetooth is Unsupported”)
case .unknown:
print(“Bluetooth is Unknown”)
@unknown default:
fatalError(“Unknown Bluetooth state”)
}
}
}