SwiftiSwift
SwiftiSwift

Reputation: 8687

SwiftUI View doesn't update

My View doesn't update when I change a property in an Array in the ObservableObject Class. I even declared a objectWillChange property and called it manually but the View just updated randomly or not how i want to. I don't understand this.

import Foundation
import SocketIO
import Combine

class SocketService: ObservableObject {
    static let instance = SocketService()

    let manager = SocketManager(socketURL: URL(RequestURL.base_url)!)
    let socket: SocketIOClient
//    let objectWillChange = ObservableObjectPublisher()

    @Published var allMessages: [Message] = []
//        {
//        willSet {
//            self.objectWillChange.send()
//        }
//    }
    @Published var writtenUsers: [PreviewMessage] = []
//        {
//        willSet {
//            self.objectWillChange.send()
//        }
//    }

    init() {
        socket = manager.defaultSocket
        setSocketEvents()
    }

    // This method is called
    func recieve_read_all_messages() {
        socket.on("recieve-read-all-messages") { (data, ack) in
            guard let arr = data as? [[String: Any]] else { return }
            guard let userID = arr[0]["userID"] as? Int else {
                print("no userID, \(#function), line: \(#line)"); return
            }

            // self.objectWillChange.send()
            for msg in self.allMessages {
                // Here im trying to change the property in the array
                msg.content.readStatus = .read
            }
    }    
}

Even when i directly change this property the view doesn't update

@EnvironmentObject private var socketService: SocketService

var body: some View {
    VStack {
         List(filteredMessages, id: \.content.uuid, rowContent: chatSpeechBubbleView)
         sendView
    }
}

private func chatSpeechBubbleView(forMessage message: Message) -> some View {
        ChatSpeechBubble(message: message)
    }

private var sendView: some View {
        Button(action: sendMessage) {
            SFSymbol(.paperplane_fill)
                .fontSize(20)
                .foregroundColor(.white)
                .rotate(.degrees(45))
                .padding(10)
                .padding(.trailing, 5)
                .backgroundColor(.blue)
                .clipCircle()
        }
        .padding(.bottom, 2)
    }

    func sendMessage() {
        for msg in socketService.allMessages {            
            msg.content.readStatus = .read
        }
    }

My other view where it should be updated:

struct DoubleCheckmark: View {
    var messageContent: MessageContent

    var body: some View {
        HStack(spacing: 0) {
            SFSymbol(.checkmark)
                .resizable()
                .scaledToFit()
            SFSymbol(.checkmark)
                .resizable()
                .scaledToFit()
                .padding(.leading, -9)
        }
        .height(13)
        .foregroundColor(self.messageContent.readStatus == .read ? .blue : .gray)
    }
}
struct ChatSpeechBubble: View {

    // MARK: - init variables
    var message: Message

    // MARK: - normal variables
    var ownSendMessage: Bool {
        message.fromUser.id == UDService.shared.user.id
    }

    // MARK: - Body
    var body: some View {
        messageContent
    }

    private var messageContent: some View {
        HStack(alignment: .bottom) {
            if message.content.text != nil {
                Text(message.content.text!)
                    .foregroundColor(.black)
            }
            if message.content.imageURL != nil {
                Spacer(minLength: 0)
            }
            Text(message.content.timeHourMinute)
                .font(.caption)
                .foregroundColor(.gray)

            if ownSendMessage {
                DoubleCheckmark(messageContent: self.message.content)
            }
        }
    }
}

Upvotes: 3

Views: 2686

Answers (1)

George
George

Reputation: 30341

Use struct, not class

The problem is that Message and its subtypes should be declared as a struct, rather than a class. Here's why.

Here is a basic example I made to demonstrate the difference:

Main part of code

class SomeObject: ObservableObject {
    @Published var users = [User(username: "George", password: "password")]
}


struct ContentView: View {
    @StateObject var object = SomeObject()

    var body: some View {
        Text("Hello world!")

        let _ = print("Body")
    }
}


let content = ContentView()
print(content.object)
content.object.users[0].password = "password123"

User as a struct

struct User {
    var username: String
    var password: String
}

User as a class

class User {
    var username: String
    var password: String

    init(username: String, password: String) {
        self.username = username
        self.password = password
    }
}
struct class
struct class

Have a look at the print logs at the bottom. With a struct, "Body" gets printed again, but why?

This is because classes are passed by reference, and structs are passed by value. This means that you can mutate the properties of the instance of a class, like so:

import SpriteKit

let sprite = SKSpriteNode()
sprite.position = CGPoint(x: 50, y: 50)
sprite.color = .red

If SKSpriteNode would have been a struct, with let properties, you would get an error for trying to change a let constant:

Cannot assign to property: '*' is a 'let' constant

Conclusion

So because Message is a class the instance isn't changing. But with a struct, the whole thing is changing. You need to change the whole thing (by using a struct) so @Published & StateObject/ObservableObject can observe the change.

Upvotes: 5

Related Questions