在 UITextView 中滚动后获取可见文本的 NSRange

Get the NSRange for the visible text after scroll in UITextView

我正在尝试将滚动文本的位置保存在 UITextView 中,以便在再次加载 ViewController 时可以 return 到该位置。我有很长的字符串,所以我希望用户能够滚动到特定位置,然后 return 稍后滚动到该位置。

我正在使用 UITextView。 scrollRangeToVisible 函数自动滚动文本视图,但我不知道如何获取用户看到的文本的 NSRange。这是最好的方法吗?我尝试使用 setContentOffset 函数,但似乎没有任何效果。

感谢任何帮助。谢谢!

我还没有对此进行彻底的测试,但我相信以下应该有效。您需要的 API 记录在 UITextInput 协议中,UITextView 采用该协议。

您首先需要获取对应于视图内给定点的UITextPosition。然后将此值转换为 UTF-16 字符偏移量。例如,我在这里每次滚动视图时打印 textView 的可见文本范围(以 UTF-16 代码单元表示):

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let topLeft = CGPoint(x: textView.bounds.minX, y: textView.bounds.minY)
    let bottomRight = CGPoint(x: textView.bounds.maxX, y: textView.bounds.maxY)
    guard let topLeftTextPosition = textView.closestPosition(to: topLeft),
        let bottomRightTextPosition = textView.closestPosition(to: bottomRight)
        else {
            return
    }
    let charOffset = textView.offset(from: textView.beginningOfDocument, to: topLeftTextPosition)
    let length = textView.offset(from: topLeftTextPosition, to: bottomRightTextPosition)
    let visibleRange = NSRange(location: charOffset, length: length)
    print("Visible range: \(visibleRange)")
}

在我的测试中,UITextView 倾向于计算几乎不包含在可见范围内的行(例如,仅一个像素),因此报告的可见范围往往比所显示的大一或两行人类用户会说。您可能需要对传递给 closesPosition(to:) 的确切 CGPoint 进行试验,以获得您想要的结果。

这里有一个 UITextView 的小扩展,它使用 characterRange(at:) 函数来代替。它还将计算的 属性 添加到 return 当前可见的文本:

extension UITextView {

    private var firstVisibleCharacterPosition: UITextPosition? {
        // ⚠️ For some reason `characterRange(at:)` returns nil for points with a low y value.
        guard let scrolledPosition = characterRange(at: contentOffset)?.start else {
            return beginningOfDocument
        }
        return scrolledPosition
    }

    private var lastVisibleCharacterPosition: UITextPosition? {
        return characterRange(at: bounds.max)?.end
    }

    /// The range of the text that is currently displayed within the text view's bounds.
    var visibleTextRange: UITextRange? {

        guard
            let first = firstVisibleCharacterPosition,
            let last = lastVisibleCharacterPosition else {
                return nil
        }

        return textRange(from: first, to: last)
    }      

    /// The string that is currently displayed within the text view's bounds.
    var visibleText: String? {
        guard let visibleTextRange = visibleTextRange else {
            return nil
        }
        return text(in: visibleTextRange)
    }

}

我在上面的代码中使用了这些 shorthand 属性:

extension CGRect {

    var min: CGPoint {
        return .init(x: minX, y: minY)
    }

    var max: CGPoint {
        return .init(x: maxX, y: maxY)
    }

}