Tutorials

MQTT in iOS with Swift (CocoaMQTT)

Use MQTT in an iOS app with Swift and CocoaMQTT: connect over TLS or WebSockets, publish and subscribe, SwiftUI integration, MQTT 5 and iOS background limits.

Updated · 9 min read

MQTT in iOS with Swift (CocoaMQTT)

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:

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:

MQTTDemo.swift
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:

Publish
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 didReceiveTrust callback, evaluate the SecTrust against your bundled CA certificate with SecTrustSetAnchorCertificates and SecTrustEvaluateWithError, and call the completion handler with the result. allowUntrustCACertificate = true skips validation entirely — handy for a quick lab test, dangerous anywhere else.
  • Mutual TLS. Load your client identity from a PKCS#12 file with SecPKCS12Import and set it via mqtt.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:

WebSocket transport
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:

MQTTService.swift
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:

DemoApp.swift
@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:

MQTT5.swift
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.

NeedWhat works on iOS
Finish a publish after the user leavesbeginBackgroundTask for a short grace period, then disconnect cleanly
Alert the user about an eventAPNs push notification sent from your backend
Refresh data occasionallyBGTaskScheduler — opportunistic, timing decided by the system
Live data while in useMQTT 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

  1. Run the app in the simulator or on a device and watch the connection state flip to connected.
  2. Open the online MQTT client, connect to the same broker and subscribe to testmqtt/ios/#. You should see the retained online status for your client ID.
  3. Publish ping to testmqtt/ios/demo/hello from the browser and confirm it appears in the app.
  4. Send the app to the background and wait. A clean disconnect clears nothing on its own, so publish offline before 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.