Tutoriels IoT

Client MQTT iOS en Swift avec CocoaMQTT et SwiftUI

Tutoriel MQTT iOS en Swift pas à pas : ajoutez CocoaMQTT avec Swift Package Manager, encapsulez-le dans un ObservableObject, affichez des données de capteurs en direct dans SwiftUI et publiez des commandes via TLS.

Application MQTT iOS en Swift sur un iPhone affichant des relevés de capteurs en direct depuis un broker cloud, avec un bouton de commande SwiftUI
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

Ce tutoriel MQTT iOS en Swift construit une petite application SwiftUI qui s'abonne à des topics de capteurs en direct et publie des commandes vers un appareil. Vous allez ajouter CocoaMQTT avec Swift Package Manager, l'encapsuler dans un modèle observable, vous connecter en TLS à broker.connectmq.com et gérer les reconnexions dont les vrais iPhone ont besoin.

Ce dont vous avez besoin

  • Xcode 15 ou plus récent et un simulateur ou appareil iOS 16+.
  • Le package CocoaMQTT (prise en charge de MQTT 3.1.1 et MQTT 5, open source, maintenu par EMQX).
  • Un broker. Pour apprendre, le serveur MQTT public gratuit à l'adresse broker.connectmq.com, réservé aux tests et aux prototypes, accepte les clients anonymes avec n'importe quel client ID non vide. Utilisez un préfixe de topic aléatoire comme connectmq/demo/<random-id>/..., car la zone publique est lisible par tout le monde.
  • En option, les outils en ligne de commande Mosquitto pour simuler un appareil.

L'application utilise deux topics :

Topic Sens Payload
<prefix>/sensors/+/temperature de l'appareil vers l'application 21.7
<prefix>/fan/set de l'application vers l'appareil ON / OFF

Le wildcard + signifie qu'un seul abonnement couvre tous les capteurs (kitchen, office, ...), et l'application construit sa liste dynamiquement. Si votre équipe livre aussi une version Android ou multiplateforme, les mêmes topics fonctionnent sans modification avec le client MQTT Android en Kotlin ou une application MQTT Flutter.

Ajouter CocoaMQTT au projet

Dans Xcode, choisissez File, Add Package Dependencies, puis saisissez :

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

Sélectionnez « Up to Next Major Version » à partir de 2.1.0 et ajoutez la bibliothèque CocoaMQTT à la cible de votre application. CocoaMQTT récupère automatiquement sa dépendance de socket. Le README du projet sur GitHub constitue la documentation officielle de CocoaMQTT et liste l'API complète des délégués.

Aucune exception App Transport Security n'est nécessaire : vous vous connectez en TLS à un hôte doté d'un certificat Let's Encrypt reconnu publiquement, donc iOS le valide via le magasin de confiance du système.

Construire le modèle MQTT iOS en Swift

Gardez le réseau hors des vues SwiftUI. Un ObservableObject possède le client, publie l'état de connexion et les relevés, et expose une méthode setFan.

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)
    }
}

Trois choix méritent d'être compris :

  1. Les callbacks basculent sur le main actor. CocoaMQTT déclenche ses closures sur sa propre file. Mettre à jour des propriétés @Published depuis un thread d'arrière-plan provoque des avertissements SwiftUI et des anomalies occasionnelles ; chaque callback encapsule donc son travail dans Task { @MainActor in ... }.
  2. S'abonner dans didConnectAck. Avec cleanSession = true, le broker oublie les abonnements à la déconnexion. S'abonner dans l'accusé de réception de connexion garantit que chaque reconnexion automatique les rétablit.
  3. Analyse défensive. N'importe qui peut publier sur un topic public : un payload non numérique est donc ignoré au lieu de faire planter l'application.

L'écran SwiftUI

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
            }
        }
    }
}

Le gestionnaire scenePhase est la partie spécifique à iOS la plus importante. Lorsque votre application passe en arrière-plan, iOS la suspend en quelques secondes et le socket meurt silencieusement. Se déconnecter proprement sur .background et se reconnecter sur .active offre aux utilisateurs une connexion neuve et abonnée dès qu'ils rouvrent l'application. La forme à deux paramètres de onChange présentée ici requiert iOS 17 ; sur iOS 16, utilisez la closure à un seul paramètre.

Tester l'application avec un capteur simulé

Lancez l'application, copiez le préfixe de topic affiché à l'écran et publiez des relevés depuis un terminal :

PREFIX="connectmq/demo/1a2b3c4d"   # collez le préfixe affiché dans l'application

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"

# Observer les commandes du ventilateur envoyées par l'application
mosquitto_sub -h broker.connectmq.com -p 1883 -i ios-sim-fan -t "$PREFIX/fan/set" -v

Les deux capteurs doivent apparaître dans la liste, et basculer le ventilateur affiche ON ou OFF dans l'abonné. Si vous préférez cliquer plutôt que taper, l'explorateur MQTT ConnectMQ Desktop et le client intégré au navigateur dans le portail font le même travail.

Afficher l'état en ligne des appareils

Une liste de températures n'est fiable que si l'utilisateur sait que les capteurs sont actifs. MQTT apporte une réponse native. Chaque appareil se connecte avec un message last will sur un topic d'état, par exemple <prefix>/sensors/kitchen/status avec le payload offline et le flag retain activé, puis publie un online retenu juste après la connexion. Si l'appareil perd l'alimentation ou le réseau, le broker publie le will en son nom. Les détails sont expliqués dans MQTT last will and testament.

