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

SwiftUI中键盘弹出时如何让TextEditor自动向上滚动文本?

解决SwiftUI中TextEditor光标被键盘遮挡的自动滚动问题

你的核心需求是让TextEditor在光标接近键盘时自动向上滚动,确保输入区域可见。当前代码仅通过添加底部padding适配键盘高度,但未处理光标位置的动态滚动,因此会出现遮挡问题。以下是两种可行的解决方案:

方案一:自定义UITextView包装(无第三方依赖)

通过UIViewRepresentable原生包装UITextView,利用其Delegate监听光标位置,结合键盘状态动态调整滚动位置。

1. 更新键盘监听类

保存完整的键盘Frame(而非仅高度),方便后续位置计算:

class KeyboardResponder: ObservableObject {
    @Published var keyboardFrame: CGRect = .zero
    
    init() {
        let center = NotificationCenter.default
        center.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
        center.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
    }
    
    @objc private func keyboardWillShow(_ notification: Notification) {
        guard let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
        keyboardFrame = frame
    }
    
    @objc private func keyboardWillHide(_ notification: Notification) {
        keyboardFrame = .zero
    }
}

2. 自定义WrappedTextView

包装UITextView并实现光标位置检查逻辑:

struct WrappedTextView: UIViewRepresentable {
    @Binding var text: String
    @ObservedObject var keyboardResponder: KeyboardResponder
    
    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.delegate = context.coordinator
        textView.font = .systemFont(ofSize: 16)
        textView.backgroundColor = .clear
        return textView
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) {
        if uiView.text != text {
            uiView.text = text
        }
        // 键盘状态变化时检查光标位置
        if !keyboardResponder.keyboardFrame.isEmpty {
            context.coordinator.adjustScrollForCursor(in: uiView)
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }
    
    class Coordinator: NSObject, UITextViewDelegate {
        let parent: WrappedTextView
        
        init(parent: WrappedTextView) {
            self.parent = parent
        }
        
        func textViewDidChange(_ textView: UITextView) {
            parent.text = textView.text
            // 每次输入后检查光标位置
            adjustScrollForCursor(in: textView)
        }
        
        func adjustScrollForCursor(in textView: UITextView) {
            guard let selectedRange = textView.selectedTextRange else { return }
            let cursorRect = textView.caretRect(for: selectedRange.end)
            
            guard let window = textView.window else { return }
            let cursorWindowRect = textView.convert(cursorRect, to: window)
            // 计算键盘顶部的Y坐标
            let keyboardTopY = window.bounds.height - parent.keyboardResponder.keyboardFrame.height
            
            // 光标底部超过键盘顶部时,调整滚动偏移
            if cursorWindowRect.maxY > keyboardTopY {
                let offsetY = cursorWindowRect.maxY - keyboardTopY + 8 // 增加8pt间距
                textView.contentOffset.y += offsetY
            }
        }
    }
}

3. 使用自定义TextView

替换原有的TextEditor,去掉外层ScrollView(避免滚动冲突):

struct NewEntryView: View {
    @ObservedObject private var keyboardResponder = KeyboardResponder()
    @State private var text: String = ""
    
    var body: some View {
        WrappedTextView(text: $text, keyboardResponder: keyboardResponder)
            .padding()
            .padding(.bottom, keyboardResponder.keyboardFrame.height)
            .animation(.easeOut(duration: 0.3), value: keyboardResponder.keyboardFrame.height)
            .background(Color(UIColor.systemBackground))
            .edgesIgnoringSafeArea(.bottom)
    }
}

方案二:使用Introspect库简化实现(依赖第三方)

如果不想自定义TextView,可以用Introspect库直接获取TextEditor底层的UITextView,代码更简洁:

1. 添加Introspect依赖

通过Swift Package Manager添加:https://github.com/siteline/SwiftUI-Introspect

2. 实现滚动调整逻辑

import Introspect

struct NewEntryView: View {
    @ObservedObject private var keyboardResponder = KeyboardResponder()
    @State private var text: String = ""
    @State private var textView: UITextView?
    
    var body: some View {
        TextEditor(text: $text)
            .padding()
            .padding(.bottom, keyboardResponder.keyboardFrame.height)
            .animation(.easeOut(duration: 0.3), value: keyboardResponder.keyboardFrame.height)
            // 绑定底层UITextView
            .introspectTextView { tv in
                textView = tv
                tv.delegate = self
            }
            // 文本或键盘状态变化时调整滚动
            .onChange(of: text) { _ in adjustScrollForCursor() }
            .onChange(of: keyboardResponder.keyboardFrame) { _ in adjustScrollForCursor() }
    }
    
    private func adjustScrollForCursor() {
        guard let textView = textView, let selectedRange = textView.selectedTextRange else { return }
        let cursorRect = textView.caretRect(for: selectedRange.end)
        
        guard let window = textView.window else { return }
        let cursorWindowRect = textView.convert(cursorRect, to: window)
        let keyboardTopY = window.bounds.height - keyboardResponder.keyboardFrame.height
        
        if cursorWindowRect.maxY > keyboardTopY {
            let offsetY = cursorWindowRect.maxY - keyboardTopY + 8
            textView.contentOffset.y += offsetY
        }
    }
}

extension NewEntryView: UITextViewDelegate {
    func textViewDidChange(_ textView: UITextView) {
        adjustScrollForCursor()
    }
}

方案说明

两种方案核心逻辑一致:

  1. 监听键盘的显示/隐藏事件,获取键盘位置
  2. 监听文本输入和光标变化,计算光标在窗口坐标系中的位置
  3. 当光标底部超过键盘顶部时,调整TextEditor的滚动偏移,确保光标可见

方案一无第三方依赖,更稳定;方案二代码更简洁,但需要引入外部库,可根据项目需求选择。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 04:42:05