IoT-Tutorials

iOS MQTT Swift Client mit CocoaMQTT und SwiftUI

Schritt-für-Schritt-Tutorial zu iOS MQTT mit Swift: CocoaMQTT per Swift Package Manager einbinden, in ein ObservableObject kapseln, Live-Sensordaten in SwiftUI anzeigen und Befehle über TLS senden.

iOS MQTT Swift App auf einem iPhone mit Live-Sensorwerten von einem Cloud-Broker und einem SwiftUI-Befehlsschalter
Probieren Sie es auf Ihrem eigenen MQTT-Broker ausHolen Sie sich im Free-Tarif einen privaten, verwalteten MQTT-Broker und starten Sie mit fertigen Codebeispielen.
Kostenlos testen Codebeispiele Keine Kreditkarte erforderlich

In diesem iOS MQTT Swift Tutorial bauen Sie eine kleine SwiftUI-App, die Live-Sensor-Topics abonniert und Befehle an ein Gerät publiziert. Sie binden CocoaMQTT per Swift Package Manager ein, kapseln es in ein beobachtbares Model, verbinden sich über TLS mit broker.connectmq.com und behandeln die Reconnects, die echte iPhones brauchen. Dasselbe Muster trägt auch in der Industrie, etwa für eine Instandhaltungs-App, die Maschinenzustände einer Linie auf dem iPad anzeigt.

Was Sie benötigen

  • Xcode 15 oder neuer und einen Simulator oder ein Gerät mit iOS 16+.
  • Das Paket CocoaMQTT (Unterstützung für MQTT 3.1.1 und MQTT 5, Open Source, gepflegt von EMQX).
  • Einen Broker. Zum Lernen akzeptiert der öffentliche MQTT-Broker unter broker.connectmq.com anonyme Clients mit jeder nicht leeren Client-ID. Er ist für Tests und Prototypen gedacht, nicht für Produktionsdaten. Verwenden Sie ein zufälliges Topic-Präfix wie connectmq/demo/<random-id>/..., denn der öffentliche Bereich ist für alle lesbar.
  • Optional die Mosquitto-CLI-Tools, um ein Gerät zu simulieren.

Die App verwendet zwei Topics:

Topic Richtung Payload
<prefix>/sensors/+/temperature Gerät zur App 21.7
<prefix>/fan/set App zum Gerät ON / OFF

Die Wildcard + bedeutet, dass ein einziges Abonnement jeden Sensor abdeckt (kitchen, office, ...), und die App baut ihre Liste dynamisch auf. Liefert Ihr Team zusätzlich eine Android- oder Cross-Platform-Version aus, funktionieren dieselben Topics unverändert mit dem Android MQTT Client in Kotlin oder einer Flutter MQTT App.

CocoaMQTT zum Projekt hinzufügen

Wählen Sie in Xcode File, Add Package Dependencies und geben Sie Folgendes ein:

https://github.com/emqx/CocoaMQTT.git

Wählen Sie "Up to Next Major Version" ab 2.1.0 und fügen Sie die Bibliothek CocoaMQTT Ihrem App-Target hinzu. CocoaMQTT zieht seine Socket-Abhängigkeit automatisch nach. Die README des Projekts auf GitHub ist die offizielle CocoaMQTT-Dokumentation und listet die vollständige Delegate-API auf.

Eine Ausnahme für App Transport Security ist nicht nötig: Sie verbinden sich über TLS mit einem Host, der ein öffentlich vertrauenswürdiges Let's-Encrypt-Zertifikat nutzt, daher validiert iOS es gegen den System-Trust-Store.

Das iOS MQTT Swift Model erstellen

Halten Sie Netzwerkcode aus SwiftUI-Views heraus. Ein ObservableObject besitzt den Client, veröffentlicht Verbindungsstatus und Messwerte und stellt eine Methode setFan bereit.

MQTTModel.swift:

import Foundation
import CocoaMQTT

@MainActor
final class MQTTModel: ObservableObject {
    @Published var isConnected = false
    @Published var readings: [String: Double] = [:]
    @Published var lastError: String?

    let topicPrefix: String
    private let mqtt: CocoaMQTT

