swiftwebsocketswiftuiobservableobjectstarscream

ObservableObject text not updating even with objectWillChange - multiple classes


I am currently writing a utility, and as part of this, a string is received (through WebSockets and the Starscream library) and the string value is then displayed in the SwiftUI view (named ReadingsView).

The structure of the code is as follows - there are two classes, the WSManager class which manages the WebSocket connections, and the GetReadings class which has the ObservableObject property, which manages and stores the readings.

When the string is received using the didReceive method in the WSManager class, it is decoded by the decodeText method in the WSManager class, which then calls the parseReceivedStrings method in the GetReadings class.

class WSManager : WebSocketDelegate {

    func didReceive(event: WebSocketEvent, client: WebSocket) {
        case .text(let string):
            // Decode the text
            DispatchQueue.main.async {
                self.decodeText(recvText: string)
                print("Received text: \(string)")
            }
            recvString = string
}

    func decodeText(recvText: String) {
        // If the message is allowed, then pass it to getReadings
        print("Decoding")
        if recvText.hasPrefix("A=") {
            getReadings.parseReceivedStrings(recvText: recvText, readingType: .allreadings)
            print("All readings received")
        } else if recvText.hasPrefix("T = ") {
            getReadings.parseReceivedStrings(recvText: recvText, readingType: .temperature)
        } else if recvText.hasPrefix("P = ") {
            getReadings.parseReceivedStrings(recvText: recvText, readingType: .pressure)
        } else if recvText.hasPrefix("H = ") {
            getReadings.parseReceivedStrings(recvText: recvText, readingType: .humidity)
        } else {
            print("Unrecognised string.")
        }
    }
}
enum ReadingType {
    case allreadings
    case temperature
    case pressure
    case humidity
}

class GetReadings: ObservableObject {


    let objectWillChange = ObservableObjectPublisher()

    @Published var temp: Float = 0.0 {
        willSet {
            print("Temp new = " + String(temp))
            objectWillChange.send()
        }
    }
    
    @Published var pressure: Float = 0.0 {
        willSet {
            print("Pressure new = " + String(pressure))
            objectWillChange.send()
        }
    }
    
    @Published var humidity: Float = 0.0 {
        willSet {
            print("Humidity new = " + String(humidity))
            objectWillChange.send()
        }
    }

    func getAll() {
        //print(readings.count)
        //print(readings.count)
        wsManager.socket.write(string: "get_all")
    }

    func parseReceivedStrings (recvText: String, readingType: ReadingType) {
        if readingType == .allreadings {
            // Drop first two characters
            let tempText = recvText.dropFirst(2)
            // Split the string into components
            let recvTextArray = tempText.components(separatedBy: ",")
            // Deal with the temperature
            temp = (recvTextArray[0] as NSString).floatValue
            // Pressure
            pressure = (recvTextArray[1] as NSString).floatValue
            // Humidity
            humidity = (recvTextArray[2] as NSString).floatValue
        }
    }
}

When the values are parsed, I would expect the values in the ReadingsView to update instantly, as I have marked the variables as @Published, as well as using the objectWillChange property to manually push the changes. The print statements within the willSet parameters reflect the new values, but the text does not update. In the ReadingsView code, I have compensated for this by manually calling the parseReceivedString method when the refresh button is pressed (this is used as part of the WebSocket protocol to send the request), but this causes the readings to be one step behind where they should be. Ideally, I would want the readings to update instantly once they have been parsed in the method described in the previous paragraph.

struct ReadingsView: View {
    @ObservedObject var getReadings: GetReadings
    var body: some View {
        VStack {
            Text(String(self.getReadings.temp))
            Text(String(self.getReadings.pressure))
            Text(String(self.getReadings.humidity))
            Button(action: {
                print("Button clicked")
                self.getReadings.getAll()
                self.getReadings.parseReceivedStrings(recvText: wsManager.recvString, readingType: .allreadings)
            }) {
                Image(systemName: "arrow.clockwise.circle.fill")
                    .font(.system(size: 30))
            }
        .padding()
        }
    }
}

I am wondering whether I have used the right declarations or whether what I am trying to do is incompatible with using multiple classes - this is my first time using SwiftUI so I may be missing a few nuances. Thank you for your help in advance.

Edited - added code

ContentView

struct ContentView: View {
    @State private var selection = 0
 
    var body: some View {
        TabView(selection: $selection){
            ReadingsView(getReadings: GetReadings())
                .tabItem {
                    VStack {
                        Image(systemName: "thermometer")
                        Text("Readings")
                    }
                } .tag(0)
            SetupView()
                .tabItem {
                    VStack {
                        Image(systemName: "slider.horizontal.3")
                        Text("Setup")
                    }
                }
                .tag(1)
        }
    }
}

Solution

  • If you are using ObservableObject you don't need to write objectWillChange.send() in willSet of your Published properties.

    Which means you can as well remove:

    let objectWillChange = ObservableObjectPublisher()
    

    which is provided by default in ObservableObject classes.

    Also make sure that if you're updating your @Published properties you do it in the main queue (DispatchQueue.main). Asynchronous requests are usually performed in background queues and you may try to update your properties in the background which will not work.

    You don't need to wrap all your code in DispatchQueue.main - just the part which updates the @Published property:

    DispatchQueue.main.async {
        self.humidity = ...
    }
    

    And make sure you create only one GetReadings instance and share it across your views. For that you can use an @EnvironmentObject.

    In the SceneDelegate where you create your ContentView:

    // create GetReadings only once here
    let getReadings = GetReadings()
    
    // pass it to WSManager
    // ...
    
    // pass it to your views
    let contentView = ContentView().environmentObject(getReadings)
    

    Then in your ReadingsView you can access it like this:

    @EnvironmentObject var getReadings: GetReadings
    

    Note that you don't need to create it in the TabView anymore:

    TabView(selection: $selection) {
        ReadingsView()
        ...
    }