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

SwiftUI中如何让ScrollView始终稳定滚动到底部

解决ScrollView滚动到底部不稳定的问题

针对聊天应用中ScrollView无法稳定滚动到底部的问题,结合你的代码给出以下关键修复方案:

1. 移除LazyVStack的随机ID

你给LazyVStack添加的.id(UUID())会导致每次视图刷新时重建整个栈,破坏ScrollViewReader对内部视图的追踪逻辑,直接移除该ID:

// 修改前
LazyVStack(alignment: .leading) {
  // ...
}.id(UUID())

// 修改后
LazyVStack(alignment: .leading) {
  // ...
}

2. 调整消息更新的监听逻辑

throttle会限制触发频率,可能错过新消息添加后的滚动时机。改用onChange直接监听messageHistory变化,同时确保等待视图渲染完成后执行滚动:

// 替换原有的onReceive代码
.onChange(of: viewModel.messageHistory) { _ in
  DispatchQueue.main.async {
    withAnimation {
      proxy.scrollTo(bottomAnchorID, anchor: .bottom)
    }
  }
}

通过DispatchQueue.main.async保证视图更新完成后再滚动,指定anchor: .bottom强制滚动到底部对齐。

3. 优化ScrollView默认锚点(可选)

给ScrollView添加默认底部锚点,强化滚动到底部的行为:

ScrollView() {
  // ...
}
.prefersDefaultScrollAnchor(.bottom)
.scrollDismissesKeyboard(.immediately)

4. 修正键盘状态处理的滚动逻辑

在handleKeyboardState方法中统一添加锚点指定,确保键盘状态变化时滚动精准:

func handleKeyboardState(_ state: KeyboardState, for proxy: ScrollViewProxy) {
  // 保留原有键盘逻辑,最后添加滚动处理
  DispatchQueue.main.async {
    proxy.scrollTo(bottomAnchorID, anchor: .bottom)
  }
}

完整修正代码

struct ChatView: View {
  
  @ObservedObject
  var viewModel = ChatModel()
  
  enum FocusedData: Int, Hashable {
    case message
  }
  
  @FocusState
  private var focusedField: FocusedData?
  
  let generator = UIImpactFeedbackGenerator(style: .light)
  
  @Namespace var topAnchorID
  @Namespace var bottomAnchorID
  
  @ObservedObject
  var keyboardObserver = KeyboardObserver()
  
  @State
  private var userMessage = ""
  
  @State
  var isUsingKeyboard = false
  
  var body: some View {
    
    ScrollViewReader{ proxy in
      VStack(spacing: 0) {
        ScrollView() {
          Color.red.frame(height: 1).id(topAnchorID)
          LazyVStack(alignment: .leading) {
            ForEach(viewModel.messageHistory, id: \.identifier) { data in
              MessageView(data: data).id(data.identifier)
            }
          }
          Color.red.frame(height: 1).id(bottomAnchorID)
        }
        .prefersDefaultScrollAnchor(.bottom)
        .scrollDismissesKeyboard(.immediately)
        .onChange(of: userMessage) { _ in
          guard !userMessage.isEmpty else { return }
          DispatchQueue.main.async {
            proxy.scrollTo(bottomAnchorID, anchor: .bottom)
          }
        }
        .onChange(of: viewModel.messageHistory) { _ in
          DispatchQueue.main.async {
            withAnimation {
              proxy.scrollTo(bottomAnchorID, anchor: .bottom)
            }
          }
        }
        .onChange(of: keyboardObserver.keyboardState) { state in
          handleKeyboardState(state, for: proxy)
        }
        .onAppear() {
          focusedField = FocusedData(rawValue: 0)
        }
        TextInputView().frame(maxWidth: .infinity)
      }
      
    }
  }
  
  func handleKeyboardState(_ state: KeyboardState, for proxy: ScrollViewProxy) {
    // 保留原有键盘状态处理逻辑
    DispatchQueue.main.async {
      proxy.scrollTo(bottomAnchorID, anchor: .bottom)
    }
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 03:10:37