import Combine import Foundation import MultipeerConnectivity struct OwnPeer { let peer: MCPeerID } struct Peer: Identifiable { enum ConnectionState { case available case joined case rejected case invitationPending } var id: String { mcPeer.displayName } let mcPeer: MCPeerID var state: ConnectionState } @Observable final class NoteEditingSessionServer: NSObject { private let session: MCSession private let browser: MCNearbyServiceBrowser private(set) var ownPeer: OwnPeer var visiblePeers: [Peer] = [] let noteChangesEmitter = PassthroughSubject() init(peer: OwnPeer) { ownPeer = peer browser = .init(peer: peer.peer, serviceType: "peered") session = .init(peer: peer.peer, securityIdentity: nil, encryptionPreference: .required) super.init() browser.delegate = self session.delegate = self } func startServer() { browser.startBrowsingForPeers() } func stopServer() { browser.stopBrowsingForPeers() session.disconnect() } func invite(peer: Peer, to note: NoteInvitation.NoteContent) { guard peer.state == .available, let note = try? JSONEncoder().encode(note) else { return } browser.invitePeer( peer.mcPeer, to: session, withContext: note, timeout: 600 ) guard let idxToUpdate = visiblePeers.firstIndex(where: { $0.mcPeer == peer.mcPeer }) else { return } visiblePeers[idxToUpdate].state = .invitationPending } func send(note: String, to peers: [MCPeerID]) { let message = NoteMessage(senderID: ownPeer.peer.displayName, content: note) guard !peers.isEmpty, let data = try? JSONEncoder().encode(message) else { return } try? session.send(data, toPeers: peers, with: .reliable) } } extension NoteEditingSessionServer: MCNearbyServiceBrowserDelegate { func browser( _ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]? ) { guard !visiblePeers.contains(where: { $0.mcPeer == peerID }) && peerID.displayName != ownPeer.peer.displayName else { return } DispatchQueue.main.async { self.visiblePeers.append(Peer(mcPeer: peerID, state: .available)) } } func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { DispatchQueue.main.async { guard let peerIdx = self.visiblePeers.firstIndex(where: { $0.mcPeer == peerID }) else { return } self.visiblePeers.remove(at: peerIdx) } } } extension NoteEditingSessionServer: MCSessionDelegate { func session( _ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState ) { DispatchQueue.main.async { guard let idx = self.visiblePeers.firstIndex(where: { $0.mcPeer == peerID }) else { return } switch state { case .connected: self.visiblePeers[idx].state = .joined case .notConnected: let currentState = self.visiblePeers[idx].state if currentState == .invitationPending || currentState == .joined { self.visiblePeers[idx].state = .rejected } default: break } } } func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { guard let message = try? JSONDecoder().decode(NoteMessage.self, from: data) else { return } let otherPeers = session.connectedPeers.filter { $0 != peerID } if !otherPeers.isEmpty { try? session.send(data, toPeers: otherPeers, with: .reliable) } DispatchQueue.main.async { self.noteChangesEmitter.send(message) } } 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: (any Error)? ) {} }