    init(host: String = "broker.connectmq.com",
         port: UInt16 = 8883,
         username: String? = nil,
         password: String? = nil,
         clientID: String? = nil,
         topicPrefix: String? = nil) {
        let suffix = String(UUID().uuidString.prefix(8)).lowercased()
        self.topicPrefix = topicPrefix ?? "connectmq/demo/\(suffix)"
        let id = clientID ?? "ios-\(suffix)"

        mqtt = CocoaMQTT(clientID: id, host: host, port: port)
        mqtt.username = username
        mqtt.password = password
        mqtt.enableSSL = true
        mqtt.keepAlive = 30
        mqtt.cleanSession = true
        mqtt.autoReconnect = true
        mqtt.autoReconnectTimeInterval = 2
        mqtt.maxAutoReconnectTimeInterval = 60

        mqtt.didConnectAck = { [weak self] client, ack in
            Task { @MainActor in
                guard let self else { return }
                self.isConnected = (ack == .accept)
                if ack == .accept {
                    client.subscribe("\(self.topicPrefix)/sensors/+/temperature", qos: .qos1)
                } else {
                    self.lastError = "Connection rejected: \(ack)"
                }
            }
        }

        mqtt.didReceiveMessage = { [weak self] _, message, _ in
            Task { @MainActor in
                guard let self,
                      let text = message.string,
                      let value = Double(text.trimmingCharacters(in: .whitespaces)) else { return }
                // Topic: <prefix>/sensors/<name>/temperature
                let parts = message.topic.split(separator: "/")
                guard parts.count >= 2 else { return }
                let name = String(parts[parts.count - 2])
                self.readings[name] = value
            }
        }

        mqtt.didDisconnect = { [weak self] _, error in
            Task { @MainActor in
                self?.isConnected = false
                if let error { self?.lastError = error.localizedDescription }
            }
        }
    }

    func connect() {
        lastError = nil
        if !mqtt.connect() {
            lastError = "Could not start connection"
        }
    }

    func disconnect() {
        mqtt.disconnect()
    }

    func setFan(_ on: Bool) {
        mqtt.publish("\(topicPrefix)/fan/set", withString: on ? "ON" : "OFF", qos: .qos1)
    }
}

Drei Entscheidungen sollten Sie verstehen:

  1. Callbacks wechseln auf den Main Actor. CocoaMQTT ruft seine Closures auf einer eigenen Queue auf. Werden @Published-Properties von einem Hintergrund-Thread aus aktualisiert, erzeugt SwiftUI Warnungen und gelegentliche Darstellungsfehler. Deshalb kapselt jeder Callback seine Arbeit in Task { @MainActor in ... }.
  2. Abonnieren in didConnectAck. Mit cleanSession = true vergisst der Broker Abonnements beim Trennen der Verbindung. Wer innerhalb der Verbindungsbestätigung abonniert, stellt sicher, dass jeder automatische Reconnect sie wiederherstellt.
  3. Defensives Parsen. Jeder kann auf ein öffentliches Topic publizieren, daher wird ein nicht numerischer Payload ignoriert, statt die App abstürzen zu lassen.

Der SwiftUI-Bildschirm

ContentView.swift:

import SwiftUI

struct ContentView: View {
    @StateObject private var model = MQTTModel()
    @State private var fanOn = false
    @Environment(\.scenePhase) private var scenePhase

    var body: some View {
        NavigationStack {
            List {
                Section("Connection") {
                    Label(model.isConnected ? "Connected" : "Disconnected",
                          systemImage: model.isConnected ? "checkmark.circle" : "xmark.circle")
                    if let err = model.lastError {
                        Text(err).foregroundStyle(.red).font(.footnote)
                    }
                    Text(model.topicPrefix).font(.caption).textSelection(.enabled)
                }
                Section("Sensors") {
                    if model.readings.isEmpty {
                        Text("Waiting for data...").foregroundStyle(.secondary)
                    }
                    ForEach(model.readings.keys.sorted(), id: \.self) { name in
                        HStack {
                            Text(name.capitalized)
                            Spacer()
                            Text(String(format: "%.1f °C", model.readings[name] ?? 0))
                                .monospacedDigit()
                        }
                    }
                }
                Section("Commands") {
                    Toggle("Fan", isOn: $fanOn)
                        .onChange(of: fanOn) { _, newValue in model.setFan(newValue) }
                        .disabled(!model.isConnected)
                }
            }
            .navigationTitle("iOS MQTT Demo")
        }
        .onAppear { model.connect() }
        .onChange(of: scenePhase) { _, phase in
            switch phase {
            case .active: model.connect()
            case .background: model.disconnect()
            default: break
            }
        }
    }
}

Der scenePhase-Handler ist der wichtige iOS-spezifische Teil. Wechselt Ihre App in den Hintergrund, suspendiert iOS sie innerhalb von Sekunden, und der Socket stirbt lautlos. Wer bei .background sauber trennt und bei .active neu verbindet, bietet Nutzern in dem Moment, in dem sie die App wieder öffnen, eine frische Verbindung mit aktiven Abonnements. Die hier gezeigte onChange-Variante mit zwei Parametern erfordert iOS 17; unter iOS 16 verwenden Sie die Closure mit einem Parameter.

