Multipeer Connectivity Tutorial

von @ralfebert · aktualisiert am 3. November 2021
Xcode 13 & iOS 15
Fortgeschrittene iOS-Entwickler*innen
Deutsch

Dieses Tutorial zeigt, wie du das Multipeer Connectivity Framework für die Kommunikation zwischen iOS-Geräten nutzen kannst. Das Beispiel zeigt, wie man die Beispiel-App ConnectedColors implementiert, die einen Farbwert zwischen mehreren Geräten synchronisiert.

Das Multipeer Connectivity-Framework stellt eine Abstraktion auf der Grundlage des Bonjour-Protokolls bereit. Intern verwendet das Framework automatisch eine geeignete Netzwerktechnologie - Wi-Fi, wenn sich beide Geräte im selben Wi-Fi-Netzwerk befinden, Peer-to-Peer Wi-Fi oder Bluetooth.

Dies ist ein Tutorial für fortgeschrittene iOS-Entwickler*innen. Es erfordert praktische Swift-Programmierkenntnisse und gute iOS-Entwicklungskenntnisse.

Projekt einrichten

  1. Verwende das neueste Xcode (dieses Tutorial wurde zuletzt mit Xcode 13 am 31. Oktober 2021 getestet).

  2. Erstelle eine neue iOS App ConnectedColors, die auf SwiftUI basiert.

Dienst bekannt machen und nach dem Dienst suchen

  1. Erstelle eine neue Klasse ColorMultipeerSession für den gesamten Connectivity-Code.

  2. Verwende den folgenden Code als Ausgangspunkt - er enthält nur den Boilerplate-Code für die Delegates – der Code macht lediglich den Dienst bekannt und alle protokolliert alle Ereignisse.

    Der serviceType identifiziert den Dienst (es muss eine eindeutige Zeichenfolge sein, die maximal 15 Zeichen lang ist und nur ASCII-Kleinbuchstaben, Zahlen und Bindestriche enthalten darf)

    Die ↗ MCPeerID identifiziert das Gerät eindeutig; der verwendete displayName ist für andere Geräte sichtbar.

    Die ↗ MCNearbyServiceAdvertiser macht den Dienst bekannt.

    Der MCNearbyServiceBrowser sucht nach dem Dienst im Netzwerk.

    Die MCSession verwaltet alle verbundenen Geräte (peers) und ermöglicht das Senden und Empfangen von Nachrichten.

    import MultipeerConnectivity
    import os
    
    class ColorMultipeerSession: NSObject, ObservableObject {
        private let serviceType = "example-color"
    private let myPeerId = MCPeerID(displayName: UIDevice.current.name)
    private let serviceAdvertiser: MCNearbyServiceAdvertiser
    private let serviceBrowser: MCNearbyServiceBrowser
    private let session: MCSession
        private let log = Logger()
    
        override init() {
            session = MCSession(peer: myPeerId, securityIdentity: nil, encryptionPreference: .none)
            serviceAdvertiser = MCNearbyServiceAdvertiser(peer: myPeerId, discoveryInfo: nil, serviceType: serviceType)
            serviceBrowser = MCNearbyServiceBrowser(peer: myPeerId, serviceType: serviceType)
    
            super.init()
    
            session.delegate = self
            serviceAdvertiser.delegate = self
            serviceBrowser.delegate = self
    
            serviceAdvertiser.startAdvertisingPeer()
            serviceBrowser.startBrowsingForPeers()
        }
    
        deinit {
            serviceAdvertiser.stopAdvertisingPeer()
            serviceBrowser.stopBrowsingForPeers()
        }
    }
    
    extension ColorMultipeerSession: MCNearbyServiceAdvertiserDelegate {
        func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error) {
            log.error("ServiceAdvertiser didNotStartAdvertisingPeer: \(String(describing: error))")
        }
    
        func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
            log.info("didReceiveInvitationFromPeer \(peerID)")
        }
    }
    
    extension ColorMultipeerSession: MCNearbyServiceBrowserDelegate {
        func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error) {
            log.error("ServiceBrowser didNotStartBrowsingForPeers: \(String(describing: error))")
        }
    
        func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) {
            log.info("ServiceBrowser found peer: \(peerID)")
        }
    
        func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
            log.info("ServiceBrowser lost peer: \(peerID)")
        }
    }
    
    extension ColorMultipeerSession: MCSessionDelegate {
        func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
            log.info("peer \(peerID) didChangeState: \(state.rawValue)")
        }
    
        func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
            log.info("didReceive bytes \(data.count) bytes")
        }
    
        public func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
            log.error("Receiving streams is not supported")
        }
    
        public func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
            log.error("Receiving resources is not supported")
        }
    
        public func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
            log.error("Receiving resources is not supported")
        }
    }
    
  3. Erstelle eine ColorMultipeerSession-Instanz als @StateObject im ContentView:

    class ContentView: View {
        @StateObject var colorSession = ColorMultipeerSession()
    
        // ...
    
    }
    
  4. Füge in der Info.plist den Schlüssel Bonjour services mit den folgenden beiden Werten hinzu (der Name des Dienstes muss übereinstimmen!)

  5. Starte die App (Simulator oder Gerät).

  6. Überprüfe optional, ob der Dienst im lokalen Netzwerk korrekt bekannt gemacht wird: → Listing all Bonjour services on the local network

