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

UITextView设置contentInset后文本跳动,求保留自动滚动的解决方案

UITextView 底部contentInset导致文本跳动问题的解决方案

问题描述

为UITextView设置底部contentInset以避免文本被键盘遮挡,但当文本高度增加到一定程度时,UITextView出现文本上下跳动的异常。尝试用textContainerInset替代contentInset解决了跳动问题,但失去了输入时的自动滚动功能。

相关代码:

class ViewController: UIViewController, UITextViewDelegate {
    
    var textView: UITextView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        textView = MyTextView(usingTextLayoutManager: true)
        
        textView.alwaysBounceVertical = true
        textView.keyboardDismissMode = .interactive
        
        textView.contentInset.bottom = 300
        
        view.backgroundColor = .white
        textView.backgroundColor = .white
        
        view.addSubview(textView)
        
        textView.translatesAutoresizingMaskIntoConstraints = false
        [   textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            textView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
        ].forEach {
            $0.isActive = true
        }
        
        textView.layer.borderColor = UIColor.black.cgColor
        textView.layer.borderWidth = 2
        
        self.loadText()
    }
    
    func loadText() {
        let nsmutableAttributedString = NSMutableAttributedString(
            string: "Hello World!",
            attributes: [
                .font: UIFont.systemFont(ofSize: 24, weight: .semibold)
        ])
        
        textView.textStorage.setAttributedString(nsmutableAttributedString)
    }
}

尝试过的替代方案:

textView.textContainerInsets.bottom = 300

问题原因

  • 固定设置contentInset.bottom会和UITextView自身的内容滚动逻辑冲突:当文本高度超过可视区域时,系统自动滚动光标到可视区域的逻辑会与手动设置的固定内边距互相干扰,导致文本跳动。
  • textContainerInset是文本容器的内边距,不属于滚动视图的contentInset范畴,因此不会触发UITextView的自动滚动调整逻辑,导致失去输入时的自动滚动功能。

可行解决方案

方案一:结合键盘通知动态调整contentInset

既然设置底部内边距的初衷是避免键盘遮挡,应该在键盘弹出/收起时动态调整contentInset,而非固定设置一个值。这样既解决遮挡问题,又不会干扰UITextView的原生滚动逻辑。

修改代码如下:

class ViewController: UIViewController, UITextViewDelegate {
    
    var textView: UITextView!
    // 保存原始contentInset
    private var originalContentInset: UIEdgeInsets = .zero
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        textView = MyTextView(usingTextLayoutManager: true)
        
        textView.alwaysBounceVertical = true
        textView.keyboardDismissMode = .interactive
        
        view.backgroundColor = .white
        textView.backgroundColor = .white
        
        view.addSubview(textView)
        
        textView.translatesAutoresizingMaskIntoConstraints = false
        [   textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            textView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
        ].forEach {
            $0.isActive = true
        }
        
        textView.layer.borderColor = UIColor.black.cgColor
        textView.layer.borderWidth = 2
        
        self.loadText()
        
        // 保存原始contentInset
        originalContentInset = textView.contentInset
        // 注册键盘通知
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }
    
    func loadText() {
        let nsmutableAttributedString = NSMutableAttributedString(
            string: "Hello World!",
            attributes: [
                .font: UIFont.systemFont(ofSize: 24, weight: .semibold)
        ])
        
        textView.textStorage.setAttributedString(nsmutableAttributedString)
    }
    
    @objc private func keyboardWillShow(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
              let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
        // 转换键盘frame到textView的坐标系
        let keyboardHeight = view.convert(keyboardFrame, to: textView.superview).height
        // 调整底部contentInset为键盘高度
        textView.contentInset.bottom = originalContentInset.bottom + keyboardHeight
        // 同时调整scrollIndicatorInsets保持同步
        textView.scrollIndicatorInsets.bottom = textView.contentInset.bottom
    }
    
    @objc private func keyboardWillHide(_ notification: Notification) {
        // 恢复原始contentInset
        textView.contentInset = originalContentInset
        textView.scrollIndicatorInsets = originalContentInset
    }
    
    deinit {
        // 移除通知监听
        NotificationCenter.default.removeObserver(self)
    }
}

方案二:固定底部内边距+修正自动滚动逻辑

如果确实需要保留固定的底部内边距(比如底部有其他UI元素),可以通过自定义UITextView,重写scrollRangeToVisible(_:)方法,修正滚动位置,避免跳动:

class FixedInsetTextView: UITextView {
    // 自定义底部内边距
    var customBottomInset: CGFloat = 0
    
    override func scrollRangeToVisible(_ range: NSRange) {
        super.scrollRangeToVisible(range)
        // 手动调整滚动位置,抵消自定义内边距的影响
        let offset = CGPoint(x: contentOffset.x, y: contentOffset.y - customBottomInset)
        setContentOffset(offset, animated: false)
    }
}

然后在ViewController中使用这个自定义TextView:

textView = FixedInsetTextView(usingTextLayoutManager: true)
(textView as? FixedInsetTextView)?.customBottomInset = 300
// 同时设置contentInset
textView.contentInset.bottom = 300

这样既保留了固定的底部内边距,又能让UITextView正常自动滚动光标到可视区域,避免文本跳动。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 06:07:02