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 :
- Les callbacks basculent sur le main actor. CocoaMQTT déclenche ses closures sur sa propre file. Mettre à jour des propriétés
@Publisheddepuis un thread d'arrière-plan provoque des avertissements SwiftUI et des anomalies occasionnelles ; chaque callback encapsule donc son travail dansTask { @MainActor in ... }. - S'abonner dans
didConnectAck. AveccleanSession = 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. - 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é :
- Créez un broker dans le portail ConnectMQ et copiez le username, le mot de passe et le client ID générés.
- Initialisez le modèle avec ces valeurs :
MQTTModel(username: "YOUR_USERNAME", password: "YOUR_PASSWORD", clientID: "YOUR_CLIENT_ID", topicPrefix: "acme/home1"). - Stockez le mot de passe dans le Keychain, pas dans
UserDefaultsni dans le code source. - Conservez
enableSSL = truesur 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.