Die App mit einem simulierten Sensor testen

Starten Sie die App, kopieren Sie das Topic-Präfix vom Bildschirm und publizieren Sie Messwerte aus einem Terminal:

PREFIX="connectmq/demo/1a2b3c4d"   # das in der App angezeigte Präfix einfügen

mosquitto_pub -h broker.connectmq.com -p 1883 -i ios-sim-kitchen \
  -t "$PREFIX/sensors/kitchen/temperature" -m "22.8"

mosquitto_pub -h broker.connectmq.com -p 1883 -i ios-sim-office \
  -t "$PREFIX/sensors/office/temperature" -m "20.1"

# Lüfterbefehle aus der App beobachten
mosquitto_sub -h broker.connectmq.com -p 1883 -i ios-sim-fan -t "$PREFIX/fan/set" -v

Beide Sensoren sollten in der Liste erscheinen, und das Umschalten des Lüfters gibt im Subscriber ON oder OFF aus. Wenn Sie lieber klicken als tippen, erledigen der MQTT-Explorer ConnectMQ Desktop und der Browser-Client im Portal dieselbe Aufgabe.

Den Online-Status von Geräten anzeigen

Eine Liste von Temperaturen ist nur dann vertrauenswürdig, wenn der Nutzer weiß, dass die Sensoren noch leben. MQTT hat dafür eine eingebaute Lösung. Jedes Gerät verbindet sich mit einer Last-Will-Nachricht auf einem Status-Topic, zum Beispiel <prefix>/sensors/kitchen/status mit dem Payload offline und gesetztem Retain-Flag, und publiziert direkt nach dem Verbinden ein retained online. Verliert das Gerät Strom oder Netzwerk, publiziert der Broker den Last Will in seinem Namen. Die Details finden Sie unter MQTT Last Will and Testament.

Ergänzen Sie auf App-Seite in didConnectAck ein zweites Abonnement für "\(topicPrefix)/sensors/+/status", speichern Sie die Werte in einem Dictionary vom Typ [String: Bool] und grauen Sie Zeilen aus, deren Sensor offline ist. Da beide Nachrichten retained sind, kennt die App den aktuellen Status jedes Sensors sofort beim Verbinden, ohne auf den nächsten Messwert zu warten.

Dasselbe Prinzip gilt für die App selbst. Müssen andere Systeme wissen, ob das Telefon eines Bedieners verbunden ist, setzen Sie im Initializer des Models einen Last Will, bevor connect() aufgerufen wird:

mqtt.willMessage = CocoaMQTTMessage(topic: "\(self.topicPrefix)/apps/\(id)/status",
                                    string: "offline", qos: .qos1, retained: true)

Für die meisten Consumer-Apps ist das unnötig, doch bei Apps für Bediener und Außendienst erhalten Vorgesetzte so einen Live-Überblick, wer gerade verbunden ist.

WebSocket statt TLS verwenden

Manche Firmen- und Hotel-WLANs blockieren Port 8883. CocoaMQTT kann MQTT über einen sicheren WebSocket auf Port 8084 tunneln, der wie gewöhnlicher HTTPS-Verkehr aussieht:

let websocket = CocoaMQTTWebSocket(uri: "/mqtt")
let client = CocoaMQTT(clientID: id, host: "broker.connectmq.com", port: 8084, socket: websocket)
client.enableSSL = true

Alles andere im Model bleibt gleich. Die Abwägungen behandelt der Artikel MQTT über WebSocket.

In Produktion mit einem privaten Broker

Sobald der Prototyp funktioniert, wechseln Sie vom öffentlichen MQTT-Broker zu Ihrem eigenen verwalteten MQTT-Broker:

  1. Erstellen Sie im ConnectMQ-Portal einen Broker und kopieren Sie den generierten Benutzernamen, das Passwort und die Client-ID.
  2. Initialisieren Sie das Model damit: MQTTModel(username: "YOUR_USERNAME", password: "YOUR_PASSWORD", clientID: "YOUR_CLIENT_ID", topicPrefix: "acme/home1").
  3. Speichern Sie das Passwort in der Keychain, nicht in UserDefaults oder im Quellcode.
  4. Behalten Sie enableSSL = true auf Port 8883 bei.

Für eine Werks-App wählen Sie als Präfix einen Teilbaum Ihres Unified Namespace (UNS), dessen Topics der ISA-95-Hierarchie folgen, etwa acme/werk-muenchen/montage/linie-1; die App abonniert dann genau die Linie, für die der Nutzer zuständig ist.