Einladen und Empfangen von Einladungen zwischen Geräten

  1. Wenn ein anderes Gerät entdeckt wird, sende eine Einladung. Wenn eine Einladung eingeht, nimm sie an. Implementiere das ObservableObject-Protokoll und füge eine @Published-Eigenschaft mit einer Liste der verbundenen Geräte hinzu. Aktualisiere sie, wenn sich der Status eines anderen Geräts ändert:

    class ColorMultipeerSession: NSObject, ObservableObject {
        // ...
    
        @Published var connectedPeers: [MCPeerID] = []
    }
    
    extension ColorMultipeerSession: MCNearbyServiceBrowserDelegate {
    
        func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) {
            log.info("ServiceBrowser found peer: \(peerID)")
            browser.invitePeer(peerID, to: session, withContext: nil, timeout: 10)
        }
    
        // ...
    
    }
    
    extension ColorMultipeerSession: MCNearbyServiceAdvertiserDelegate {
    
        func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
            log.info("didReceiveInvitationFromPeer \(peerID)")
            invitationHandler(true, session)
        }
    
        // ...
    
    }
    
    extension ColorMultipeerSession: MCSessionDelegate {
    
        func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
            log.info("peer \(peerID) didChangeState: \(state.rawValue)")
            DispatchQueue.main.async {
        connectedPeers = session.connectedPeers
    }
        }
    
        // ...
    }
    

    Hinweis: Dieser Code lädt jeden Peer automatisch ein. Die MCBrowserViewController Klasse kann verwendet werden, um nach Peers zu suchen und sie manuell einzuladen.

    Außerdem nimmt dieser Code alle eingehenden Verbindungen automatisch an. Das wäre wie ein öffentlicher Chat; es ist darauf zu achten, dass alle empfangenen Daten geprüft und bereinigt werden, da man ggf. nicht jedem Gerät vertrauen kann. Um die Sitzungen privat zu halten, sollte der Nutzer benachrichtigt und aufgefordert werden, eingehende Verbindungen zu bestätigen. Dies lässt sich mit der MCAdvertiserAssistant-Klasse bewerkstelligen.

Senden und Empfangen von Farbwerten

  1. Füge eine Eigenschaft currentColor hinzu und aktualisiere sie, wenn eine Nachricht empfangen wird. Biete eine Methode an, um einen neuen Farbwert zu senden. Wenn einen Wert vom MCSessionDelegate eingeht, dekodiere ihn und aktualisiere die Eigenschaft currentColor. Das folgende Beispiel verwendet ein enum zum Kodieren und Dekodieren der Werte:

    enum NamedColor: String, CaseIterable {
        case red, green, yellow
    }
    
    class ColorMultipeerSession: NSObject, ObservableObject {
    
        // ...
    
        @Published var currentColor: NamedColor? = nil
    
        /// ...
    
        func send(color: NamedColor) {
        log.info("sendColor: \(String(describing: color)) to \(self.session.connectedPeers.count) peers")
        self.currentColor = color
    
        if !session.connectedPeers.isEmpty {
            do {
                try session.send(color.rawValue.data(using: .utf8)!, toPeers: session.connectedPeers, with: .reliable)
            } catch {
                log.error("Error for sending: \(String(describing: error))")
            }
        }
    }
    
    }
    
    extension ColorMultipeerSession: MCSessionDelegate {
    
        // ...
    
        func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
        if let string = String(data: data, encoding: .utf8), let color = NamedColor(rawValue: string) {
            log.info("didReceive color \(string)")
            DispatchQueue.main.async {
                self.currentColor = color
            }
        } else {
            log.info("didReceive invalid value \(data.count) bytes")
        }
    }
    }
    

UI-Aktualisierung

  1. Implementiere ein View, das die Namen der aktuell verbundenen Geräte anzeigt, die aktuelle Farbe als Hintergrund verwendet und es ermöglicht, Werte zu senden:

    struct ContentView: View {
        @StateObject var colorSession = ColorMultipeerSession()
    
        var body: some View {
            VStack(alignment: .leading) {
                Text("Connected Devices:")
                Text(String(describing: colorSession.connectedPeers.map(\.displayName)))
    
                Divider()
    
                HStack {
                    ForEach(NamedColor.allCases, id: \.self) { color in
                        Button(color.rawValue) {
                            colorSession.send(color: color)
                        }
                        .padding()
                    }
                }
                Spacer()
            }
            .padding()
            .background((colorSession.currentColor.map(\.color) ?? .clear).ignoresSafeArea())
        }
    }
    
    extension NamedColor {
        var color: Color {
            switch self {
            case .red:
                return .red
            case .green:
                return .green
            case .yellow:
                return .yellow
            }
        }
    }
    
  2. Führe die aktualisierte App auf zwei Geräten aus und teste die Verbindung.

Weitere Informationen