import Combine import Foundation import MultipeerConnectivity struct NoteInvitation: Identifiable { struct NoteContent: Codable { let title: String let noteSnapshot: String } var id: MCPeerID { invitatorID } var noteName: String { note.title } let invitatorID: MCPeerID let note: NoteContent private var invitationHandler: ((Bool) -> Void)? init( invitatorID: MCPeerID, note: NoteContent, invitationHandler: ((Bool) -> Void)? = nil ) { self.invitatorID = invitatorID self.note = note self.invitationHandler = invitationHandler } mutating func accept() { invitationHandler?(true) invitationHandler = nil } mutating func decline() { invitationHandler?(false) invitationHandler = nil } } @Observable final class NoteEditingSessionClient: NSObject { private let session: MCSession private let advertiser: MCNearbyServiceAdvertiser private(set) var ownPeer: MCPeerID var invitations: [NoteInvitation] = [] let noteChangesEmitter = PassthroughSubject() init(peer: MCPeerID) { ownPeer = peer session = MCSession( peer: peer, securityIdentity: nil, encryptionPreference: .required ) advertiser = MCNearbyServiceAdvertiser( peer: peer, discoveryInfo: [:], serviceType: "peered" ) super.init() advertiser.delegate = self session.delegate = self } func startBrowsingForNotes() { advertiser.startAdvertisingPeer() } func stopBrowsingForNotes() { advertiser.stopAdvertisingPeer() session.disconnect() } func send(note: String, to peer: MCPeerID) { let message = NoteMessage(senderID: ownPeer.displayName, content: note) guard let data = try? JSONEncoder().encode(message) else { return } try? session.send(data, toPeers: [peer], with: .reliable) } } extension NoteEditingSessionClient: MCNearbyServiceAdvertiserDelegate { func advertiser( _ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void ) { guard let context, let noteContent = try? JSONDecoder().decode(NoteInvitation.NoteContent.self, from: context) else { return } DispatchQueue.main.async { self.invitations.append( .init( invitatorID: peerID, note: noteContent, invitationHandler: { [weak self, invitationHandler] accepted in guard let self else { return } invitationHandler(accepted, self.session) DispatchQueue.main.async { self.invitations.removeAll { $0.id == peerID } } } ) ) } } } extension NoteEditingSessionClient: MCSessionDelegate { func session( _ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState ) {} func session( _ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID ) { guard let message = try? JSONDecoder().decode(NoteMessage.self, from: data) else { return } 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)? ) {} }