CocoaMQTT is the most widely used MQTT client library for Swift on iOS and macOS. It supports MQTT 3.1.1 and MQTT 5, TLS, WebSockets and automatic reconnects, and it installs with Swift Package Manager or CocoaPods. This tutorial connects an iOS app to a broker over TLS, wires the client into SwiftUI, shows MQTT 5 properties and explains what iOS does to your connection when the app goes to the background.
Install CocoaMQTT
In Xcode choose File → Add Package Dependencies and enter the repository URL, or add it to Package.swift:
dependencies: [
.package(url: "https://github.com/emqx/CocoaMQTT.git", from: "2.1.0")
]With CocoaPods, add pod 'CocoaMQTT' to your Podfile and run pod install. The examples target iOS 17 and Swift 5.9 or later.
Connect over TLS
Create a client with a unique client ID, set credentials and turn on TLS. CocoaMQTT exposes callbacks either through a delegate protocol or as closure properties; closures keep small examples short:
import CocoaMQTT
let clientID = "testmqtt-ios-" + String(UUID().uuidString.prefix(8))
let mqtt = CocoaMQTT(clientID: clientID, host: "broker.connectmq.com", port: 8883)
mqtt.username = "username"
mqtt.password = "password"
mqtt.enableSSL = true
mqtt.keepAlive = 60
mqtt.cleanSession = true
mqtt.autoReconnect = true
mqtt.didConnectAck = { mqtt, ack in
print("CONNACK:", ack)
if ack == .accept {
mqtt.subscribe("testmqtt/ios/demo/#", qos: .qos1)
}
}
mqtt.didReceiveMessage = { _, message, _ in
print("\(message.topic): \(message.string ?? "<binary>")")
}
mqtt.didDisconnect = { _, error in
print("disconnected:", error?.localizedDescription ?? "clean")
}
_ = mqtt.connect()connect() only starts the connection attempt and returns immediately; success or failure arrives in didConnectAck and didDisconnect. A CONNACK other than .accept — for example .badUsernameOrPassword or .notAuthorized — means the broker refused you. The reason codes table explains each value.
Publishing is a single call:
mqtt.publish("testmqtt/ios/demo/hello", withString: "hello from iOS", qos: .qos1, retained: false)For JSON, encode with JSONEncoder and pass the bytes using CocoaMQTTMessage(topic:payload:qos:retained:).
TLS details: private CAs and client certificates
With enableSSL = true, CocoaMQTT validates the server certificate against the iOS trust store, which works out of the box for brokers with publicly trusted certificates. Two situations need more work:
- Private CA or self-signed certificate. Implement the
didReceiveTrustcallback, evaluate theSecTrustagainst your bundled CA certificate withSecTrustSetAnchorCertificatesandSecTrustEvaluateWithError, and call the completion handler with the result.allowUntrustCACertificate = trueskips validation entirely — handy for a quick lab test, dangerous anywhere else. - Mutual TLS. Load your client identity from a PKCS#12 file with
SecPKCS12Importand set it viamqtt.sslSettings = [kCFStreamSSLCertificates as String: [identity] as CFArray].
App Transport Security mainly governs URL-loading APIs, so iOS will not stop you from opening an unencrypted socket on port 1883. That is a reason to enforce TLS yourself, not a loophole to use. Before debugging Swift code, check that the broker's certificate and port work with the steps in testing an MQTT TLS connection.
MQTT over secure WebSockets
Some corporate and carrier networks block ports 1883 and 8883. WebSockets over TLS travel on a port that looks like normal HTTPS traffic:
let websocket = CocoaMQTTWebSocket(uri: "/mqtt")
let wsClient = CocoaMQTT(clientID: clientID, host: "broker.connectmq.com",
port: 8084, socket: websocket)
wsClient.enableSSL = true
wsClient.username = "username"
wsClient.password = "password"
_ = wsClient.connect()The path must match the broker's WebSocket endpoint — a wrong path typically fails with an HTTP 404 during the upgrade. See MQTT over WebSockets for how the transport works.
Wire it into SwiftUI
Wrap the client in an ObservableObject that owns it for the lifetime of the app, and publish state changes on the main thread:
import CocoaMQTT
import Foundation
final class MQTTService: ObservableObject {
@Published var isConnected = false
@Published var lastMessages: [String] = []
private let mqtt: CocoaMQTT
init(clientID: String) {
mqtt = CocoaMQTT(clientID: clientID, host: "broker.connectmq.com", port: 8883)
mqtt.username = "username"
mqtt.password = "password"
mqtt.enableSSL = true
mqtt.autoReconnect = true
mqtt.willMessage = CocoaMQTTMessage(topic: "testmqtt/ios/\(clientID)/status",
string: "offline", qos: .qos1, retained: true)
mqtt.didConnectAck = { [weak self] mqtt, ack in
guard ack == .accept else { return }
mqtt.publish("testmqtt/ios/\(clientID)/status", withString: "online", qos: .qos1, retained: true)
mqtt.subscribe("testmqtt/ios/demo/#", qos: .qos1)
DispatchQueue.main.async { self?.isConnected = true }
}
mqtt.didDisconnect = { [weak self] _, _ in
DispatchQueue.main.async { self?.isConnected = false }
}
mqtt.didReceiveMessage = { [weak self] _, message, _ in
let line = "\(message.topic): \(message.string ?? "")"
DispatchQueue.main.async { self?.lastMessages.append(line) }
}
}
func connect() { _ = mqtt.connect() }
func disconnect() { mqtt.disconnect() }
func send(_ text: String) {
mqtt.publish("testmqtt/ios/demo/chat", withString: text, qos: .qos1, retained: false)
}
}The retained online/offline pair with a Last Will gives other clients a reliable presence indicator: if the app vanishes without a clean disconnect, the broker publishes offline for it. MQTT Last Will explains the mechanics. Then drive the connection from the scene phase:
@main
struct DemoApp: App {
@StateObject private var mqtt = MQTTService(clientID: InstallID.value)
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
ContentView().environmentObject(mqtt)
}
.onChange(of: scenePhase) { _, phase in
switch phase {
case .active: mqtt.connect()
case .background: mqtt.disconnect()
default: break
}
}
}
}InstallID.value stands for your own helper that creates a UUID once and stores it, so the client ID stays stable across launches.
MQTT 5 with CocoaMQTT5
For MQTT 5, use the CocoaMQTT5 class. Its API mirrors the 3.1.1 client but adds property objects:
let mqtt5 = CocoaMQTT5(clientID: clientID, host: "broker.connectmq.com", port: 8883)
mqtt5.username = "username"
mqtt5.password = "password"
mqtt5.enableSSL = true
let connectProps = MqttConnectProperties()
connectProps.sessionExpiryInterval = 0
mqtt5.connectProperties = connectProps
_ = mqtt5.connect()
// after the connection is accepted:
let publishProps = MqttPublishProperties()
publishProps.contentType = "application/json"
mqtt5.publish("testmqtt/ios/demo/json", withString: "{\"ok\":true}",
qos: .qos1, DUP: false, retained: false, properties: publishProps)A non-zero sessionExpiryInterval asks the broker to keep the session — and queued QoS 1 messages — after the app disconnects. The shared test endpoint does not keep offline sessions, so test that behaviour against your own private broker. MQTT persistent sessions covers the rules.
iOS background limits
This is where most iOS MQTT projects get surprised. Shortly after the user leaves your app, iOS suspends it: no code runs, keep-alive PINGREQs stop, and after 1.5 × the keep-alive interval the broker considers the client gone and fires its Last Will. There is no general-purpose background mode that keeps an arbitrary socket alive.
| Need | What works on iOS |
|---|---|
| Finish a publish after the user leaves | beginBackgroundTask for a short grace period, then disconnect cleanly |
| Alert the user about an event | APNs push notification sent from your backend |
| Refresh data occasionally | BGTaskScheduler — opportunistic, timing decided by the system |
| Live data while in use | MQTT connection while the scene is active |
Design for reconnects instead of fighting the platform: connect on .active, disconnect on .background, and fetch any missed state on return — from retained messages, a persistent session or your API. When the network changes while the app is active, NWPathMonitor from the Network framework tells you when to trigger a reconnect without waiting for the keep-alive timeout. iOS background limits for MQTT covers the edge cases in depth.
Test it
- Run the app in the simulator or on a device and watch the connection state flip to connected.
- Open the online MQTT client, connect to the same broker and subscribe to
testmqtt/ios/#. You should see the retainedonlinestatus for your client ID. - Publish
pingtotestmqtt/ios/demo/hellofrom the browser and confirm it appears in the app. - Send the app to the background and wait. A clean disconnect clears nothing on its own, so publish
offlinebefore disconnecting if you want the browser to see it immediately; if the app is killed instead, the Last Will appears after the keep-alive timeout.
Watch your iOS app's messages live
Connecting to a broker on your Mac from a real device? iOS 14 and later show a Local Network permission prompt for LAN addresses, and your Info.plist needs an NSLocalNetworkUsageDescription entry.
CocoaMQTT gives Swift apps a complete MQTT client with very little code. Connect over TLS, keep the client in one long-lived object, follow the scene phase instead of fighting background suspension, and use retained presence messages plus a Last Will so the rest of your system always knows whether the app is really there.
Frequently asked questions
Can an iOS app stay connected to an MQTT broker in the background?
Not reliably. iOS suspends most apps shortly after they leave the foreground, and the socket stops responding to keep-alives. Use the connection while the app is active, request a short background task to finish work, and deliver urgent events through push notifications (APNs).
Does CocoaMQTT support MQTT 5?
Yes. CocoaMQTT 2.x includes a separate CocoaMQTT5 class with MQTT 5 connect, publish and subscribe properties such as session expiry, content type and user properties. The classic CocoaMQTT class speaks MQTT 3.1.1.
Why does my CocoaMQTT TLS connection fail with a self-signed certificate?
iOS only trusts certificates that chain to a CA in the system trust store. For a private CA, validate the server certificate yourself in the didReceiveTrust callback against your bundled CA. Setting allowUntrustCACertificate to true disables validation and must never ship in production.


