NEW: Start my new Ultimate Portfolio App course with a free Hacking with Swift+ trial! >>

Observed Object Not Updating View

Forums > macOS

Hi - I have a simple view with one text block that is references variable The variable is stored within my class and is @Published - see below.....

See at bottom of class "func receive"

When the network message is received I see it printed in the debug I then update the variable to match the message I then print the updated variable so I can see it has been changed But the text in my view does not change

Any Ideas

import SwiftUI
import Network

struct ContentView: View {

    //@EnvironmentObject var connect: Connect
    //@ObservedObject var connect = Connect.sharedInstance
    // @ObservedObject var connect = Connect()
    //@ObservedObject var globalVariable:BlobModel = BlobModel.sharedInstance
    @ObservedObject var connect = Connect.sharedInstance
    var body: some View {

        Text("Incoming Message - \(self.connect.score)")
            .padding(100)
            .onAppear(){
                let communication = Connect()
                let port2U = NWEndpoint.Port.init(integerLiteral: 1984)
                communication.listenUDP(port: port2U)
            }

    }// END OF BODY
}// END OF VIEW
import Foundation
import Network

class Connect: ObservableObject {
    static let sharedInstance = Connect()

    private var talking: NWConnection?
    private var listening: NWListener?

    @Published var incomingUDPMessage: String = ""
    @Published var score: String = "1"

    func listenUDP(port: NWEndpoint.Port) {
        do {
            self.listening = try NWListener(using: .udp, on: port)
            self.listening?.stateUpdateHandler = {(newState) in
                switch newState {
                case .ready:
                print("ready")
                default:
                break
                }
            }

            self.listening?.newConnectionHandler = {(newConnection) in
                newConnection.stateUpdateHandler = {newState in
                    switch newState {
                    case .ready:
                    print("new connection")
                    self.receive(on: newConnection)
                    default:
                    break
                    }
                }

                newConnection.start(queue: DispatchQueue(label: "new client"))
                }
            } catch {
        print("unable to create listener")
        }
        self.listening?.start(queue: .main)
    }// END OF FUNC - LISTEN TO UDP

    func receive(on connection: NWConnection) {
        connection.receiveMessage { (data, context, isComplete, error) in
            if let error = error {
                print(error)
                return
            }
            if let data = data, !data.isEmpty {
                let incomingString = String(decoding: data, as: UTF8.self)
                    print(incomingString)
                    self.score = incomingString
                    print(self.score)

            }
        }

    }// END OF FUNC - RECEIVE

}// END OF CLASS - CONNECT

   

The thing that occurs to me is that since the score property on the Connect singleton is being updated by the packet handler, it's not necessarily happening on the main UI thread but on some background interrupt handler.

Two things need to happen for the published score to be sent to the view:

  1. in the method where you update score you first need to call objectWillChange.send()
  2. that call needs to happen on the main UI thread, so hand a closure doing the notification and the actual updating off to DispatchQueue.main

   

Hi @sbeitzel Thanks for your advice. I've tried to implement your solution but no joy. I'm not sure if I'm doing this correctly.

At Top Of Class I Have...

import Combine

class Connect: ObservableObject {
    static let sharedInstance = Connect()

    private var talking: NWConnection?
    private var listening: NWListener?

    //@Published var messageData = MessageData()

    let objectWillChange = ObservableObjectPublisher()
    @Published var theMessage = "Still No Message" {
        didSet {
            objectWillChange.send()
        }
    }

then in the receive method I have...

 func receive(on connection: NWConnection) {
        connection.receiveMessage { (data, context, isComplete, error) in
            if let error = error {
                print(error)
                return
            }
            if let data = data, !data.isEmpty {
                let incomingString = String(decoding: data, as: UTF8.self)
                    print("Incoming String -\(incomingString)")
                    DispatchQueue.main.async() {
                        self.theMessage = incomingString
                        print("The Message - \(self.theMessage)")
                    }
            }
        }

    }

   

Hmm, I think I didn't explain clearly. What I meant was, go back to the first code that you posted, and then change the implementation of the receive method on Connect like this:

func receive(on connection: NWConnection) {
        connection.receiveMessage { (data, context, isComplete, error) in
            if let error = error {
                print(error)
                return
            }
            if let data = data, !data.isEmpty {
                let incomingString = String(decoding: data, as: UTF8.self)
                print(incomingString)
                // This is where we make sure that our notifications get sent on the main UI thread
                DispatchQueue.main.async { [weak self] in // this update shouldn't be the one thing that keeps the object alive
                    self?.objectWillChange.send()
                    self?.score = incomingString
                    print(self?.score ?? "Self has been garbage collected")
                }

            }
        }

    }// END OF FUNC - RECEIVE

If you're importing Foundation, you don't need to import Combine just to get DispatchQueue. And you don't need to provide your own objectWillChange property -- you get one provided for you by virtue of the @ObservableObject annotation.

   

Hi @sbeitzel Thanks for the explaination Unfortunately Still No Joy

i can see from the debug that score is being updated but not reflected in the view

Found loads of similar solutions online but none seem to work

Will keep trying

Current View

import SwiftUI
import Network

struct ContentView: View {

