# MultipeerConnectivity to Network.framework Transition Guide This document explains how to transition an iOS app from MultipeerConnectivity (MPC) to Network.framework for peer-to-peer networking. --- ## Why Transition? - **Network.framework** provides lower-level control over connections - Better performance with direct TCP/UDP streams - More flexible discovery options (can combine with BLE for faster discovery) - AWDL (Apple Wireless Direct Link) support via `includePeerToPeer = true` - Future-proof: Network.framework is Apple's modern networking API --- ## Architecture Overview ### MultipeerConnectivity Architecture ``` MCNearbyServiceAdvertiser → Advertises service MCNearbyServiceBrowser → Discovers services MCSession → Manages connections & data transfer MCPeerID → Identifies peers ``` ### Network.framework Architecture ``` NWListener → Advertises service (replaces MCNearbyServiceAdvertiser) NWBrowser → Discovers services (replaces MCNearbyServiceBrowser) NWConnection → Individual connection per peer (replaces MCSession) UUID / Custom struct → Identifies peers (replaces MCPeerID) ``` --- ## New Files to Create ### 1. Models **`DiscoveredHost.swift`** - Represents a discovered host ```swift import Foundation struct DiscoveredHost: Identifiable, Hashable { let id: UUID let deviceName: String let eventName: String let discoveredAt: Date let endpointID: String // Unique identifier from NWBrowser init(deviceName: String, eventName: String, endpointID: String) { self.id = UUID() self.deviceName = deviceName self.eventName = eventName self.discoveredAt = Date() self.endpointID = endpointID } func hash(into hasher: inout Hasher) { hasher.combine(endpointID) } static func == (lhs: DiscoveredHost, rhs: DiscoveredHost) -> Bool { lhs.endpointID == rhs.endpointID } } ``` **`ConnectedPeer.swift`** - Represents a connected peer ```swift import Foundation struct ConnectedPeer: Identifiable, Hashable { let id: UUID let displayName: String let connectedAt: Date var isStable: Bool // For traffic gate pattern init(displayName: String, isStable: Bool = false) { self.id = UUID() self.displayName = displayName self.connectedAt = Date() self.isStable = isStable } } enum P2PConnectionState { case idle case hosting case browsing case connecting case connected case disconnected case failed(Error) } ``` ### 2. NetworkFraming.swift - TCP Message Framing Network.framework uses raw TCP streams, so you need length-prefixed framing: ```swift import Foundation import Network enum NetworkFraming { static let headerSize = 4 // UInt32 for length static let maxMessageSize: UInt32 = 10_485_760 // 10 MB /// Frame data with 4-byte length prefix static func frameData(_ data: Data) -> Data { var length = UInt32(data.count).bigEndian var framedData = Data(bytes: &length, count: headerSize) framedData.append(data) return framedData } /// Send framed data over connection static func send(_ data: Data, over connection: NWConnection, completion: @escaping (Error?) -> Void) { let framedData = frameData(data) connection.send(content: framedData, completion: .contentProcessed { error in completion(error) }) } /// Receive framed data from connection static func receive(from connection: NWConnection, completion: @escaping (Result) -> Void) { // Read 4-byte header connection.receive(minimumIncompleteLength: headerSize, maximumLength: headerSize) { headerData, _, isComplete, error in if let error = error { completion(.failure(error)) return } guard let headerData = headerData, headerData.count == headerSize else { completion(.failure(FramingError.incompleteHeader)) return } let length = headerData.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian } guard length > 0, length <= maxMessageSize else { completion(.failure(FramingError.invalidMessageSize(length))) return } // Read payload connection.receive(minimumIncompleteLength: Int(length), maximumLength: Int(length)) { payloadData, _, _, error in if let error = error { completion(.failure(error)) return } guard let payloadData = payloadData else { completion(.failure(FramingError.incompletePayload)) return } completion(.success(payloadData)) } } } /// Continuous receive loop static func startReceiving(from connection: NWConnection, onData: @escaping (Data) -> Void, onError: @escaping (Error) -> Void) { receive(from: connection) { result in switch result { case .success(let data): onData(data) startReceiving(from: connection, onData: onData, onError: onError) case .failure(let error): onError(error) } } } } enum FramingError: LocalizedError { case incompleteHeader case incompletePayload case invalidMessageSize(UInt32) var errorDescription: String? { switch self { case .incompleteHeader: return "Incomplete header" case .incompletePayload: return "Incomplete payload" case .invalidMessageSize(let size): return "Invalid size: \(size)" } } } ``` ### 3. BLEDiscoveryManager.swift (Optional - For Fast Discovery) BLE advertisement wakes AWDL faster than Bonjour alone: ```swift import Foundation import CoreBluetooth import Combine @MainActor class BLEDiscoveryManager: NSObject, ObservableObject { @Published var isAdvertising = false @Published var isScanning = false var onHostDiscovered: ((String) -> Void)? // Use a valid UUID (hex characters only: 0-9, A-F) private let serviceUUID = CBUUID(string: "YOUR-UUID-HERE") private var peripheralManager: CBPeripheralManager? private var centralManager: CBCentralManager? private var eventName = "" private var discoveredPeripherals: Set = [] // MARK: - Host Mode (Peripheral) func startAdvertising(eventName: String) { self.eventName = eventName peripheralManager = CBPeripheralManager(delegate: self, queue: nil) } func stopAdvertising() { peripheralManager?.stopAdvertising() peripheralManager = nil isAdvertising = false } // MARK: - Guest Mode (Central) func startScanning() { discoveredPeripherals.removeAll() centralManager = CBCentralManager(delegate: self, queue: nil) } func stopScanning() { centralManager?.stopScan() centralManager = nil isScanning = false } } extension BLEDiscoveryManager: CBPeripheralManagerDelegate { nonisolated func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { Task { @MainActor in if peripheral.state == .poweredOn { let truncatedName = String(eventName.prefix(8)) // BLE has 28-byte limit peripheral.startAdvertising([ CBAdvertisementDataServiceUUIDsKey: [serviceUUID], CBAdvertisementDataLocalNameKey: truncatedName ]) isAdvertising = true } } } } extension BLEDiscoveryManager: CBCentralManagerDelegate { nonisolated func centralManagerDidUpdateState(_ central: CBCentralManager) { Task { @MainActor in if central.state == .poweredOn { central.scanForPeripherals(withServices: [serviceUUID]) isScanning = true } } } nonisolated func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi: NSNumber) { Task { @MainActor in guard !discoveredPeripherals.contains(peripheral.identifier) else { return } discoveredPeripherals.insert(peripheral.identifier) let hostName = advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? "Unknown" onHostDiscovered?(hostName) } } } ``` --- ## Key Implementation Patterns ### 1. Network Parameters Configuration ```swift private func createNetworkParameters() -> NWParameters { let parameters = NWParameters.tcp parameters.includePeerToPeer = true // Enables AWDL parameters.serviceClass = .responsiveData // Low-latency let options = NWProtocolTCP.Options() options.enableKeepalive = true options.keepaliveInterval = 30 parameters.defaultProtocolStack.transportProtocol = options return parameters } ``` ### 2. Host Mode - NWListener (replaces MCNearbyServiceAdvertiser) ```swift func startHosting(eventName: String) { let parameters = createNetworkParameters() listener = try NWListener(using: parameters) // Include device name in TXT record let deviceName = UIDevice.current.name let txtEntry = "deviceName=\(deviceName)" var txtData = Data() txtData.append(UInt8(txtEntry.utf8.count)) txtData.append(contentsOf: txtEntry.utf8) listener?.service = NWListener.Service( name: eventName, type: "_yourservice._tcp", txtRecord: txtData ) listener?.stateUpdateHandler = { [weak self] state in guard let self else { return } Task { @MainActor in self.handleListenerState(state) } } listener?.newConnectionHandler = { [weak self] connection in guard let self else { return } Task { @MainActor in self.handleNewConnection(connection) } } listener?.start(queue: .main) } ``` ### 3. Guest Mode - NWBrowser (replaces MCNearbyServiceBrowser) ```swift func startBrowsing() { let parameters = createNetworkParameters() browser = NWBrowser(for: .bonjour(type: "_yourservice._tcp", domain: nil), using: parameters) browser?.browseResultsChangedHandler = { [weak self] results, changes in guard let self else { return } Task { @MainActor in for change in changes { switch change { case .added(let result): self.handleHostFound(result) case .removed(let result): self.handleHostLost(result) default: break } } } } browser?.start(queue: .main) } private func handleHostFound(_ result: NWBrowser.Result) { guard case .service(let name, let type, let domain, _) = result.endpoint else { return } let endpointID = "\(name).\(type).\(domain)" // Extract device name from TXT record var deviceName = "Unknown Device" if case .bonjour(let txtRecord) = result.metadata { if let name = txtRecord["deviceName"] { deviceName = name } } let host = DiscoveredHost(deviceName: deviceName, eventName: name, endpointID: endpointID) availableHosts[endpointID] = host } ``` ### 4. Connecting to Host (replaces invitePeer) ```swift func joinHost(_ host: DiscoveredHost) { let endpoint = NWEndpoint.service( name: host.eventName, type: "_yourservice._tcp", domain: "local", interface: nil ) let parameters = createNetworkParameters() hostConnection = NWConnection(to: endpoint, using: parameters) hostConnection?.stateUpdateHandler = { [weak self] state in guard let self else { return } Task { @MainActor in self.handleConnectionState(state) } } hostConnection?.start(queue: .main) } ``` ### 5. Handling New Connections (Host Side) ```swift private func handleNewConnection(_ connection: NWConnection) { let peerID = UUID() guestConnections[peerID] = connection connectedPeers[peerID] = ConnectedPeer(displayName: "Guest", isStable: false) connection.stateUpdateHandler = { [weak self] state in guard let self else { return } Task { @MainActor in // Handle state changes } } connection.start(queue: .main) // Start receiving data NetworkFraming.startReceiving(from: connection) { [weak self] data in // Handle received data } onError: { [weak self] error in // Handle error, remove connection } // Traffic Gate: Wait before marking as stable Task { @MainActor in try? await Task.sleep(nanoseconds: 4_000_000_000) // 4 seconds guard connectedPeers[peerID] != nil else { return } stablePeers.insert(peerID) connectedPeers[peerID]?.isStable = true onPeerJoined?(peerID) } } ``` ### 6. Broadcasting Data (replaces MCSession.send) ```swift func broadcastData(_ data: Data) { // Traffic Gate: Only send to stable peers let stableConnections = guestConnections.filter { stablePeers.contains($0.key) } for (_, connection) in stableConnections { NetworkFraming.send(data, over: connection) { error in if let error = error { print("Send failed: \(error)") } } } } ``` --- ## Callback Mapping | MultipeerConnectivity | Network.framework | |----------------------|-------------------| | `session(_:peer:didChange:)` | `connection.stateUpdateHandler` | | `session(_:didReceive:fromPeer:)` | `NetworkFraming.startReceiving()` | | `advertiser(_:didReceiveInvitationFromPeer:)` | `listener.newConnectionHandler` | | `browser(_:foundPeer:withDiscoveryInfo:)` | `browser.browseResultsChangedHandler` (.added) | | `browser(_:lostPeer:)` | `browser.browseResultsChangedHandler` (.removed) | --- ## Info.plist Updates Add these keys for BLE discovery (if using): ```xml UIBackgroundModes bluetooth-central bluetooth-peripheral NSBluetoothAlwaysUsageDescription Uses Bluetooth to quickly discover nearby sessions. NSBluetoothPeripheralUsageDescription Uses Bluetooth to advertise sessions to nearby devices. NSLocalNetworkUsageDescription Uses local network for peer-to-peer connections. ``` Keep existing Bonjour services: ```xml NSBonjourServices _yourservice._tcp ``` --- ## Important Considerations ### 1. Traffic Gate Pattern Keep the stabilization delay (4 seconds) before marking peers as stable. This prevents data loss on mixed 5G+WiFi networks. ### 2. Connection Retry Logic Implement exponential backoff (1s, 2s, 4s) for connection retries, just like with MPC. ### 3. Swift 6 Concurrency Use `guard let self else { return }` pattern before `Task` blocks to avoid Swift 6 warnings: ```swift // Good pattern connection.stateUpdateHandler = { [weak self] state in guard let self else { return } Task { @MainActor in self.handleState(state) } } ``` ### 4. Reconnection Handling Add an `onHostConnected` callback to clear "disconnected" UI states when reconnecting: ```swift var onHostConnected: (() -> Void)? // In connection success handler: isConnectedToHost = true onHostConnected?() // Clears "host lost" banners ``` ### 5. Device Name in TXT Record Network.framework doesn't expose device names like MPC's `MCPeerID.displayName`. Include it in the TXT record: ```swift // Host: Add to TXT record let txtEntry = "deviceName=\(UIDevice.current.name)" // Guest: Extract from metadata if case .bonjour(let txtRecord) = result.metadata { if let deviceName = txtRecord["deviceName"] { // Use deviceName } } ``` --- ## Migration Checklist - [ ] Create `DiscoveredHost` and `ConnectedPeer` models - [ ] Create `NetworkFraming` utility for TCP framing - [ ] Create `P2PConnectionManager` with NWListener/NWBrowser - [ ] (Optional) Create `BLEDiscoveryManager` for fast discovery - [ ] Update Info.plist with BLE permissions (if using BLE) - [ ] Update all views to use new manager - [ ] Replace `MCPeerID` with `UUID` or custom struct - [ ] Implement traffic gate pattern (4-second stabilization) - [ ] Implement connection retry with exponential backoff - [ ] Add `onHostConnected` callback for reconnection handling - [ ] Test: Host starts, guest joins - [ ] Test: Multiple guests join simultaneously - [ ] Test: Guest leaves and rejoins - [ ] Test: Host disconnects, guests receive notification - [ ] Keep old `MultipeerManager` as fallback initially --- ## File Structure ``` YourApp/ ├── Managers/ │ ├── MultipeerManager.swift (keep as fallback) │ ├── P2PConnectionManager.swift (NEW - main networking) │ ├── BLEDiscoveryManager.swift (NEW - optional fast discovery) │ └── NetworkFraming.swift (NEW - TCP framing) ├── Models/ │ ├── DiscoveredHost.swift (NEW) │ └── ConnectedPeer.swift (NEW) ``` --- ## Full Mesh Network (Up to 8 Devices) For apps where all devices need to communicate with each other (like a group chat or multiplayer game), you need a **full mesh topology** where every device connects to every other device. ### Mesh Network Architecture ``` Device A / | \ / | \ Device B----Device C \ | / \ | / Device D ``` In a full mesh with N devices, each device maintains N-1 connections. ### Key Differences from Host/Guest Model | Host/Guest Model | Full Mesh Model | |------------------|-----------------| | One host, multiple guests | All peers are equal | | Host runs NWListener only | Every device runs NWListener AND NWBrowser | | Guests run NWBrowser only | Every device can initiate or accept connections | | One-to-many communication | Many-to-many communication | ### MeshPeer Model ```swift import Foundation struct MeshPeer: Identifiable, Hashable { let id: String // Unique device identifier (persisted across sessions) let displayName: String let discoveredAt: Date var isConnected: Bool var isStable: Bool init(id: String, displayName: String) { self.id = id self.displayName = displayName self.discoveredAt = Date() self.isConnected = false self.isStable = false } func hash(into hasher: inout Hasher) { hasher.combine(id) } static func == (lhs: MeshPeer, rhs: MeshPeer) -> Bool { lhs.id == rhs.id } } ``` ### MeshNetworkManager ```swift import Foundation import Network import UIKit import Combine @MainActor class MeshNetworkManager: NSObject, ObservableObject { // MARK: - Published Properties @Published var discoveredPeers: [String: MeshPeer] = [:] @Published var connectedPeers: [String: MeshPeer] = [:] @Published var isRunning = false // MARK: - Callbacks var onMessageReceived: ((Data, MeshPeer) -> Void)? var onPeerConnected: ((MeshPeer) -> Void)? var onPeerDisconnected: ((MeshPeer) -> Void)? // MARK: - Private Properties private let serviceType = "_yourmesh._tcp" private let myPeerID: String // Unique, persistent device ID private let myDisplayName: String private var listener: NWListener? private var browser: NWBrowser? private var connections: [String: NWConnection] = [:] // peerID -> connection private var stablePeers: Set = [] private var pendingConnections: Set = [] // Peers we're connecting to // MARK: - Initialization override init() { // Create or retrieve persistent device ID if let savedID = UserDefaults.standard.string(forKey: "meshPeerID") { self.myPeerID = savedID } else { let newID = UUID().uuidString UserDefaults.standard.set(newID, forKey: "meshPeerID") self.myPeerID = newID } self.myDisplayName = UIDevice.current.name super.init() } // MARK: - Network Parameters private func createParameters() -> NWParameters { let parameters = NWParameters.tcp parameters.includePeerToPeer = true parameters.serviceClass = .responsiveData return parameters } // MARK: - Start/Stop Mesh func startMesh() { guard !isRunning else { return } startListener() startBrowser() isRunning = true print("Mesh started with ID: \(myPeerID)") } func stopMesh() { listener?.cancel() listener = nil browser?.cancel() browser = nil for (_, connection) in connections { connection.cancel() } connections.removeAll() connectedPeers.removeAll() discoveredPeers.removeAll() stablePeers.removeAll() pendingConnections.removeAll() isRunning = false } // MARK: - Listener (Accept Incoming Connections) private func startListener() { do { let parameters = createParameters() listener = try NWListener(using: parameters) // Advertise with our peer ID and display name in TXT record let txtEntries = "peerID=\(myPeerID)\tdisplayName=\(myDisplayName)" var txtData = Data() for entry in txtEntries.split(separator: "\t") { let entryString = String(entry) txtData.append(UInt8(entryString.utf8.count)) txtData.append(contentsOf: entryString.utf8) } listener?.service = NWListener.Service( name: myPeerID, // Use peer ID as service name for uniqueness type: serviceType, txtRecord: txtData ) listener?.stateUpdateHandler = { [weak self] state in guard let self else { return } Task { @MainActor in if case .failed(let error) = state { print("Listener failed: \(error)") } } } listener?.newConnectionHandler = { [weak self] connection in guard let self else { return } Task { @MainActor in self.handleIncomingConnection(connection) } } listener?.start(queue: .main) } catch { print("Failed to start listener: \(error)") } } // MARK: - Browser (Discover Other Peers) private func startBrowser() { let parameters = createParameters() browser = NWBrowser(for: .bonjour(type: serviceType, domain: nil), using: parameters) browser?.browseResultsChangedHandler = { [weak self] results, changes in guard let self else { return } Task { @MainActor in self.handleBrowseResults(changes: changes) } } browser?.start(queue: .main) } private func handleBrowseResults(changes: Set) { for change in changes { switch change { case .added(let result): handlePeerDiscovered(result) case .removed(let result): handlePeerLost(result) default: break } } } private func handlePeerDiscovered(_ result: NWBrowser.Result) { // Extract peer info from TXT record var peerID: String? var displayName = "Unknown" if case .bonjour(let txtRecord) = result.metadata { peerID = txtRecord["peerID"] displayName = txtRecord["displayName"] ?? "Unknown" } guard let peerID = peerID, peerID != myPeerID else { return } // Ignore self // Already connected? guard connections[peerID] == nil else { return } let peer = MeshPeer(id: peerID, displayName: displayName) discoveredPeers[peerID] = peer print("Discovered peer: \(displayName) (\(peerID))") // Decide who initiates connection using tie-breaker // Lower peer ID initiates to avoid duplicate connections if myPeerID < peerID && !pendingConnections.contains(peerID) { initiateConnection(to: peer, result: result) } } private func handlePeerLost(_ result: NWBrowser.Result) { if case .service(let name, _, _, _) = result.endpoint { // Service name is the peer ID discoveredPeers.removeValue(forKey: name) } } // MARK: - Connection Management /// Initiate outgoing connection (we have lower ID) private func initiateConnection(to peer: MeshPeer, result: NWBrowser.Result) { pendingConnections.insert(peer.id) let parameters = createParameters() let connection = NWConnection(to: result.endpoint, using: parameters) connection.stateUpdateHandler = { [weak self] state in guard let self else { return } Task { @MainActor in self.handleConnectionState(state, peerID: peer.id, isOutgoing: true) } } connection.start(queue: .main) connections[peer.id] = connection print("Initiating connection to: \(peer.displayName)") } /// Handle incoming connection (other peer had lower ID) private func handleIncomingConnection(_ connection: NWConnection) { // We need to identify who this connection is from // Send our peer ID immediately after connection is ready connection.stateUpdateHandler = { [weak self] state in guard let self else { return } Task { @MainActor in self.handleIncomingConnectionState(state, connection: connection) } } connection.start(queue: .main) } private func handleIncomingConnectionState(_ state: NWConnection.State, connection: NWConnection) { switch state { case .ready: // Wait for the remote peer to identify themselves receiveIdentification(from: connection) case .failed, .cancelled: connection.cancel() default: break } } private func receiveIdentification(from connection: NWConnection) { NetworkFraming.receive(from: connection) { [weak self] result in guard let self else { return } Task { @MainActor in switch result { case .success(let data): if let message = try? JSONDecoder().decode(MeshMessage.self, from: data), case .identification(let peerID, let displayName) = message.type { self.finalizeIncomingConnection(connection, peerID: peerID, displayName: displayName) } case .failure(let error): print("Failed to receive identification: \(error)") connection.cancel() } } } } private func finalizeIncomingConnection(_ connection: NWConnection, peerID: String, displayName: String) { // Check if we already have a connection to this peer if let existing = connections[peerID] { // Keep the connection from the lower ID peer if myPeerID < peerID { // We should have initiated, reject this incoming connection.cancel() return } else { // They should have initiated, close our outgoing attempt existing.cancel() } } connections[peerID] = connection var peer = MeshPeer(id: peerID, displayName: displayName) peer.isConnected = true connectedPeers[peerID] = peer discoveredPeers.removeValue(forKey: peerID) // Start receiving messages startReceiving(from: connection, peerID: peerID) // Start stabilization startStabilization(for: peerID) print("Incoming connection from: \(displayName)") } private func handleConnectionState(_ state: NWConnection.State, peerID: String, isOutgoing: Bool) { switch state { case .ready: if isOutgoing { // Send identification sendIdentification(to: peerID) var peer = discoveredPeers[peerID] ?? MeshPeer(id: peerID, displayName: "Unknown") peer.isConnected = true connectedPeers[peerID] = peer discoveredPeers.removeValue(forKey: peerID) pendingConnections.remove(peerID) // Start receiving if let connection = connections[peerID] { startReceiving(from: connection, peerID: peerID) } // Start stabilization startStabilization(for: peerID) print("Connected to: \(peer.displayName)") } case .failed(let error): print("Connection failed to \(peerID): \(error)") removeConnection(peerID: peerID) case .cancelled: removeConnection(peerID: peerID) default: break } } private func sendIdentification(to peerID: String) { guard let connection = connections[peerID] else { return } let message = MeshMessage( type: .identification(peerID: myPeerID, displayName: myDisplayName), senderID: myPeerID ) if let data = try? JSONEncoder().encode(message) { NetworkFraming.send(data, over: connection) { error in if let error = error { print("Failed to send identification: \(error)") } } } } private func startReceiving(from connection: NWConnection, peerID: String) { NetworkFraming.startReceiving(from: connection) { [weak self] data in guard let self else { return } Task { @MainActor in self.handleReceivedData(data, from: peerID) } } onError: { [weak self] error in guard let self else { return } Task { @MainActor in print("Receive error from \(peerID): \(error)") self.removeConnection(peerID: peerID) } } } private func startStabilization(for peerID: String) { Task { @MainActor in try? await Task.sleep(nanoseconds: 4_000_000_000) // 4 seconds guard connectedPeers[peerID] != nil else { return } stablePeers.insert(peerID) connectedPeers[peerID]?.isStable = true if let peer = connectedPeers[peerID] { onPeerConnected?(peer) } print("Peer stabilized: \(peerID)") } } private func removeConnection(peerID: String) { connections[peerID]?.cancel() connections.removeValue(forKey: peerID) pendingConnections.remove(peerID) stablePeers.remove(peerID) if let peer = connectedPeers.removeValue(forKey: peerID) { onPeerDisconnected?(peer) } } // MARK: - Message Handling private func handleReceivedData(_ data: Data, from peerID: String) { guard let message = try? JSONDecoder().decode(MeshMessage.self, from: data) else { return } switch message.type { case .identification: // Already handled during connection setup break case .broadcast(let payload): if let peer = connectedPeers[peerID] { onMessageReceived?(payload, peer) } case .direct(let payload): if let peer = connectedPeers[peerID] { onMessageReceived?(payload, peer) } } } // MARK: - Sending Messages /// Send to all connected peers func broadcast(_ data: Data) { let message = MeshMessage(type: .broadcast(payload: data), senderID: myPeerID) guard let encoded = try? JSONEncoder().encode(message) else { return } for peerID in stablePeers { if let connection = connections[peerID] { NetworkFraming.send(encoded, over: connection) { error in if let error = error { print("Broadcast failed to \(peerID): \(error)") } } } } } /// Send to specific peer func send(_ data: Data, to peerID: String) { guard stablePeers.contains(peerID), let connection = connections[peerID] else { return } let message = MeshMessage(type: .direct(payload: data), senderID: myPeerID) guard let encoded = try? JSONEncoder().encode(message) else { return } NetworkFraming.send(encoded, over: connection) { error in if let error = error { print("Send failed to \(peerID): \(error)") } } } /// Send to all except specified peers func broadcast(_ data: Data, excluding: Set) { let message = MeshMessage(type: .broadcast(payload: data), senderID: myPeerID) guard let encoded = try? JSONEncoder().encode(message) else { return } for peerID in stablePeers where !excluding.contains(peerID) { if let connection = connections[peerID] { NetworkFraming.send(encoded, over: connection) { error in if let error = error { print("Broadcast failed to \(peerID): \(error)") } } } } } } ``` ### MeshMessage Protocol ```swift import Foundation struct MeshMessage: Codable { enum MessageType: Codable { case identification(peerID: String, displayName: String) case broadcast(payload: Data) case direct(payload: Data) } let type: MessageType let senderID: String let timestamp: Date init(type: MessageType, senderID: String) { self.type = type self.senderID = senderID self.timestamp = Date() } } ``` ### Usage Example ```swift struct MeshChatView: View { @StateObject private var mesh = MeshNetworkManager() @State private var messages: [(String, MeshPeer)] = [] @State private var inputText = "" var body: some View { VStack { // Connected peers HStack { Text("Connected: \(mesh.connectedPeers.count)") ForEach(Array(mesh.connectedPeers.values)) { peer in Text(peer.displayName) .padding(4) .background(peer.isStable ? Color.green : Color.orange) .cornerRadius(4) } } // Messages List(messages, id: \.0) { message, peer in Text("\(peer.displayName): \(message)") } // Input HStack { TextField("Message", text: $inputText) Button("Send") { if let data = inputText.data(using: .utf8) { mesh.broadcast(data) inputText = "" } } } } .onAppear { mesh.onMessageReceived = { data, peer in if let text = String(data: data, encoding: .utf8) { messages.append((text, peer)) } } mesh.startMesh() } .onDisappear { mesh.stopMesh() } } } ``` ### Connection Tie-Breaker Strategy To avoid duplicate connections (both peers trying to connect to each other), use a **deterministic tie-breaker**: ```swift // Lower peer ID initiates the connection if myPeerID < discoveredPeerID { initiateConnection(to: peer) } else { // Wait for them to connect to us } ``` This ensures: - Only ONE connection is created between any two peers - No race conditions or duplicate connections - Deterministic behavior across all devices ### Mesh Network Limits | Devices | Connections per Device | Total Connections | |---------|----------------------|-------------------| | 2 | 1 | 1 | | 3 | 2 | 3 | | 4 | 3 | 6 | | 5 | 4 | 10 | | 6 | 5 | 15 | | 7 | 6 | 21 | | 8 | 7 | 28 | Formula: Total connections = N × (N-1) / 2 ### Mesh Network Considerations 1. **Connection Overhead**: Each device maintains N-1 connections. With 8 devices, that's 7 connections per device and 28 total connections in the network. 2. **Message Duplication**: When broadcasting, each peer sends to all others. Ensure your app handles potential duplicate messages (use message IDs). 3. **Partial Mesh**: For larger groups, consider a **partial mesh** where not everyone connects to everyone. Use relay nodes instead. 4. **Battery Impact**: More connections = more battery usage. Consider reducing broadcast frequency or implementing sleep modes. 5. **Network Partition**: Handle cases where the mesh splits (some devices can't reach others). Implement reconnection logic. 6. **Message Ordering**: Messages may arrive out of order. Use timestamps or sequence numbers if order matters. ### Message Relay (Optional) For messages that need to reach peers you're not directly connected to: ```swift func relayBroadcast(_ data: Data, originalSenderID: String, seenBy: Set) { var updatedSeenBy = seenBy updatedSeenBy.insert(myPeerID) let message = MeshMessage( type: .relayedBroadcast(payload: data, originalSender: originalSenderID, seenBy: updatedSeenBy), senderID: myPeerID ) guard let encoded = try? JSONEncoder().encode(message) else { return } // Forward to peers who haven't seen it for peerID in stablePeers where !updatedSeenBy.contains(peerID) { if let connection = connections[peerID] { NetworkFraming.send(encoded, over: connection) { _ in } } } } ``` This allows messages to propagate through the entire mesh even if not all devices are directly connected.