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

如何设置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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.05 18:54:02