Côté application, ajoutez un second abonnement dans didConnectAck pour "\(topicPrefix)/sensors/+/status", stockez les valeurs dans un dictionnaire [String: Bool] et grisez les lignes dont le capteur est hors ligne. Comme les deux messages sont retenus, l'application connaît l'état actuel de chaque capteur dès sa connexion, sans attendre le relevé suivant.

Le même principe s'applique à l'application elle-même. Si d'autres systèmes doivent savoir si le téléphone d'un opérateur est connecté, définissez un will dans l'initialiseur du modèle, avant l'appel à connect() :

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

Pour la plupart des applications grand public, c'est inutile, mais pour les applications d'opérateurs et d'intervention terrain, cela donne aux superviseurs une vue en direct des personnes connectées.

Utiliser WebSocket au lieu de TLS

Certains réseaux Wi-Fi d'entreprise et d'hôtel bloquent le port 8883. CocoaMQTT peut faire passer MQTT dans un WebSocket sécurisé sur le port 8084, qui ressemble à du trafic HTTPS ordinaire :

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

Tout le reste du modèle reste identique. Les compromis sont détaillés dans MQTT over WebSocket.

Passer en production sur un broker privé

Une fois le prototype fonctionnel, quittez le serveur MQTT public gratuit pour votre propre broker MQTT managé :

  1. Créez un broker dans le portail ConnectMQ et copiez le username, le mot de passe et le client ID générés.
  2. Initialisez le modèle avec ces valeurs : MQTTModel(username: "YOUR_USERNAME", password: "YOUR_PASSWORD", clientID: "YOUR_CLIENT_ID", topicPrefix: "acme/home1").
  3. Stockez le mot de passe dans le Keychain, pas dans UserDefaults ni dans le code source.
  4. Conservez enableSSL = true sur le port 8883.

Comme ConnectMQ lie les client IDs aux identifiants émis, évitez de coder en dur un identifiant partagé unique dans chaque copie installée d'une application de l'App Store. Un backend qui émet des identifiants par utilisateur ou par appareil est une conception plus sûre : l'application authentifie l'utilisateur, reçoit ses propres username, mot de passe et client ID MQTT via HTTPS, les stocke dans le Keychain, et votre backend peut révoquer l'accès d'un seul utilisateur sans publier de mise à jour pour tous les autres. Le guide des bonnes pratiques de sécurité MQTT approfondit la gestion des identifiants.

Liste de dépannage

Problème Vérification
didConnectAck ne se déclenche jamais enableSSL est-il activé pour le port 8883, et désactivé pour 1883 ? Une incohérence TLS/clair bloque la négociation.
L'ack vaut .identifierRejected Le client ID est vide ou déjà utilisé par une autre session.
L'ack vaut .badUsernameOrPassword Identifiants copiés avec des espaces en fin de chaîne, ou mauvais broker.
Fonctionne sur le simulateur, pas sur l'appareil L'appareil est sur un réseau qui bloque 8883 ; essayez WebSocket sur 8084.
Les valeurs s'arrêtent après le déverrouillage du téléphone Reconnexion scenePhase manquante, ou abonnements non rétablis dans didConnectAck.
Avertissements d'exécution violets dans SwiftUI Propriété @Published mise à jour en dehors du main actor.

Démarrer avec ConnectMQ

Votre application SwiftUI n'est qu'à un changement d'identifiants de la production. Créez un broker Serverless gratuit sur app.connectmq.com sans carte bancaire, collez le username, le mot de passe et le client ID dans MQTTModel, puis utilisez l'explorateur de topics en direct du portail pour voir votre iPhone et vos appareils communiquer. ConnectMQ exploite le broker pour que vous puissiez vous concentrer sur l'application.

Questions fréquentes

Quelle bibliothèque MQTT Swift utiliser sur iOS ?

CocoaMQTT est le choix le plus répandu, avec prise en charge de Swift Package Manager, TLS, WebSocket et des clients MQTT 3.1.1 et MQTT 5. Pour une comparaison plus large entre plateformes, consultez la présentation des bibliothèques clientes MQTT.

Une application iOS peut-elle recevoir des messages MQTT en arrière-plan ?

Pas de manière fiable. iOS suspend les applications en arrière-plan et leurs sockets : reconnectez-vous lorsque l'application redevient active et utilisez les notifications push pour les alertes qui doivent atteindre un téléphone verrouillé.

Ai-je besoin d'une exception App Transport Security pour MQTT ?

Non, lorsque vous vous connectez en TLS à un broker qui utilise un certificat reconnu publiquement, comme broker.connectmq.com sur les ports 8883 et 8084. Les règles ATS s'appliquent au chargement d'URL, et MQTT en TCP clair sur 1883 est de toute façon à éviter dans les applications publiées.

Comment utiliser MQTT 5 avec CocoaMQTT ?

Utilisez la classe CocoaMQTT5 au lieu de CocoaMQTT. Son API est similaire et ajoute les propriétés MQTT 5 ; ConnectMQ prend en charge les deux versions du protocole.

Articles liés

Les meilleurs outils CLI MQTT pour tester, scripter et déboguer

Un comparatif pratique de mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, la CLI MQTT.js, curl et mqttui, avec le même test exécuté sur le serveur MQTT public gratuit broker.connectmq.com pour choisir le bon outil pour le scripting et le débogage.

9 min de lecture

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.