Este tutorial de iOS MQTT en Swift construye una pequeña app SwiftUI que se suscribe a topics de sensores en vivo y publica comandos hacia un dispositivo. Añadirás CocoaMQTT con Swift Package Manager, lo envolverás en un modelo observable, te conectarás por TLS a broker.connectmq.com y gestionarás las reconexiones que necesitan los iPhone reales.
Qué necesitas
- Xcode 15 o posterior y un simulador o dispositivo con iOS 16+.
- El paquete CocoaMQTT (compatible con MQTT 3.1.1 y MQTT 5, de código abierto y mantenido por EMQX).
- Un broker. Para aprender, el servidor MQTT público gratuito en broker.connectmq.com acepta clientes anónimos con cualquier client ID no vacío. Usa un prefijo de topic aleatorio como
connectmq/demo/<random-id>/..., porque el área pública es legible por todo el mundo; úsalo solo para pruebas y prototipos, nunca para datos de producción. - Opcionalmente, las herramientas CLI de Mosquitto para simular un dispositivo.
La app usa dos topics:
| Topic | Dirección | Payload |
|---|---|---|
<prefix>/sensors/+/temperature |
dispositivo a app | 21.7 |
<prefix>/fan/set |
app a dispositivo | ON / OFF |
El comodín + significa que una sola suscripción cubre todos los sensores (kitchen, office, ...), y la app construye su lista de forma dinámica. Si tu equipo también publica una versión para Android o multiplataforma, los mismos topics funcionan sin cambios con el cliente MQTT para Android en Kotlin o con una app Flutter MQTT.
Añadir CocoaMQTT al proyecto
En Xcode elige File, Add Package Dependencies, e introduce:
https://github.com/emqx/CocoaMQTT.git
Selecciona "Up to Next Major Version" desde la 2.1.0 y añade la biblioteca CocoaMQTT al target de tu app. CocoaMQTT incorpora automáticamente su dependencia de sockets. El README del proyecto en GitHub es la documentación oficial de CocoaMQTT y recoge la API completa de delegados.
No hace falta ninguna excepción de App Transport Security: te conectas por TLS a un host con un certificado de Let's Encrypt de confianza pública, así que iOS lo valida contra el almacén de confianza del sistema.
Construir el modelo iOS MQTT en Swift
Mantén la red fuera de las vistas SwiftUI. Un ObservableObject es dueño del cliente, publica el estado de la conexión y las lecturas, y expone un método 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)
}
}
Merece la pena entender tres decisiones:
- Los callbacks saltan al main actor. CocoaMQTT ejecuta sus closures en su propia cola. Actualizar propiedades
@Publisheddesde un hilo en segundo plano provoca advertencias de SwiftUI y fallos ocasionales, así que cada callback envuelve su trabajo enTask { @MainActor in ... }. - Suscribirse en
didConnectAck. ConcleanSession = true, el broker olvida las suscripciones al desconectarse. Suscribirse dentro de la confirmación de conexión hace que cada reconexión automática las restaure. - Análisis defensivo. Cualquiera puede publicar en un topic público, así que un payload no numérico se ignora en lugar de bloquear la app.
La pantalla 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
}
}
}
}
El manejador de scenePhase es la parte importante específica de iOS. Cuando tu app pasa a segundo plano, iOS la suspende en cuestión de segundos y el socket muere en silencio. Desconectar limpiamente en .background y reconectar en .active da a los usuarios una conexión nueva y suscrita en el momento en que vuelven a abrir la app. La forma de onChange con dos parámetros que se muestra aquí requiere iOS 17; en iOS 16 usa el closure de un solo parámetro.
Probar la app con un sensor simulado
Ejecuta la app, copia el prefijo de topic de la pantalla y publica lecturas desde una terminal:
PREFIX="connectmq/demo/1a2b3c4d" # pega el prefijo que muestra la app
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"
# Observar los comandos del ventilador que envía la app
mosquitto_sub -h broker.connectmq.com -p 1883 -i ios-sim-fan -t "$PREFIX/fan/set" -v
Ambos sensores deberían aparecer en la lista, y al accionar el ventilador se imprime ON u OFF en el suscriptor. Si prefieres hacer clic en lugar de escribir, el explorador MQTT ConnectMQ Desktop y el cliente en el navegador del portal hacen el mismo trabajo.
Mostrar el estado en línea de los dispositivos
Una lista de temperaturas solo es fiable si el usuario sabe que los sensores siguen vivos. MQTT tiene una respuesta integrada. Cada dispositivo se conecta con un mensaje de last will en un topic de estado, por ejemplo <prefix>/sensors/kitchen/status con el payload offline y el flag retain activado, y publica un online retenido justo después de conectarse. Si el dispositivo pierde la alimentación o la red, el broker publica el will en su nombre. Los detalles están en last will and testament en MQTT.
En el lado de la app, añade una segunda suscripción en didConnectAck para "\(topicPrefix)/sensors/+/status", guarda los valores en un diccionario [String: Bool] y atenúa las filas cuyo sensor esté desconectado. Como ambos mensajes son retenidos, la app conoce el estado actual de cada sensor en cuanto se conecta, sin esperar a la siguiente lectura.
La misma idea se aplica a la propia app. Si otros sistemas necesitan saber si el teléfono de un operario está conectado, define un will en el inicializador del modelo, antes de llamar a connect():
mqtt.willMessage = CocoaMQTTMessage(topic: "\(self.topicPrefix)/apps/\(id)/status",
string: "offline", qos: .qos1, retained: true)
En la mayoría de las apps de consumo esto no es necesario, pero en las apps para operarios y servicio técnico de campo ofrece a los supervisores una vista en vivo de quién está conectado.
Usar WebSocket en lugar de TLS
Algunas redes Wi-Fi corporativas y de hoteles bloquean el puerto 8883. CocoaMQTT puede tunelizar MQTT a través de WebSocket seguro en el puerto 8084, que se parece al tráfico HTTPS normal:
let websocket = CocoaMQTTWebSocket(uri: "/mqtt")
let client = CocoaMQTT(clientID: id, host: "broker.connectmq.com", port: 8084, socket: websocket)
client.enableSSL = true
Todo lo demás en el modelo se mantiene igual. Las ventajas y desventajas se explican en MQTT sobre WebSocket.
Pasar a producción con un broker privado
Cuando el prototipo funcione, deja el servidor público y pasa a tu propio broker MQTT gestionado:
- Crea un broker en el portal de ConnectMQ y copia el usuario, la contraseña y el client ID generados.
- Inicializa el modelo con ellos:
MQTTModel(username: "YOUR_USERNAME", password: "YOUR_PASSWORD", clientID: "YOUR_CLIENT_ID", topicPrefix: "acme/home1"). - Guarda la contraseña en el Keychain, no en
UserDefaultsni en el código fuente. - Mantén
enableSSL = trueen el puerto 8883.
Como ConnectMQ vincula los client ID a las credenciales emitidas, evita codificar un único inicio de sesión compartido en cada copia instalada de una app de la App Store. Un backend que emita credenciales por usuario o por dispositivo es el diseño más seguro: la app inicia la sesión del usuario, recibe por HTTPS su propio usuario, contraseña y client ID de MQTT, los guarda en el Keychain, y tu backend puede revocar el acceso de un solo usuario sin tener que publicar una actualización de la app para todos los demás. La guía de buenas prácticas de seguridad en MQTT profundiza en la gestión de credenciales.
Lista de comprobación para solucionar problemas
| Problema | Qué comprobar |
|---|---|
didConnectAck nunca se dispara |
¿Está enableSSL activado para el puerto 8883 y desactivado para el 1883? Una discrepancia TLS/plano bloquea el handshake. |
El ack es .identifierRejected |
El client ID está vacío o ya lo usa otra sesión. |
El ack es .badUsernameOrPassword |
Credenciales copiadas con espacios al final, o broker equivocado. |
| Funciona en el simulador, pero no en el dispositivo | El dispositivo está en una red que bloquea el 8883; prueba WebSocket en el 8084. |
| Los valores se detienen tras desbloquear el teléfono | Falta la reconexión con scenePhase, o las suscripciones no se restauran en didConnectAck. |
| Advertencias moradas de SwiftUI en tiempo de ejecución | Una propiedad @Published se actualiza fuera del main actor. |
Empieza con ConnectMQ
Tu app SwiftUI está a un cambio de credenciales de la producción. Crea un broker Serverless gratuito en app.connectmq.com sin tarjeta de crédito, pega el usuario, la contraseña y el client ID en MQTTModel y usa el explorador de topics en vivo del portal para ver cómo tu iPhone y tus dispositivos hablan entre sí. ConnectMQ gestiona el broker para que tú te centres en la app.
Preguntas frecuentes
¿Qué biblioteca MQTT para Swift debería usar en iOS?
CocoaMQTT es la opción más utilizada, con soporte para Swift Package Manager, TLS, WebSocket y clientes tanto MQTT 3.1.1 como MQTT 5. Para una comparación más amplia entre plataformas, consulta la panorámica de bibliotecas cliente MQTT.
¿Puede una app iOS recibir mensajes MQTT en segundo plano?
No de forma fiable. iOS suspende las apps en segundo plano y sus sockets, así que vuelve a conectar cuando la app pase a estar activa y usa notificaciones push para las alertas que deban llegar a un teléfono bloqueado.
¿Necesito una excepción de App Transport Security para MQTT?
No, si te conectas con TLS a un broker que usa un certificado de confianza pública, como hace broker.connectmq.com en los puertos 8883 y 8084. Las reglas de ATS se aplican a la carga de URL, y de todos modos conviene evitar MQTT sobre TCP plano en el 1883 en las apps publicadas.
¿Cómo uso MQTT 5 con CocoaMQTT?
Usa la clase CocoaMQTT5 en lugar de CocoaMQTT. Su API es similar y añade las propiedades de MQTT 5; ConnectMQ es compatible con ambas versiones del protocolo.



