Получите NSRange для видимого текста после прокрутки в UITextView

Я пытаюсь сохранить местоположение прокручиваемого текста в UITextView, чтобы я мог вернуться в это место при повторной загрузке ViewController. У меня очень длинные строки, поэтому я хочу, чтобы пользователь мог прокрутить до определенного места, а затем вернуться к нему позже.

Я использую UITextView. scrollRangeToVisible для автоматической прокрутки текстового представления, но я не знаю, как получить NSRange текста, который видит пользователь. Это лучший способ сделать это? Я пытался использовать функцию setContentOffset, но это, похоже, ничего не дало.

Любая помощь приветствуется. Спасибо!


person L.EXE    schedule 29.06.2018    source источник
comment
Чтобы создать проблему, вы можете поделиться каким-либо кодом?   -  person Kamran    schedule 29.06.2018


Ответы (2)


Я не проверял это полностью, но я считаю, что следующее должно работать. Необходимые API задокументированы в протоколе UITextInput, который UITextView принимает.

Сначала вам нужно получить UITextPosition, соответствующий заданной точке внутри представления. Затем вы должны преобразовать это значение в смещение символов UTF-16. Например, здесь я печатаю видимый диапазон текста (в единицах кода UTF-16) textView каждый раз, когда прокручивается представление:

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 имел тенденцию подсчитывать строки, которые едва попадали в видимый диапазон (например, только на один пиксель), поэтому сообщаемый видимый диапазон, как правило, был на одну или две строки больше, чем сказал бы пользователь-человек. Возможно, вам придется поэкспериментировать с точным CGPoint, которое вы передаете в closesPosition(to:), чтобы получить желаемые результаты.

person Ole Begemann    schedule 29.06.2018
comment
Спасибо, это очень полезно. Я попытался реализовать это, сделав CGPoint из bounds.maxY вместо использования bounds.origin. Это работает довольно хорошо примерно для первой четверти текста, но после этого он просто снова начинает идти к началу, что странно. - person L.EXE; 30.06.2018
comment
Хм, у меня вроде нормально работает. Я отредактировал свой ответ, включив в него более полный пример кода. Я тестировал это только на игровой площадке с простым текстом (без форматирования) длиной ок. 3000 символов, но я подтвердил, что приведенные значения точны. - person Ole Begemann; 30.06.2018

Вот небольшое расширение UITextView, которое вместо этого использует функцию characterRange(at:). Он также добавляет вычисляемое свойство для возврата видимого в данный момент текста:

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)
    }

}

Я использовал эти сокращенные свойства в приведенном выше коде:

extension CGRect {

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

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

}
person Mischa    schedule 27.12.2018