Da ConnectMQ Client IDs an ausgegebene Zugangsdaten bindet, sollten Sie nicht einen gemeinsamen Login fest in jede installierte Kopie einer App-Store-App einbauen. Sicherer ist ein Backend, das Zugangsdaten pro Nutzer oder pro Gerät ausstellt: Die App meldet den Nutzer an, erhält über HTTPS ihren eigenen MQTT-Benutzernamen, ihr Passwort und ihre Client ID, speichert sie in der Keychain, und Ihr Backend kann den Zugriff eines einzelnen Nutzers widerrufen, ohne allen anderen ein App-Update ausliefern zu müssen. Der Leitfaden zu Best Practices für MQTT-Sicherheit geht tiefer auf den Umgang mit Zugangsdaten ein.

Checkliste zur Fehlerbehebung

Problem Prüfen
didConnectAck wird nie ausgelöst Ist enableSSL für Port 8883 gesetzt und für 1883 deaktiviert? Ein Mismatch zwischen TLS und Klartext lässt den Handshake hängen.
Ack ist .identifierRejected Die Client ID ist leer oder wird bereits von einer anderen Session verwendet.
Ack ist .badUsernameOrPassword Zugangsdaten mit nachgestellten Leerzeichen kopiert oder falscher Broker.
Funktioniert im Simulator, nicht auf dem Gerät Das Gerät ist in einem Netzwerk, das 8883 blockiert; probieren Sie WebSocket auf 8084.
Werte stoppen nach dem Entsperren des Telefons Fehlender scenePhase-Reconnect oder Abonnements werden nicht in didConnectAck wiederhergestellt.
Violette SwiftUI-Laufzeitwarnungen @Published-Property wird außerhalb des Main Actors aktualisiert.

Starten Sie mit ConnectMQ

Ihre SwiftUI-App ist nur einen Austausch der Zugangsdaten von der Produktion entfernt. Erstellen Sie unter app.connectmq.com ohne Kreditkarte einen kostenlosen Serverless-Broker, fügen Sie Benutzername, Passwort und Client-ID in MQTTModel ein und beobachten Sie im Live-Topic-Explorer des Portals, wie Ihr iPhone und Ihre Geräte miteinander kommunizieren. ConnectMQ betreibt den Broker, damit Sie sich auf die App konzentrieren können.

Häufig gestellte Fragen

Welche Swift-MQTT-Bibliothek sollte ich unter iOS verwenden?

CocoaMQTT ist die am weitesten verbreitete Wahl, mit Unterstützung für Swift Package Manager, TLS, WebSocket sowie Clients für MQTT 3.1.1 und MQTT 5. Einen plattformübergreifenden Vergleich bietet die Übersicht der MQTT-Client-Bibliotheken.

Kann eine iOS-App MQTT-Nachrichten im Hintergrund empfangen?

Nicht zuverlässig. iOS suspendiert Apps im Hintergrund samt ihrer Sockets. Verbinden Sie sich daher neu, sobald die App aktiv wird, und nutzen Sie Push-Benachrichtigungen für Alarme, die ein gesperrtes Telefon erreichen müssen.

Brauche ich eine Ausnahme für App Transport Security für MQTT?

Nein, wenn Sie sich per TLS mit einem Broker verbinden, der ein öffentlich vertrauenswürdiges Zertifikat verwendet, wie es broker.connectmq.com auf den Ports 8883 und 8084 tut. ATS-Regeln gelten für das Laden von URLs, und MQTT über unverschlüsseltes TCP auf Port 1883 sollten Sie in ausgelieferten Apps ohnehin vermeiden.

Wie verwende ich MQTT 5 mit CocoaMQTT?

Verwenden Sie die Klasse CocoaMQTT5 statt CocoaMQTT. Ihre API ist ähnlich und ergänzt MQTT-5-Properties; ConnectMQ unterstützt beide Protokollversionen.

Verwandte Artikel

Die besten MQTT-CLI-Tools zum Testen, Skripten und Debuggen

Ein praxisnaher Vergleich von mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, der MQTT.js-CLI, curl und mqttui – mit demselben Test gegen broker.connectmq.com, damit Sie das richtige Tool zum Skripten und Debuggen wählen.

8 Min. Lesezeit

Alle Artikel →

Ihr privater MQTT-Broker ist nur wenige Klicks entfernt

Konto anlegen, Broker-Zugangsdaten erhalten und das erste Gerät verbinden — ohne Server, die installiert, gepatcht oder skaliert werden müssen.