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

聊天应用场景下SwiftUI List如何在键盘弹出且列表在底部时自动上移

解决方案

实现思路

  • 监听系统键盘的弹出/收起通知,获取键盘高度和对应动画参数,保证过渡效果和系统键盘完全同步
  • 动态修改List的底部内容内边距,让List可视区域随键盘位置同步调整
  • 可选搭配ScrollViewReader实现键盘弹出时自动滚动到列表最底部,符合聊天场景交互需求

完整实现代码

import SwiftUI
import Combine

struct ChatView: View {
    @State var text: String = ""
    @FocusState private var keyboardVisible: Bool
    @State private var keyboardHeight: CGFloat = 0
    @State private var cancellables = Set<AnyCancellable>()
    
    var body: some View {
        NavigationView {
            VStack(spacing: 0) {
                ScrollViewReader { proxy in
                    List {
                        ForEach(1...100, id: \.self) { item in
                            Text("\(item)")
                                .id(item)
                        }
                    }
                    .listStyle(.plain)
                    .contentInsets(.init(top: 0, leading: 0, bottom: keyboardHeight, trailing: 0))
                    // 不需要自动滚到底部可以删除下面的onChange逻辑
                    .onChange(of: keyboardHeight) { newValue in
                        guard newValue > 0 else { return }
                        withAnimation {
                            proxy.scrollTo(100, anchor: .bottom)
                        }
                    }
                    .navigationTitle("Conversations")
                    .toolbar {
                        ToolbarItem(placement: .navigationBarTrailing) {
                            Button {
                                keyboardVisible = false
                            } label: {
                                Text("Hide Keyboard")
                            }
                        }
                    }
                }
                ZStack {
                    Color.red
                        .frame(height: 44)
                    TextEditor(text: $text)
                        .frame(height: 32)
                        .focused($keyboardVisible)
                }
            }
            .onAppear {
                // 监听键盘弹出
                NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
                    .sink { notification in
                        guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
                              let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return }
                        // 减去底部安全区高度避免多余内边距
                        let safeAreaBottom = UIApplication.shared.connectedScenes
                            .filter { $0.activationState == .foregroundActive }
                            .compactMap { $0 as? UIWindowScene }
                            .first?.windows.first?.safeAreaInsets.bottom ?? 0
                        let height = keyboardFrame.height - safeAreaBottom
                        withAnimation(.easeInOut(duration: duration)) {
                            keyboardHeight = height
                        }
                    }
                    .store(in: &cancellables)
                
                // 监听键盘收起
                NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
                    .sink { notification in
                        guard let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return }
                        withAnimation(.easeInOut(duration: duration)) {
                            keyboardHeight = 0
                        }
                    }
                    .store(in: &cancellables)
            }
        }
    }
}

关键说明

  • 监听通知时同时获取键盘动画时长,保证List内边距调整的动画和键盘弹出动画完全同步,不会出现割裂感
  • 计算键盘高度时减去了底部安全区高度,避免出现多余的底部空白
  • iOS15及以上支持.contentInsets修饰符,如果要兼容更低版本,可以替换为.padding(.bottom, keyboardHeight)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.01 06:57:01