Sotiris Kaniras
Sotiris Kaniras

Reputation: 680

SwiftUI - @Published array doesn’t update child view

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?

Upvotes: 2

Views: 161

Answers (1)

malhal
malhal

Reputation: 30571

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.

Upvotes: 2

Related Questions