SwiftUI - LazyVStack inside ScrollView gives unexpected behaviour

3k views Asked by At

I'm building a chat app using SwiftUI and I'm having difficulties getting a LazyVStack to work inside a ScrollView. Here are my questions:

Question 1

Where I have .id(message.id), why is this id required for scrollView.scrollTo(chatMessageViewModel.arrayOfMessages.last?.id, anchor: .bottom) to work, when the ForEach is assigning the same id using id: \.1.id? If I comment out the .id(message.id) line, scrollView.scrollTo doesn't work.

Question 2:

a) If I comment out the code .id(message.id),on an iPhone 6S, I get 11 messages loaded in view, however, the print statement print(message.messageContent) prints out 22 messages. Why does this happen?

b) Why are the print(message.messageContent) print statements not printed in order? I thought a LazyVStack would render in vertical order?

c) As I scroll down to reveal the 12th message, I get "Message 23" printed to the console instead of "Message 12". Why is this?

import SwiftUI


struct ChatMessageModel: Identifiable {
    var id: String
    var messageContent: String
}



class ChatMessageViewModel: ObservableObject {
    
    @Published var arrayOfMessages: [ChatMessageModel] = [ChatMessageModel(id: "1", messageContent: "Message 1"),
                                                          ChatMessageModel(id: "2", messageContent: "Message 2"),
                                                          ChatMessageModel(id: "3", messageContent: "Message 3"),
                                                          ChatMessageModel(id: "4", messageContent: "Message 4"),
                                                          ChatMessageModel(id: "5", messageContent: "Message 5"),
                                                          ChatMessageModel(id: "6", messageContent: "Message 6"),
                                                          ChatMessageModel(id: "7", messageContent: "Message 7"),
                                                          ChatMessageModel(id: "8", messageContent: "Message 8"),
                                                          ChatMessageModel(id: "9", messageContent: "Message 9"),
                                                          ChatMessageModel(id: "10", messageContent: "Message 10"),
                                                          ChatMessageModel(id: "11", messageContent: "Message 11"),
                                                          ChatMessageModel(id: "12", messageContent: "Message 12"),
                                                          ChatMessageModel(id: "13", messageContent: "Message 13"),
                                                          ChatMessageModel(id: "14", messageContent: "Message 14"),
                                                          ChatMessageModel(id: "15", messageContent: "Message 15"),
                                                          ChatMessageModel(id: "16", messageContent: "Message 16"),
                                                          ChatMessageModel(id: "17", messageContent: "Message 17"),
                                                          ChatMessageModel(id: "18", messageContent: "Message 18"),
                                                          ChatMessageModel(id: "19", messageContent: "Message 19"),
                                                          ChatMessageModel(id: "20", messageContent: "Message 20"),
                                                          ChatMessageModel(id: "21", messageContent: "Message 21"),
                                                          ChatMessageModel(id: "22", messageContent: "Message 22"),
                                                          ChatMessageModel(id: "23", messageContent: "Message 23"),
                                                          ChatMessageModel(id: "24", messageContent: "Message 24"),
                                                          ChatMessageModel(id: "25", messageContent: "Message 25")]
    
}



struct ChatMessagesView: View {
    
    @StateObject var chatMessageViewModel = ChatMessageViewModel()
    
    
    var body: some View {
        
        ScrollViewReader { scrollView in
            
            ScrollView (.vertical, showsIndicators: true) {
                
                LazyVStack (spacing: 0) {
                    
                    ForEach(Array(zip(chatMessageViewModel.arrayOfMessages.indices, chatMessageViewModel.arrayOfMessages)), id: \.1.id) { (index, message) in
                        
                        Text("Index is \(index) with message: \(message.messageContent)")
                            .padding(.vertical, 20)
                        

                            .id(message.id)
                        
                        
                            .onAppear {
                                print(message.messageContent)
                            }
                        
                    }
                }
            }
            .onAppear {
                scrollView.scrollTo(chatMessageViewModel.arrayOfMessages.last?.id, anchor: .bottom)
            }
        }
        .environmentObject(chatMessageViewModel)
    }
}
1

There are 1 answers

1
Guillermo Jiménez On

What you are seeing is the intented way the LazyVStack works, it will create and destroy views as needed LazyVStack, that's why the onAppear is called when you are scrolling, about the ordering as you can see the order is fine swiftui handles the views creation in its own cycle.

As I scroll down to reveal the 12th message, I get "Message 23" printed to the console instead of "Message 12". Why is this?

That's because the bounce animation when you reach the bottom, the upper message will be recreated.

struct ChatMessagesView: View {
    
    @StateObject var chatMessageViewModel = ChatMessageViewModel()
    
    
    var body: some View {
        
        ScrollViewReader { scrollView in
            
            ScrollView (.vertical, showsIndicators: true) {
                
                LazyVStack (spacing: 0) {
                    
                    ForEach(chatMessageViewModel.arrayOfMessages.indices, id: \.self) { index in
                        let message = chatMessageViewModel.arrayOfMessages[index]
                        Text("Index is \(index) with message: \(message.messageContent)")
                            .padding(.vertical, 20)
                            .id(message.id)
                            .onAppear {
                                print(message.messageContent)
                            }
                    }
                }
            }
            .onAppear {
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    withAnimation{
                    scrollView.scrollTo(chatMessageViewModel.arrayOfMessages.last?.id)
                    }
               }
            }
        }
        .environmentObject(chatMessageViewModel)
    }
}

I would add some animation and the timeout so the scroll looks smoother.