Search code examples
swiftoopswiftuiobservablemainview

SwiftUI View not being refreshed properly


I am struggling recently with my app and I think it is because I am missing something in the conception. I am fairly new to the OOP and probably it is my fault but I can't say where it is.

The thing is that I have different views in my app which display properties from an @Observable class. For some reason in the different view I see different values displayed while all of them have to be the same since it comes from the same property of the observable class. I am using shared instance, which I know is not correct but I have tried with @Environment and directly to inject the class instance but it is all the same. Clearly it is my fault, and I would highly appreciate where I am wrong.

Here is the case - I will describe it simply for the sake of the principle. I have a class which handles UDP socket connection. In that class I receive a string which is going to be processed further more. String is being received every second.

class UDPHandler {
      
      //use shared instance - I know it is not correct methods 
      var fromNMEA = NMEAReader.share

      func udpSocket(_ sock: GCDAsyncUdpSocket, didReceive data: Data, fromAddress address: Data, withFilterContext filterContext: Any?) {
    if let receivedString = String(data: data, encoding: .utf8) {
        
        //here I receive the string, this function is being triggered and the string is pass to a function in another class which will handle further actions with the string
        fromNMEA.processRawString(rawData: receivedString)
        
    }

}

Then I have the @Observable class which stores all the properties to be displayed and the function that processes the string that comes from the UDPHandler

@Observable class NMEAReader {

//create shared instance
static let shared = NMEAReader()

var boatSpeedLag = Double() //value to be displayed 


//there are a lot more values but it doesn't work even with one

func processRawString(rawData: String){

//here I make different manipulations over the string, take the values, convert them to Doubles, calculate different things and update all the properties declared

}
//even more support functions here ... and so on.



}

Then it comes the view part. I have couple of views which I call in the ContentView

struct ContentView {

//shared instance so I can get to the observable class
var nmea = NMEAReader.shared

var body: some View {



//portrait view, where I pass the shared instance

            PortraitView(nmea: nmea).opacity((horizontalSizeClass == .compact && verticalSizeClass == .regular) ? 1 : 0)

   }
}

struct PortraitView {

     VStack{
         //this is a separate settings menu from where I start the socket communication
         NavigationLink(destination: SettingsMenu()) {
                     Image(systemName: "gear")
                       .dynamicTypeSize(.xxxLarge)
                       .foregroundStyle(Color(UIColor.systemGray))
         }


         //all views are of the same type, with a single Text line where I just display the value. They are so many, so I can compare and see what is going on, in reality views are different. I pass to them the shared instance of the NMEAReader class so they can update when it changes
         HStack{
            SmallDisplayCell(nmea: nmea)
            SmallDisplayCell(nmea: nmea)
         }
         HStack{
            SmallDisplayCell(nmea: nmea)
            SmallDisplayCell(nmea: nmea)
            SmallDisplayCell(nmea: nmea)

         }
         HStack{
            SmallDisplayCell(nmea: nmea)
            SmallDisplayCell(nmea: nmea)
            SmallDisplayCell(nmea: nmea)
         }
     }
}

Then I have the SettingsMenu() where I just start / stop the communication with the outside world.

    struct SettingsMenu: View {
    
    //initialize an instance of the UDPHandler, so I can start/stop the communication
    private var connector = UDPHandler()
    
    var body: some View {
        VStack(){

                Button("Start") {
                    connector.start()
                }
                Button("Stop") {
                    connector.stop()
                }

            
        }
    }
}

SmallDisplayView is made very simple for the test, however it is not so complicated in the real app too.

struct SmallDisplayCell: View {

var nmea: NMEAReader

var value = Double()

var body: some View {

    Text(String(nmea.boatSpeedLag))
      .frame(width: width, height: width, alignment: .center)

    }//END OF BODY
}//END OF STRUCTURE

And that is it. Yet I can't get all the views of the display cell to be updated simultaneously. Please see the picture, so you can get idea of what I am talking about.

Any ideas and advices on the OOP and how can I implement that structure would be much appreciated, thanks.

enter image description here


Solution

  • So, after I read the article and implemented the suggestions from workingdog support Ukraine I ended up with the following configuration:

    1. Moved class UDPHandler functionality inside the NMEAReader() which is my observable class.
    2. Included the NMEAReader() instance at the Environment, from where I can access it within different views in my app.
    3. calling the function udpSocket which receives the data within a DispatchQueue.main.async {} block. Otherwise views are not updated synchronously and it becomes a mess.

    After all of the above it works like a charm, exactly as expected. Not very surprised, though, suggestions and advices from workingdog support Ukraine are always correct and educational, thanks again.

    This is the result that I wanted to get:

    enter image description here