swiftuiraspberry-pimqttraspberry-pi3cocoamqtt

SwiftUI & CocoaMQTT Server - How to connect without using button?


I am building an app that will read a value over the Wi-Fi, provided by a Raspberry Pi and I am using MQTT server and client. Using UIKit was easy and the delegate methods were very user friendly in terms of the control over the matt server/client. However, I am building the app with SwiftUI, I have a view of an anemometer (wind sensor) display and I would like to get a NMEA and use the values from the string to display them on the view. I am struggling to maintain connection with the broker without using buttons in my view. I would like when the view is loaded to connect, subscribe to the topic, get the messages (which are being broadcasted constantly over the wi-fi) and display them. When I am using buttons for that it seems fine but if I use onAppear method it can't connect to the RPi and receive messages. Any good advices?. Here is my code:

//
//  ContentView.swift
//  ExtasyCompleteNavigation
//
//  Created by Vasil Borisov on 13.06.23.
//

import SwiftUI
import CocoaMQTT

struct ContentView: View {
    
    let mqttClient = CocoaMQTT(clientID: "Navigation", host: "raspberrypi.local", port: 1883)
    
    var body: some View {
        
        VStack {
            AnemometerView()
            Button("Connect"){
                    _ = mqttClient.connect()
                }
            Button("Subscribe"){
                mqttClient.subscribe("windData")
            }
            Button("Get Message"){
                mqttClient.didReceiveMessage = { mqtt, message, id in
                    print((message.string!))
                }
            }
            Button("Disconnect"){
                mqttClient.disconnect()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

I tried to use .onAppear method, so it will connect once the view is loaded but it doesn't seem to work. If i check the server log it tells me that can't connect due to protocol error. Anybody experienced that?

EDIT-1 So, this is the code that I use in the .onAppear() method. I know that it would be better if I connect first, then in the didConnectAck method subscribe to the topic and then somehow receive the message in the didReceiveMessage closure. I just don't know how? Is there any work arounds which will mimic delegate methods as I used to use in UIKit? Or better way?

//
//  ContentView.swift
//  ExtasyCompleteNavigation
//
//  Created by Vasil Borisov on 13.06.23.
//

import SwiftUI
import CocoaMQTT

struct ContentView: View {
    
    let mqttClient = CocoaMQTT(clientID: "Navigation", host: "raspberrypi.local", port: 1883)
    
    var windAngle = NavigationData()
    
    var body: some View {
        
        VStack {
            AnemometerView()
                .onAppear(){
                    _ = mqttClient.connect()
                    mqttClient.subscribe("windData")
                    mqttClient.didReceiveMessage = { mqtt, message, id in
                        if let safeMessage = message.string {
                            windAngle.windAngleString = safeMessage
                            print(windAngle.windAngleString)
                            print(windAngle.windAngleTest)
                        }
                    }
                }
//            Button("Connect"){
//
//                }
//            Button("Subscribe"){
//
//            }
//            Button("Get Message"){
//
//            }
            Button("Disconnect"){
                mqttClient.disconnect()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Solution

  • Is there any work arounds which will mimic delegate methods as I used to use in UIKit? Or better way?

    This is the approach (untested) I would use to connect to a Raspberry Pi using MQTT.

    The important parts here are: separate the logic from the UI, hence use a MQTTManager and have this MQTTManager be a CocoaMQTTDelegate as well as an ObservableObject to listen to the messages received.

    struct ContentView: View {
        @StateObject var mqttManager = MQTTManager() // <--- here
        
        var body: some View {
            VStack {
                Text(mqttManager.message)  // <-- for testing
                // AnemometerView()
                Button("Disconnect"){
                    mqttManager.mqttClient.disconnect()
                }
            }
            .onAppear {
                mqttManager.mqttClient.connect()
                // --- could be in didConnectAck, adjust as required
                mqttManager.mqttClient.subscribe("windData")
            }
        }
    }
    
    // something like this
    class MQTTManager: ObservableObject, CocoaMQTTDelegate {    // <--- here
        
        let mqttClient = CocoaMQTT(clientID: "Navigation", host: "raspberrypi.local", port: 1883)
        
        @Published var message = ""
        
        init() {
            // ....
            mqttClient.keepAlive = 60
            mqttClient.delegate = self  // <--- here
        }
        
        // .... CocoaMQTTDelegate functions ....
    
        func mqtt(_ mqtt: CocoaMQTT, didSubscribeTopics success: NSDictionary, failed: [String]) {
            print("topic: \(success)")
        }
        
        func mqtt(_ mqtt: CocoaMQTT, didUnsubscribeTopics topics: [String]) {
            print("topic: \(topics)")
        }
        
        func mqtt(_ mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) {
            print("ack: \(ack)")
        }
        
        func mqtt(_ mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) {
            print("message published: \(message.string.description), id: \(id)")
        }
        
        func mqtt(_ mqtt: CocoaMQTT, didPublishAck id: UInt16) {
            print("id: \(id)")
        }
        
        func mqtt(_ mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16) {
            print("message received: \(message.string.description), id: \(id)")
            if let str = message.string { // <--- here or something like this
                self.message = str
            }
        }
        
        func mqtt(_ mqtt: CocoaMQTT, didSubscribeTopics success: NSDictionary, failed: [String]) {
            print("didSubscribeTopics")
        }
        
        func mqtt(_ mqtt: CocoaMQTT, didUnsubscribeTopic topic: String) {
            print("didUnsubscribeTopic")
        }
        
        func mqttDidPing(_ mqtt: CocoaMQTT) {
        }
        
        func mqttDidReceivePong(_ mqtt: CocoaMQTT) {
        }
        
        func mqttDidDisconnect(_ mqtt: CocoaMQTT, withError err: Error?) {
            print("\(err.description)")
        }
    }