SwiftUI Sheet视图键盘避让问题求助
解决Sheet聊天视图中键盘弹出仅TextField上移的问题
问题场景
开发的聊天视图通过设置presentationDetent为0.8的.sheet组件展示,点击TextField输入评论时整个视图被向上推起,期望实现类似Instagram评论区的效果:仅TextField随键盘上移,评论展示区域位置保持不变。已尝试GeometryReader+ZStack结合.ignoresSafeArea(.keyboard, edges: .bottom)、拆分组件等方法,问题未解决。
核心解决方案
默认情况下Sheet会自动随键盘调整位置,要实现仅TextField跟随键盘,需手动监听键盘高度变化,动态调整TextField的底部偏移,同时让评论区ScrollView占据固定可用空间,不随键盘移动。具体步骤:
- 通过
NotificationCenter监听键盘弹出/收起事件,获取键盘高度 - 使用
@State变量存储键盘高度,实时更新 - 给TextField所在视图添加
.offset修饰,根据键盘高度调整底部位置 - 调整评论区ScrollView布局,使其占据Sheet内除顶部导航栏和TextField外的所有空间
修改后的可复现代码
主视图ButtonView(无需修改)
struct ButtonView: View { @State private var showChat: Bool = false var body: some View { VStack { Button { showChat.toggle() } label: { Text("Tap Me!") } } .sheet(isPresented: $showChat, content: { ContentView(viewModel: ContentViewViewModel()) .presentationDetents([.fraction(0.8)]) .presentationDragIndicator(.hidden) .overlay( VStack { RoundedRectangle(cornerRadius: 2) .fill(Color.gray) .frame(width: 40, height: 5) .padding(.top, 15) .opacity(0.8) Label("Chat", systemImage: "message.badge") .lineLimit(nil) .padding(.top, 5) .padding([.leading, .trailing], 16) Divider() .padding(.top, 5) .padding([.leading, .trailing], 16) Spacer() } .frame(maxWidth: .infinity, alignment: .top) ) }) } }
数据模型与ViewModel(无需修改)
struct UserMockComments: Identifiable { let userName: String var comment: String var id = UUID().uuidString } class ContentViewViewModel: ObservableObject { @Published var mock = [UserMockComments]() @Published var text: String = "" init() { fetchData() } func fetchData() { let mockComments: [UserMockComments] = [ .init(userName: "OHeg12", comment: "Hello World!"), .init(userName: "OHeg12", comment: "Hello World!"), .init(userName: "OHeg12", comment: "Hello World!"), .init(userName: "OHeg12", comment: "Hello World!"), .init(userName: "OHeg12", comment: "Hello World!"), .init(userName: "OHeg12", comment: "Hello World!"), .init(userName: "OHeg12", comment: "Hello World!"), .init(userName: "OHeg12", comment: "Hello World!"), .init(userName: "OHeg12", comment: "Hello World!"), .init(userName: "OHeg12", comment: "Hello World!"), .init(userName: "OHeg12", comment: "Hello World!"), .init(userName: "OHeg12", comment: "Hello World!") ] self.mock = mockComments } }
修改后的ContentView
struct ContentView: View { @StateObject var viewModel = ContentViewViewModel() @FocusState var focus: Bool // 存储键盘高度 @State private var keyboardHeight: CGFloat = 0 var body: some View { VStack(spacing: 0) { // 评论展示区域,占满剩余空间 comments .frame(maxHeight: .infinity) // 输入框区域,随键盘偏移 commentTextField .padding(.horizontal, 10) .offset(y: -keyboardHeight) .animation(.easeOut(duration: 0.2), value: keyboardHeight) } .ignoresSafeArea(.keyboard, edges: .bottom) .onAppear { // 监听键盘弹出事件 NotificationCenter.default.addObserver( forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main ) { notification in guard let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } keyboardHeight = frame.height } // 监听键盘收起事件 NotificationCenter.default.addObserver( forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main ) { _ in keyboardHeight = 0 } } .onDisappear { // 移除监听 NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) } } var comments: some View { VStack(spacing: 0) { Spacer().frame(height: 105) ScrollViewReader { scroll in ScrollView(showsIndicators: true) { ForEach(viewModel.mock) { mockComments in VStack(alignment: .leading) { Text(mockComments.userName) Text(mockComments.comment) .padding(.horizontal, 10) } .id(mockComments.id) } } .scrollDismissesKeyboard(.immediately) } } } @ViewBuilder var commentTextField: some View { VStack { HStack { TextField("Type your comment", text: $viewModel.text, axis: .vertical) .keyboardType(.twitter) .padding([.leading, .vertical], 6) .focused($focus) } .padding([.bottom, .horizontal], 10) .border(.lightGray, width: 1, cornerRadius: 10) .padding(.bottom, 10) } } } #Preview { @StateObject var viewModel = ContentViewViewModel() return ContentView(viewModel: viewModel) }
关键修改点说明
- 布局结构调整:将原ZStack+GeometryReader改为VStack,让评论区ScrollView用
frame(maxHeight: .infinity)占据Sheet内的剩余空间,确保键盘弹出时不会被压缩。 - 键盘监听:通过
NotificationCenter获取键盘高度,实时更新keyboardHeight变量。 - TextField偏移:给输入框区域添加
.offset(y: -keyboardHeight),配合动画让输入框平滑跟随键盘上移。 - 移除多余嵌套:简化原布局中的嵌套结构,避免不必要的视图层级影响布局逻辑。
内容的提问来源于stack exchange,提问作者Omar Hegazy
相关产品推荐
相关产品推荐

