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

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)
}

关键修改点说明

  1. 布局结构调整:将原ZStack+GeometryReader改为VStack,让评论区ScrollView用frame(maxHeight: .infinity)占据Sheet内的剩余空间,确保键盘弹出时不会被压缩。
  2. 键盘监听:通过NotificationCenter获取键盘高度,实时更新keyboardHeight变量。
  3. TextField偏移:给输入框区域添加.offset(y: -keyboardHeight),配合动画让输入框平滑跟随键盘上移。
  4. 移除多余嵌套:简化原布局中的嵌套结构,避免不必要的视图层级影响布局逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 01:17:05