Сохранять позицию прокрутки в ScrollView при добавлении новых элементов

Вопрос или проблема

В данный момент я сталкиваюсь с проблемами в ChatView моего приложения. Это пример кода того, чего я хочу добиться. Он имеет функциональность, такую как загрузка более старых сообщений, когда пользователь почти достиг верха, и загрузка более новых сообщений, когда он внизу.

Проблема в том, что когда пользователь загружает более новые сообщения, scrollview постоянно прокручивается вниз, и это происходит рекурсивно. Это была та же проблема при загрузке более старых сообщений (он постоянно прокручивался вверх), но я исправил это с помощью модификатора flipped(). Однако проблема с нижней частью все еще остается.

struct ChatMessage: View {
    
    let text: String
    
    var body: some View {
        HStack {
            Text(text)
                .foregroundStyle(.white)
                .padding()
                .background(.blue)
                .clipShape(
                    RoundedRectangle(cornerRadius: 16)
                )
                .overlay(alignment: .bottomLeading) {
                    Image(systemName: "arrowtriangle.down.fill")
                        .font(.title)
                        .rotationEffect(.degrees(45))
                        .offset(x: -10, y: 10)
                        .foregroundStyle(.blue)
                }
            Spacer()
        }
        .padding(.horizontal)
    }
}



struct Message : Identifiable,Equatable {
    var id: Int
    var text: String
}

struct GoodChatView: View {
    
    
    @State var messages: [Message] = []
    
    @State private var scrollViewProxy: ScrollViewProxy? // Хранит прокси
    
    @State var messageId: Int?
    
    var body: some View {
        ScrollViewReader { scrollView in
            ScrollView {
                
                LazyVStack {
                    
                    ForEach(messages, id: \.id) { message in
                        ChatMessage(text: "\(message.text)")
                            .flippedUpsideDown()
                            .onAppear {
                                if message == messages.last {
                                    print("старые данные")
                                    loadMoreData()
                                }
                                if message == messages.first {
                                    print("новые данные")
                                    loadNewData()
                                }
                            }
                            
                    }
                }
                .scrollTargetLayout()
            }
            .flippedUpsideDown()
            .scrollPosition(id: $messageId)
            .onAppear {
               
                for i in 1...20 {
                    let message = Message(id: i, text: "\(i)")
                    messages.append(message)
                }
                
                messageId = messages.first?.id
            }
            
        }
    }
    
    func loadMoreData() {
    
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            let count = messages.count
            
            var newMessages: [Message] = []
            
            for i in count+1...count+20 {
                let message = Message(id: i, text: "\(i)")
                newMessages.append(message)
            }
            
            
            messages.append(contentsOf: newMessages)

        }
    }
    
    func loadNewData() {
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            
            
            let count = messages.count
            
            var newMessages: [Message] = []
            
            for i in count+1...count+20 {
                let message = Message(id: i, text: "\(i)")
                newMessages.append(message)
            }
            
            newMessages = newMessages.reversed()
            
            messages.insert(contentsOf: newMessages, at: 0)
            
        }
        
    }
}

struct FlippedUpsideDown: ViewModifier {
    func body(content: Content) -> some View {
        content
            .rotationEffect(.radians(Double.pi))
            .scaleEffect(x: -1, y: 1, anchor: .center)
    }
}

extension View {
    func flippedUpsideDown() -> some View {
        modifier(FlippedUpsideDown())
    }
}

Буду признателен за любую помощь. Если есть другие способы достичь этого, дайте знать 🙂

Ответ или решение

Чтобы сохранить положение прокрутки в компоненте ScrollView при добавлении новых сообщений в вашем приложении чата, необходимо применять несколько принципов и подходов. В вашем коде, необходимо учесть несколько моментов, чтобы избежать бесконечной прокрутки вверх и вниз и обеспечить правильное отображение новых сообщений, а также удобство пользователя.

1. Понимание проблемы

Ваш код использует ScrollView и LazyVStack для отображения сообщений в чате. При добавлении новых сообщений, ваш интерфейс прокручивает содержимое автоматически. Это становится проблемой, когда вы загружаете новые сообщения, так как прокрутка ведёт к некорректному поведению. Это происходит из-за того, что при добавлении сообщений интерфейс продолжает обновляться, вызывая дополнительные действия прокрутки.

2. Сохранение позиции прокрутки

Чтобы избежать проблемы с непрерывной прокруткой, необходимо сохранить позицию прокрутки перед добавлением новых сообщений и восстанавливать её после. Один из способов – использовать ScrollViewReader совместно со свойством scrollTo.

3. Корректировка кода

struct GoodChatView: View {
    @State var messages: [Message] = []
    @State private var scrollViewProxy: ScrollViewProxy? // Храните прокси
    @State var messageId: Int?

    var body: some View {
        ScrollViewReader { scrollView in
            ScrollView {
                LazyVStack {
                    ForEach(messages, id: \.id) { message in
                        ChatMessage(text: "\(message.text)")
                            .flippedUpsideDown()
                            .onAppear {
                                if message == messages.last {
                                    loadMoreData()
                                } else if message == messages.first {
                                    loadNewData()
                                }
                            }
                    }
                }
                .scrollTargetLayout()
            }
            .flippedUpsideDown()
            .onAppear {
                for i in 1...20 {
                    let message = Message(id: i, text: "\(i)")
                    messages.append(message)
                }
                messageId = messages.first?.id
            }
        }
    }

    func loadMoreData() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            let count = messages.count
            var newMessages = (count + 1...count + 20).map { Message(id: $0, text: "\($0)") }
            messages.append(contentsOf: newMessages)
            scrollToBottom() // Вернуться к нижней части
        }
    }

    func loadNewData() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            let count = messages.count
            var newMessages = (count + 1...count + 20).map { Message(id: $0, text: "\($0)") }
            messages.insert(contentsOf: newMessages.reversed(), at: 0)
            scrollToTop() // Вернуться к верхней части
        }
    }

    private func scrollToBottom() {
        DispatchQueue.main.async {
            if let lastId = messages.last?.id {
                messageId = lastId
            }
        }
    }

    private func scrollToTop() {
        DispatchQueue.main.async {
            if let firstId = messages.first?.id {
                messageId = firstId
            }
        }
    }
}

struct FlippedUpsideDown: ViewModifier {
    func body(content: Content) -> some View {
        content
            .rotationEffect(.radians(Double.pi))
            .scaleEffect(x: -1, y: 1, anchor: .center)
    }
}

extension View {
    func flippedUpsideDown() -> some View {
        modifier(FlippedUpsideDown())
    }
}

4. Оптимизация

В приведенном коде были добавлены функции scrollToBottom и scrollToTop, которые устанавливают текущую позицию прокрутки. Это помогает избегать проблем с непрерывной прокруткой при обновлении списка сообщений.

Заключение

Данный подход позволит вам сохранить положение прокрутки при загрузке новых сообщений и предотвратить нежелательное поведение интерфейса. Вы сможете создать более удобный и отзывчивый чат, сохраняя при этом целостность пользовательского опыта. Пользуйтесь данными рекомендациями, чтобы обеспечить качественное взаимодействие с вашим приложением.

Оцените материал
Добавить комментарий

Капча загружается...