You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI聊天视图自动滚动至底部问题求助

SwiftUI ChatView 自动滚动到底部解决方案

要实现添加新消息和视图出现时自动滚动到底部,需要修正ScrollViewReader的使用方式,并监听数据源变化,以下是具体步骤和修改后的代码:

关键修改点

  • 利用ScrollViewReader提供的ScrollViewProxy对象执行滚动操作
  • 给消息列表底部添加唯一标识(如"bottom"),作为滚动目标锚点
  • 在视图首次出现时触发滚动
  • 监听目标聊天的消息数组变化,新消息添加后自动滚动
  • 监听加载状态变化,确保显示等待提示时也能滚动到底部

修改后的完整代码

import SwiftUI

struct ChatView: View {
    var chatId: UUID
    @Environment(\.colorScheme) var colorScheme
    @FocusState private var isEditing
    @EnvironmentObject var chatViewModel: ChatViewModel
    
    // 快速获取当前聊天对象
    private var currentChat: Chat? {
        chatViewModel.chats.first { $0.id == chatId }
    }
    
    var body: some View {
        VStack {
            ScrollViewReader { proxy in
                ScrollView {
                    VStack {
                        ForEach(currentChat?.messages ?? []) { message in
                            messageView(message: message)
                        }
                        if chatViewModel.isWaitingForResponse {
                            HStack {
                                ProgressView("Waiting for response...")
                                    .progressViewStyle(.circular)
                                    .tint(Theme.secondaryColor)
                                    .padding()
                                Spacer()
                            }
                        }
                        // 添加底部锚点,确保滚动到最底部
                        Color.clear.id("bottom")
                    }
                    .padding()
                }
                // 视图出现时滚动到底部
                .onAppear {
                    withAnimation {
                        proxy.scrollTo("bottom", anchor: .bottom)
                    }
                }
                // 消息数组变化时自动滚动
                .onChange(of: currentChat?.messages) { _ in
                    withAnimation {
                        proxy.scrollTo("bottom", anchor: .bottom)
                    }
                }
                // 加载状态变化时滚动到底部
                .onChange(of: chatViewModel.isWaitingForResponse) { _ in
                    withAnimation {
                        proxy.scrollTo("bottom", anchor: .bottom)
                    }
                }
            }
            HStack {
                TextField("Enter a message...", text: $chatViewModel.currentTextInput, axis: .vertical)
                    .padding()
                    .lineLimit(5)
                    .onAppear {
                        isEditing = true
                    }
                    .focused($isEditing)
                    .overlay(
                        RoundedRectangle(cornerRadius: 30)
                            .stroke(Theme.secondaryColor)
                    )
                if !chatViewModel.currentTextInput.isEmpty {
                    Button {
                        guard !chatViewModel.currentTextInput.isEmpty else { return }
                        chatViewModel.sendMessage(chatId: chatId, message: chatViewModel.currentTextInput)
                    } label: {
                        Image(systemName: "paperplane")
                            .foregroundStyle(LinearGradient(colors: Theme.gradients, startPoint: .top, endPoint: .bottom))
                    }
                } else {
                    Button {} label: {
                        Image(systemName: "camera")
                            .foregroundStyle(LinearGradient(colors: Theme.gradients, startPoint: .top, endPoint: .bottom))
                    }
                    Button {} label: {
                        Image(systemName: "mic.circle")
                            .foregroundStyle(LinearGradient(colors: Theme.gradients, startPoint: .top, endPoint: .bottom))
                    }
                }
            }
            .padding()
        }
        .background(colorScheme == .dark ? Theme.darkBackground : Theme.lightBackground)
    }

    func messageView(message: Message) -> some View {
        HStack {
            if message.role == .user { Spacer() }
            Text(message.content)
                .padding()
                .background(message.role == .user ? Theme.secondaryColor : Theme.tertiaryColor)
                .foregroundColor(.white)
                .clipShape(RoundedRectangle(cornerRadius: 20))
            if message.role == .assistant || message.role == .system { Spacer() }
        }
    }
}

struct ChatView_Previews: PreviewProvider {
    static var previews: some View {
        ChatView(chatId: UUID())
            .environmentObject(ChatViewModel())
    }
}

核心说明

  1. ScrollViewProxy的作用:之前的代码未使用ScrollViewReader提供的proxy对象,这是触发滚动操作的核心入口。
  2. 底部锚点设计:通过透明的Color.clear设置id,确保滚动目标始终处于列表最底部,覆盖消息、加载提示等所有内容。
  3. 精准监听变化:通过onChange分别监听消息数组和加载状态的变化,确保所有需要滚动的场景都能触发操作。
  4. 平滑动画:添加withAnimation让滚动过程更自然,提升用户体验。

内容的提问来源于stack exchange,提问作者Diego Perdomo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 18:52:33