2

Here's my model:

struct ChatRoom: Identifiable, Equatable {
    static func == (lhs: ChatRoom, rhs: ChatRoom) -> Bool {
        lhs.id == rhs.id
    }
    
    struct LastMessage: Codable {
        let isSeen: Bool
        var isOfCurrentUser: Bool?
        let createdAt: Date
        let senderId, text: String
    }
    
    let id: String
    let userIds: [String]
    var lastMessage: LastMessage
    let otherUser: User
    let currentUserAvatarObject: [String : Any]?
    let isTyping: Bool
    var blockerIds: [String]
    let archiverIds: [String]
    let messages: Int
    let senderMessages: Int
    let receiverMessages: Int
}

I have the ChatRoomsListView view which initializes its ViewModel like so:

struct ChatRoomsListView: View {
    @StateObject private var viewModel = ViewModel()
    
    var body: some View {
        ZStack {
            Color.black.ignoresSafeArea()
            
            VStack {
                HStack {
                    Button {
                    } label: {
                        Image(systemName: "camera")
                            .scaledToFit()
                            .foregroundColor(.white)
                            .frame(width: 30, height: 30)
                            .padding(6)
                            .background(
                                Circle().foregroundColor(.white.opacity(0.15))
                            )
                    }
                    
                    Spacer()
                    
                    Text("Chats")
                        .foregroundColor(.white)
                        .offset(x: -20)
                    
                    Spacer()
                }
                
                ScrollView(.vertical) {
                    LazyVStack {
                        ForEach(viewModel.chatRooms) { chatRoom in
                            ChatRoomView(chatRoom: chatRoom)
                        }
                    }
                }
            }.padding(.vertical, 44)
            
            if viewModel.chatRooms.isEmpty {
                Text("It seems you haven’t chatted with anyone yet! That’s ok!")
                    .foregroundColor(.white)
                    .multilineTextAlignment(.center)
                    .padding(.horizontal)
            }
        }
    }
}

private struct ChatRoomView: View {
    let chatRoom: ChatRoom
    
    private var text: String {
        chatRoom.isTyping ? NSLocalizedString("Typing...", comment: "") : chatRoom.lastMessage.text
    }
    
    private var hasUnseenMessage: Bool {
        !chatRoom.lastMessage.isSeen && chatRoom.lastMessage.isOfCurrentUser == false
    }
    
    var body: some View {
        ZStack {
            Color.black.ignoresSafeArea()
            
            VStack(spacing: 0) {
                HStack {
                    if hasUnseenMessage {
                        Circle()
                            .frame(width: 10, height: 10)
                            .foregroundColor(.blue)
                    }
                    
                    VStack(alignment: .leading) {
                        Text(chatRoom.otherUser.username)
                            .foregroundColor(.white)
                        
                        Text(text)
                            .foregroundColor(.white)
                            .lineLimit(1)
                    }
                    
                    Spacer()
                    
                    if chatRoom.lastMessage.isSeen && chatRoom.lastMessage.isOfCurrentUser == true {
                        Image(systemName: "checkmark.circle")
                            .foregroundColor(.white)
                    }
                }.padding()
            }
        }
    }
}

And here’s the ViewModel:

extension ChatRoomsListView {
    class ViewModel: ObservableObject {
        @Published var chatRooms = [ChatRoom]()

    
    // -----------------------------
    
    @Injected private var chatRoomsService: ChatRoomsRepository
    @Injected private var currentUserService: CurrentUserRepository
    
    // -----------------------------
    
    init() {
        getChatRooms()
        subscribeToChatRoomUpdates()
    }
    
    private func getChatRooms() {
        guard let currentUser = currentUserService.user else { return }
        
        chatRoomsService.getChatRooms(currentUser: currentUser) { [weak self] chatRooms in
            self?.chatRooms = chatRooms
        }
    }
    
    private func subscribeToChatRoomUpdates() {
        guard let currentUser = currentUserService.user else { return }
        
        chatRoomsService.subscribeToChatRoomUpdates(currentUser: currentUser) { [weak self] chatRooms in
            DispatchQueue.main.async {
                for chatRoom in chatRooms {
                    if let index = self?.chatRooms.firstIndex(where: { $0.id == chatRoom.id }) {
                        self?.chatRooms[index] = chatRoom
                    } else {
                        self?.chatRooms.insert(chatRoom, at: 0)
                    }
                }
                
                self?.chatRooms.removeAll(where: { $0.archiverIds.contains(currentUser.id) })
                self?.chatRooms.sort(by: { $0.lastMessage.createdAt > $1.lastMessage.createdAt })
            }
        }
    }
}
}

My problem is that once the getChatRooms is called and changes the chatRooms array for the first time, after that, every time the subscribeToChatRoomUpdates is called doesn't redraw the ChatRoomView child view. On the other hand, ChatRoomsListView gets updated properly.

Why is that happening?

4
  • So when you are on ChatRoomView and any changes come for that chat room you want to redraw ChatRoomView? also can you show code of ChatRoom Commented Sep 13, 2022 at 11:20
  • Show a minimal example code: stackoverflow.com/help/minimal-reproducible-example What you show does not compile and cannot be run to find what is wrong with it. Also, how about trying to move class ViewModel: ObservableObject outside the extension ChatRoomsListView. Commented Sep 13, 2022 at 11:25
  • @NiravD I added the ChatRoom model. All changes come from the ViewModel, in real-time from the subscribeToChatRoomUpdates method. Commented Sep 13, 2022 at 14:42
  • @workingdogsupportUkraine I edited the code I provided... I think it should build now, with the exception of the dependencies (the repositories). Also, why are you suggesting to move the ViewModel outside the extension? Commented Sep 13, 2022 at 14:51

1 Answer 1

2

I think your implementation of Equatable - where it only compares id has broken the ability for the ChatView to detect a change to the let chatRoom: ChatRoom, so SwiftUI doesn't call body because the ids are unchanged between the old value and the new value. Try removing Equatable.

By the way, you need to unsubscribe to updates in the object's deinit or you might get a crash. And your use of self? is problematic, look into retain cycles in blocks.

Sign up to request clarification or add additional context in comments.

3 Comments

You are a genius!!! That was it! Quick question... why is the use of self? problematic? Isn’t it the way to solve retain cycles? I know that's a question irrelevant to my original post, but you got me curious! :)
This might help, see "Therefore, make sure to not use weak self if there’s work to be done with the referencing instance as soon as the closure gets executed." here avanderlee.com/swift/weak-self
That's a great answer! I would have never thought about specific Equatable conformance breaking the comparison mechanism... For those from the future: some use cases, like appending your type to a NavigationPath, require the type to conform to Hashable. Make sure that your implementation of static func == accounts for comparing the property you mutate that shall trigger a re-rendering of the view.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.