Este tutorial de MQTT no iOS com Swift constrói um pequeno app SwiftUI que assina topics de sensores ao vivo e publica comandos para um dispositivo. Você vai adicionar o CocoaMQTT com o Swift Package Manager, encapsulá-lo em um modelo observável, conectar via TLS ao servidor MQTT público broker.connectmq.com e tratar as reconexões de que iPhones reais precisam.
O que você precisa
- Xcode 15 ou mais recente e um simulador ou dispositivo com iOS 16+.
- O pacote CocoaMQTT (suporte a MQTT 3.1.1 e MQTT 5, open source, mantido pela EMQX).
- Um broker. Para aprender, o servidor MQTT público (gratuito) em broker.connectmq.com aceita clientes anônimos com qualquer client ID não vazio. Use um prefixo de topic aleatório como
connectmq/demo/<random-id>/..., porque o servidor público pode ser lido por qualquer pessoa e serve apenas para testes. - Opcionalmente, as ferramentas CLI do Mosquitto para simular um dispositivo.
O app usa dois topics:
| Topic | Direção | Payload |
|---|---|---|
<prefix>/sensors/+/temperature |
do dispositivo para o app | 21.7 |
<prefix>/fan/set |
do app para o dispositivo | ON / OFF |
O curinga + faz com que uma única assinatura cubra todos os sensores (kitchen, office, ...), e o app monta a lista dinamicamente. Se a sua equipe também distribui uma versão Android ou multiplataforma, os mesmos topics funcionam sem mudanças com o cliente MQTT para Android em Kotlin ou um app MQTT com Flutter.
Adicionando o CocoaMQTT ao projeto
No Xcode, escolha File, Add Package Dependencies, e informe:
https://github.com/emqx/CocoaMQTT.git
Selecione "Up to Next Major Version" a partir de 2.1.0 e adicione a biblioteca CocoaMQTT ao target do app. O CocoaMQTT traz sua dependência de sockets automaticamente. O README do projeto no GitHub é a documentação oficial do CocoaMQTT e lista toda a API de delegate.
Nenhuma exceção de App Transport Security é necessária: você está se conectando via TLS a um host com um certificado Let's Encrypt publicamente confiável, então o iOS o valida contra o repositório de certificados do sistema.
Construindo o modelo MQTT para iOS em Swift
Mantenha a rede fora das views SwiftUI. Um ObservableObject é dono do cliente, publica o estado da conexão e as leituras e expõe um 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)
}
}
Vale a pena entender três escolhas:
- Os callbacks passam para o main actor. O CocoaMQTT dispara suas closures na própria fila. Atualizar propriedades
@Publisheda partir de uma thread em segundo plano gera avisos do SwiftUI e falhas ocasionais, então cada callback envolve seu trabalho emTask { @MainActor in ... }. - Assinar em
didConnectAck. ComcleanSession = true, o broker esquece as assinaturas ao desconectar. Assinar dentro da confirmação de conexão garante que cada reconexão automática as restaure. - Parse defensivo. Qualquer pessoa pode publicar em um topic público, então um payload não numérico é ignorado em vez de derrubar o app.
A tela 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
}
}
}
}
O handler de scenePhase é a parte importante e específica do iOS. Quando o app vai para o segundo plano, o iOS o suspende em segundos e o socket morre silenciosamente. Desconectar de forma limpa em .background e reconectar em .active dá ao usuário uma conexão nova e já com as assinaturas no momento em que ele reabre o app. A forma de onChange com dois parâmetros mostrada aqui exige iOS 17; no iOS 16, use a closure de um parâmetro.
Testando o app com um sensor simulado
Rode o app, copie o prefixo de topic da tela e publique leituras a partir de um terminal:
PREFIX="connectmq/demo/1a2b3c4d" # cole o prefixo mostrado no 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"
# Acompanhe os comandos de ventilador enviados pelo app
mosquitto_sub -h broker.connectmq.com -p 1883 -i ios-sim-fan -t "$PREFIX/fan/set" -v
Os dois sensores devem aparecer na lista, e acionar o ventilador imprime ON ou OFF no assinante. Se preferir clicar a digitar, o ConnectMQ Desktop MQTT explorer e o cliente no navegador do portal fazem o mesmo trabalho.
Mostrando o status online dos dispositivos
Uma lista de temperaturas só é confiável se o usuário souber que os sensores estão vivos. O MQTT tem uma resposta nativa para isso. Cada dispositivo se conecta com uma mensagem de last will em um topic de status, por exemplo <prefix>/sensors/kitchen/status com o payload offline e a flag retain ativada, e publica um online retido logo após se conectar. Se o dispositivo perder energia ou rede, o broker publica o will em nome dele. Os detalhes estão em MQTT last will and testament.
No lado do app, adicione uma segunda assinatura em didConnectAck para "\(topicPrefix)/sensors/+/status", guarde os valores em um dicionário [String: Bool] e deixe em cinza as linhas cujo sensor está offline. Como as duas mensagens são retidas, o app descobre o status atual de cada sensor no momento em que se conecta, sem esperar a próxima leitura.
A mesma ideia vale para o próprio app. Se outros sistemas precisam saber se o celular de um operador está conectado, defina um will no inicializador do modelo, antes de chamar connect():
mqtt.willMessage = CocoaMQTTMessage(topic: "\(self.topicPrefix)/apps/\(id)/status",
string: "offline", qos: .qos1, retained: true)
Para a maioria dos apps de consumo isso é desnecessário, mas em apps para operadores e equipes de campo dá aos supervisores uma visão ao vivo de quem está conectado.
Usando WebSocket em vez de TLS
Algumas redes Wi-Fi corporativas e de hotéis bloqueiam a porta 8883. O CocoaMQTT consegue tunelar o MQTT por WebSocket seguro na porta 8084, que parece tráfego HTTPS comum:
let websocket = CocoaMQTTWebSocket(uri: "/mqtt")
let client = CocoaMQTT(clientID: id, host: "broker.connectmq.com", port: 8084, socket: websocket)
client.enableSSL = true
Todo o resto do modelo continua igual. Os prós e contras estão em MQTT sobre WebSocket.
Indo para produção em um broker privado
Quando o protótipo funcionar, saia do servidor MQTT público e passe para o seu próprio broker MQTT gerenciado:
- Crie um broker no portal do ConnectMQ e copie o usuário, a senha e o client ID gerados.
- Inicialize o modelo com eles:
MQTTModel(username: "YOUR_USERNAME", password: "YOUR_PASSWORD", clientID: "YOUR_CLIENT_ID", topicPrefix: "acme/home1"). - Guarde a senha no Keychain, não em
UserDefaultsnem no código-fonte. - Mantenha
enableSSL = truena porta 8883.
Como o ConnectMQ vincula os client IDs às credenciais emitidas, evite fixar no código um único login compartilhado em todas as cópias instaladas de um app da App Store. Um backend que emite credenciais por usuário ou por dispositivo é o design mais seguro: o app faz o login do usuário, recebe o seu próprio usuário, senha e client ID MQTT via HTTPS, guarda-os no Keychain, e o seu backend pode revogar o acesso de um único usuário sem precisar distribuir uma atualização do app para todos os outros. O guia de boas práticas de segurança MQTT se aprofunda no tratamento de credenciais.
Checklist de solução de problemas
| Problema | Verifique |
|---|---|
didConnectAck nunca dispara |
enableSSL está ativado para a porta 8883 e desativado para a 1883? Uma divergência entre TLS e texto puro trava o handshake. |
O ack é .identifierRejected |
O client ID está vazio ou já em uso por outra sessão. |
O ack é .badUsernameOrPassword |
Credenciais copiadas com espaços no final, ou broker errado. |
| Funciona no simulador, não no aparelho | O aparelho está em uma rede que bloqueia a 8883; tente WebSocket na 8084. |
| Os valores param depois de desbloquear o celular | Falta a reconexão via scenePhase, ou as assinaturas não são restauradas em didConnectAck. |
| Avisos roxos de runtime do SwiftUI | Propriedade @Published atualizada fora do main actor. |
Comece a usar o ConnectMQ
O seu app SwiftUI está a uma troca de credenciais da produção. Crie um broker Serverless gratuito em app.connectmq.com, sem cartão de crédito, cole o usuário, a senha e o client ID no MQTTModel e use o explorador de topics ao vivo do portal para ver o seu iPhone e os seus dispositivos conversando. Como broker MQTT gerenciado, o ConnectMQ opera o broker para que você possa se concentrar no app.
Perguntas frequentes
Qual biblioteca MQTT em Swift devo usar no iOS?
O CocoaMQTT é a escolha mais usada, com suporte ao Swift Package Manager, TLS, WebSocket e clientes MQTT 3.1.1 e MQTT 5. Para um comparativo mais amplo entre plataformas, veja a visão geral de bibliotecas cliente MQTT.
Um app iOS consegue receber mensagens MQTT em segundo plano?
Não de forma confiável. O iOS suspende apps em segundo plano e seus sockets, então reconecte quando o app ficar ativo e use push notifications para alertas que precisam chegar a um celular bloqueado.
Preciso de uma exceção de App Transport Security para MQTT?
Não, quando você se conecta via TLS a um broker que usa um certificado publicamente confiável, como o broker.connectmq.com faz nas portas 8883 e 8084. As regras do ATS se aplicam ao carregamento de URLs, e MQTT em TCP puro na 1883 deve ser evitado em apps publicados de qualquer forma.
Como usar MQTT 5 com o CocoaMQTT?
Use a classe CocoaMQTT5 em vez de CocoaMQTT. A API é parecida e acrescenta as propriedades do MQTT 5; o ConnectMQ suporta as duas versões do protocolo.