    //@EnvironmentObject var connect: Connect
    //@ObservedObject var connect = Connect.sharedInstance
    // @ObservedObject var connect = Connect()
    //@ObservedObject var globalVariable:BlobModel = BlobModel.sharedInstance
   // @ObservedObject var connect = Connect.sharedInstance
    @ObservedObject var connect = Connect.sharedInstance
    let communication = Connect()

    var body: some View {

        VStack {
            Text("Incoming Message - \(self.connect.theMessage)")
                .padding(100)
                .onAppear(){
                    // LISTENER
                    let port2U = NWEndpoint.Port.init(integerLiteral: 1984)
                    communication.listenUDP(port: port2U)
                }// ON APPEAR

            Button(action: {
                let host = NWEndpoint.Host.init("localhost")
                let port = NWEndpoint.Port.init("1984")
                self.communication.connectToUDP(hostUDP: host, portUDP: port!)
                self.communication.sendUDP("/cue/MyText/start")
            }) {
                Text("smoke")
            }

        }// END VSTACK
    }// END OF BODY
}// END OF VIEW

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

Current Class

import Foundation
import Network

class Connect: ObservableObject {
    static let sharedInstance = Connect()

    private var talking: NWConnection?
    private var listening: NWListener?

    //@Published var messageData = MessageData()
    @Published var theMessage = "Still No Message"

    // DEFINE LISTENER
    func listenUDP(port: NWEndpoint.Port) {
        do {
            self.listening = try NWListener(using: .udp, on: port)
            self.listening?.stateUpdateHandler = {(newState) in
                switch newState {
                case .ready:
                print("ready")
                default:
                break
                }
            }

            self.listening?.newConnectionHandler = {(newConnection) in
                newConnection.stateUpdateHandler = {newState in
                    switch newState {
                    case .ready:
                    print("new connection")
                    self.receive(on: newConnection)
                    default:
                    break
                    }
                }

                newConnection.start(queue: DispatchQueue(label: "new client"))
                }
            } catch {
        print("unable to create listener")
        }
        self.listening?.start(queue: .main)
    }// END OF FUNC - LISTEN TO UDP

    // DEFINE ON RECEIVE
    func receive(on connection: NWConnection) {
        connection.receiveMessage { (data, context, isComplete, error) in
            if let error = error {
                print(error)
                return
            }
            if let data = data, !data.isEmpty {
                let incomingString = String(decoding: data, as: UTF8.self)
                print("Incoming String -\(incomingString)")
                    DispatchQueue.main.async { [weak self] in
                        self?.objectWillChange.send()
                        self?.theMessage = incomingString
                        print(self?.theMessage ?? "Self Got Binned")
                }
            }
        }

    }// END OF FUNC - RECEIVE

    // DEFINE TALKER
    func connectToUDP(hostUDP:NWEndpoint.Host,portUDP:NWEndpoint.Port) {
        self.talking = NWConnection(host: hostUDP, port: portUDP, using: .udp)
        self.talking?.stateUpdateHandler = { (newState) in
            switch (newState) {
                case .ready:
                break
                default:
                break
            }
        }
        self.talking?.start(queue: .main)
    }// END OF DEFINE TALKER

    // SEND A MESSAGE
    func sendUDP(_ content: String) {
        let contentToSendUDP = content.data(using: String.Encoding.utf8)

        self.talking?.send(content: contentToSendUDP, completion: NWConnection.SendCompletion.contentProcessed(({ (NWError) in
            if (NWError == nil) {
                // code
            } else {
                print("ERROR! Error when data (Type: String) sending. NWError: \n \(NWError!) ")
            }
        })))
    }

}// END OF CLASS - CONNECT

   

SOLVED Firstly thanks to @sbeitzel for help with "main thread" etc

As mentioned we still couldnt get the view to update.

The solution was staring us in the face... In the "Connect" class we create a singleton instance when at the top we declare..

static let sharedInstance = Connect()

In the content view we create a second instance of Connect() when we declare...

let communication = Connect()

SOLUTION -> Remove the redeclaration of Connect() in ContentView This will flag errors in the button code below but thats easy to fix Replace any mention of "communication" with "connect"

   

Hacking with Swift is sponsored by RevenueCat

SPONSORED Building and maintaining in-app subscription infrastructure is hard. Luckily there's a better way. With RevenueCat, you can implement subscriptions for your app in hours, not months, so you can get back to building your app.

Try it for free

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.