NavigationLink page is automatically dismissed when the parent view is updated
Asked Answered
V

1

6

I am trying to create a chat app using SwiftUI and Firebase. My current approach involves using chatrooms documents and within each chatroom have a subcollection of messages.

Therefore, I am using two different models to fetch the chatroom data and the message data. At first I fetch the data regarding the chatrooms to have a list view. Once the user clicks on one of the chats, a NavigationLink is triggered which brings them to the details of the chat, which is when I fetch the messages the data.

What I would like to do is to keep track of the last message sent so that it can be visible from the list of chatrooms. This is currently done by updating the chatroom model from inside the NavigationLink. Since I have a real time listener on the data from firebase, this triggers an update on the parent view, which reloads and dismisses the NavigationLink.

My desired behaviour is to not dismiss the NavigationLink even though the parent view updates.

I would highly appreciate any help.

Parent View

struct ChatListView: View {
var group: Group

@EnvironmentObject var sessionStore: SessionStore
@EnvironmentObject var groupModel: GroupModel

@ObservedObject var chatroomModel = ChatRoomModel()
@State var joinModal = false

init(group: Group) {
    self.group = group
    
    chatroomModel.fetchData(groupID: group.id)
}

var body: some View {
    NavigationView {
        ZStack {
            BackgroundView()
            
            VStack {
                ScrollView {
                    ForEach(chatroomModel.chatrooms) { chatroom in
                        NavigationLink (destination: ChatMessagesView(chatroom: chatroom, chatroomModel: chatroomModel)) {
                    (...)
                    }
                }
                .padding()
                .frame(maxWidth: .infinity)
            }
            
        }
        .navigationBarTitle("Chats")
    }

Child View

struct ChatMessagesView: View {
let chatroom: ChatRoom
let chatroomModel: ChatRoomModel
@EnvironmentObject var sessionStore: SessionStore


@ObservedObject var messagesModel = MessagesModel()

@State var messageField = ""

init (chatroom: ChatRoom, chatroomModel: ChatRoomModel) {
    self.chatroom = chatroom
    self.chatroomModel = chatroomModel
    messagesModel.fetchData(documentID: self.chatroom.id ?? "erflwekrjfne")
}

var body: some View {
    GeometryReader { geo in
        
        VStack {
            
            CustomScrollView(scrollToEnd: true) {
                LazyVStack (spacing: 20){
                    ForEach(messagesModel.messages) { message in
                        MessageView(message: message)
                    }
                }
            }
            .padding()

            
            HStack {
                TextField("Type here...", text: $messageField)
                    .textFieldStyle(TextInputStyle())
                    .padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
                
                Button(action: {
                    if messageField != "" {
                        messagesModel.sendMessage(content: messageField, documentID: chatroom.id!, displayName: sessionStore.session?.firstName)

                        * Updates parent view and triggers problem *
                        chatroomModel.updateChatroom(id: chatroom.id!, fields: ["lastMessage": messageField, "lastSender": sessionStore.session?.firstName ?? "Anon"])
                        
                        messageField = ""
                    }
                }, label: {
                    HStack {
                        Image(systemName: "paperplane.circle")
                            .resizable()
                            .frame(width: 50, height: 50, alignment: .center
                    }
                })
            }
            .padding(EdgeInsets(top: 0, leading: 10, bottom: 10, trailing: 10))
        }
        .navigationBarTitle(chatroom.title ?? "Chat", displayMode: .inline)
        
    }
}

}

Valenevalenka answered 29/7, 2021 at 16:30 Comment(4)
Chang ObservedObject to StateObject. Get rid of the custom inits and fetch at onAppearIow
developer.apple.com/documentation/swiftui/…Iow
Does this answer your question? SwiftUI Binding to parent view re-renders child viewIow
Hi! I applied your suggested changes but unfortunately it still doesn't work... Is passing the model chatroom object to the child view the correct way of going about this?Valenevalenka
M
0

Ensure you have unique identifier if you're using ForEach

set unique id to your ForEach loop:

ForEach(msgViewModel.msgs.sorted(by: {$0.key < $1.key}), id:\.self.id) {
. . .
}

If you're wrapping your list in Array then you're id will be \.element.id:

ForEach(Array(msgViewModel.msgs.enumerated()), id: \.element.id) { index, page in
. . .
}

for me, this fixed dismissal of the first child. if you only have one child, then this is probably all you need.

If you have further nested NavigationLink

add .isDetailLink(false) modifier to the NavigationLink

Here's demo code with the fix:

import SwiftUI

@main
struct UIExperimentApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    @StateObject var msgViewModel = MsgViewModel()
    
    var body: some View {
        NavigationView {
            List {
                /// use this if you're wrapping the list in `Array`:  ForEach(Array(msgViewModel.msgs.enumerated()), id: \.element.id) { index, page in
                ForEach(msgViewModel.msgs, id:\.self.id) { msg in
                    NavigationLink(destination: ChildOneView(msgViewModel: msgViewModel, msg: msg)) {
                        Text("Msg read: \(msg.read ? "Yes" : "No")")
                            .padding()
                    }
                    .isDetailLink(false)
                }
            }
        }
    }
}

struct ChildOneView: View {
    @ObservedObject var msgViewModel: MsgViewModel
    var msg: Msg
    
    var body: some View {
        VStack {
            Text("Child One Message: \(msg)")
            
            NavigationLink(destination: ChildTwoView(msgViewModel: msgViewModel, msg: msg)) {
                Text("Go two Child two")
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
            .isDetailLink(false)
        }
    }
}

struct ChildTwoView: View {
    @ObservedObject var msgViewModel: MsgViewModel
    var msg: Msg
    
    var body: some View {
        VStack {
            Text("Child two: Message \(msg)")
                .padding(40)
            
            Button("Mark as Read") {
                if let index = msgViewModel.msgs.firstIndex(where: { $0.id == msg.id }) {
                    msgViewModel.msgs[index].read.toggle()
                }
            }
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
        }
    }
}

class MsgViewModel : ObservableObject {
    @Published var msgs = [
        Msg(id: "id1", read:false, content: "Hello"),
        Msg(id: "id2", read:false, content: "World")
    ]
}

struct Msg {
    var id : String
    var read = false
    var content : String
}

Links that helped me:

  1. Nested NavigationLinks not working properly in latest SwiftUI
  2. SOLVED: Pop multiple nested views using NavigationLink and isActive
  3. Dismiss multiple Views in a NavigationView
  4. In SwiftUI, iOS15, 2nd level NavigationLink, isActive is not working
Maryleemarylin answered 16/10 at 23:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.