如何设置SwiftUI ScrollView的content offset解决键盘遮挡内容问题
解决方案
直接操作UIScrollView的contentOffset是最贴合你需求的实现方式,完整实现步骤如下:
步骤1:新增ScrollView访问器,获取底层UIScrollView实例
通过该扩展你可以直接拿到SwiftUI ScrollView对应的UIKit实例,后续直接修改其contentOffset即可:
struct ScrollViewAccessor: UIViewRepresentable { @Binding var scrollView: UIScrollView? func makeUIView(context: Context) -> UIView { let view = UIView() // 异步查找父层级的UIScrollView DispatchQueue.main.async { var parent = view.superview while parent != nil { if let scrollView = parent as? UIScrollView { self.scrollView = scrollView break } parent = parent?.superview } } return view } func updateUIView(_ uiView: UIView, context: Context) {} }
步骤2:改造自定义UITextField,上报激活状态与自身坐标
你需要在输入框成为第一响应者时,将自身的frame转换为相对于ScrollView的坐标并上报:
struct CustomTextField: UIViewRepresentable { @Binding var text: String /// 输入框激活回调,参数为输入框的全局frame var onBeginEditing: (CGRect) -> Void var onEndEditing: () -> Void func makeUIView(context: Context) -> UITextField { let textField = UITextField() textField.delegate = context.coordinator return textField } func updateUIView(_ uiView: UITextField, context: Context) { uiView.text = text } func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, UITextFieldDelegate { let parent: CustomTextField init(_ parent: CustomTextField) { self.parent = parent } func textFieldDidBeginEditing(_ textField: UITextField) { let globalFrame = textField.convert(textField.bounds, to: nil) parent.onBeginEditing(globalFrame) } func textFieldDidEndEditing(_ textField: UITextField) { parent.onEndEditing() } } }
步骤3:页面逻辑实现,计算偏移量并手动滚动
struct YourPageView: View { /// 你已经实现的键盘高度监听变量 @State private var keyboardHeight: CGFloat = 0 /// 存储当前激活输入框的全局frame @State private var activeTextFieldFrame: CGRect? /// 存储ScrollView的底层实例 @State private var scrollView: UIScrollView? /// 输入框和键盘之间的预留间距,可调整 private let reservedSpacing: CGFloat = 12 var body: some View { ScrollView { VStack(spacing: 16) { // 你的多个自定义输入框 ForEach(0..<15, id: \.self) { index in CustomTextField(text: .constant("")) { frame in activeTextFieldFrame = frame adjustScrollOffset() } onEndEditing: { activeTextFieldFrame = nil } .frame(height: 44) .border(.gray) .padding(.horizontal, 16) } } .padding(.bottom, keyboardHeight + reservedSpacing) // 保留你已有的底部内边距设置 .background(ScrollViewAccessor(scrollView: $scrollView)) // 插入访问器获取ScrollView实例 } // 你已有的键盘通知监听 .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notify in guard let frame = notify.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } keyboardHeight = frame.height adjustScrollOffset() } .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in keyboardHeight = 0 } } /// 核心逻辑:计算并调整ScrollView的contentOffset private func adjustScrollOffset() { guard let scrollView = scrollView, let activeFrame = activeTextFieldFrame, keyboardHeight > 0 else { return } // 计算键盘顶部在全局坐标系的Y值,若有导航栏/TabBar需要对应调整 let screenHeight = UIScreen.main.bounds.height let keyboardTopY = screenHeight - keyboardHeight // 输入框底部的全局Y值 let textFieldBottomY = activeFrame.maxY // 仅当输入框底部被键盘遮挡时才滚动 guard textFieldBottomY > keyboardTopY else { return } // 计算需要滚动的距离 let needScrollOffset = textFieldBottomY - keyboardTopY + reservedSpacing // 直接修改contentOffset实现滚动,可搭配键盘动画参数做平滑过渡 scrollView.contentOffset.y += needScrollOffset } }
优化提示
- 你可以从键盘通知的userInfo里拿到
UIResponder.keyboardAnimationDurationUserInfoKey和UIResponder.keyboardAnimationCurveUserInfoKey,给contentOffset的修改加上匹配的动画,体验和系统原生一致 - 如果页面存在导航栏、底部TabBar,需要在计算
keyboardTopY时减去对应组件的高度,避免偏移量计算错误 - 坐标转换建议直接转换为相对于ScrollView的坐标,适配嵌套视图的场景
内容的提问来源于stack exchange,提问作者Tometoyou
相关产品推荐
相关产品推荐

