Отсутствие делегатных обратных вызовов для автозаменяемого текста в TextKit 2
Я создал пользовательский текстовый редактор с использованием TextKit 2, и столкнулся с проблемой, когда определенные изменения текста, инициированные клавиатурой iOS, не вызывают обычные методы делегата или жизненный цикл редактирования текста.
Описание проблемы
Замена по предложению слов
Когда я печатаю букву, например “t”, и выбираю предложенное слово (“the”) из предложений iOS-клавиатуры, замена происходит мгновенно без какого-либо обратного вызова делегата, фиксирующего это изменение. Мне нужно обнаруживать эти замены для применения пользовательских стилей к заменяемым словам.
Сокращение двойного пробела в точку
Когда пользователь печатает два пробела (которые клавиатура преобразует в точку), это также происходит молча без вызова:
insertText(_:)textView(_:shouldChangeTextIn:replacementText:)textViewDidChange(_:)
Текущие методы делегата не работают
Стандартные методы делегата не предоставляют информацию об этих автоматических изменениях:
func insertText(_ text: String)
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool
func textViewDidChange(_ textView: UITextView)
Ограниченное обходное решение
Единственное место, где можно наблюдать изменения — это обратные вызовы хранилища TextKit:
func textStorage(_ textStorage: NSTextStorage, willProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int)
func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int)
Однако эти обратные вызовы не уточняют происхождение или причину изменения.
Вопрос
Кто-нибудь нашел надежный способ обнаруживать или отслеживать замены текста, инициированные клавиатурой (такие как предиктивный текст или автопунктуация), в TextKit 2?
Обратные вызовы хранения текста в TextKit 2
Обратные вызовы хранения текста в TextKit 2 являются основным механизмом для обнаружения замен текста, инициированных клавиатурой, но они не предоставляют контекста об источнике изменений. Система клавиатуры iOS operates на уровне, который обходит традиционные методы делегирования для функций предиктивного ввода и авто-пунктуации, что затрудняет программное обнаружение этих конкретных замен.
Содержание
- Понимание проблемы замен текста
- Почему стандартные методы делегирования не работают
- Доступные методы обнаружения
- Обходные пути и решения
- Альтернативные подходы
- Лучшие практики и рекомендации
Понимание проблемы замен текста
Система клавиатуры iOS operates независимо от делегатов редактирования текста на уровне приложения, создавая фундаментальное разрыв между системными заменами текста и механизмами уведомления на уровне приложения. Когда пользователи взаимодействуют с предложениями предиктивного ввода или функциями авто-пунктуации, клавиатура вносит прямые изменения в хранилище текста, не следуя стандартной цепочке обратных вызовов делегирования.
Это разделение является намеренным по设计у - текстовая система Apple отдает приоритет производительности и пользовательскому опыту, а не детальному уведомлению о каждом изменении текста. Клавиатуре необходимо выполнять мгновенные замены для поддержания плавности набора текста, что было бы нарушено, если бы каждая замена должна была ждать вызовов методов делегирования и потенциальных переопределений со стороны пользователя.
Ключевое понимание: Система предиктивного ввода клавиатуры работает путем перехвата текстового ввода до того, как он достигает методов делегирования вашего приложения, делая эти изменения невидимыми для стандартного жизненного цикла редактирования текста.
Почему стандартные методы делегирования не работают
Стандартные методы делегирования, которые вы реализовали, предназначены для захвата изменений текста, инициированных пользователем, но они не учитывают системные изменения, которые происходят на уровне клавиатуры:
func insertText(_ text: String) // Захватывает только прямой ввод пользователя
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool // Обходится для авто-замен
func textViewDidChange(_ textView: UITextView) // Не вызывается для предиктивного текста
Согласно документации Apple и обсуждениям разработчиков, эти методы намеренно обходятся для определенных системных изменений текста для обеспечения плавного пользовательского опыта. Форумы разработчиков Apple подтверждают, что система выполняет оптимизации, которые пропускают обычную цепочку делегирования по причинам производительности.
Доступные методы обнаружения
Обратные вызовы хранилища текста
Как вы обнаружили, наиболее надежным методом для обнаружения этих изменений являются обратные вызовы делегата NSTextStorage:
func textStorage(_ textStorage: NSTextStorage, willProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int)
func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int)
Эти обратные вызовы будут срабатывать для всех изменений текста, включая замены, инициированные клавиатурой. Однако, как вы отметили, они не предоставляют контекста об источнике изменения.
Пользовательская обработка текстового ввода
Вы можете реализовать пользовательскую обработку текстового ввода для захвата изменений на разных этапах:
class CustomTextView: UITextView {
override func insertText(_ text: String) {
// Логирование прямого ввода пользователя
super.insertText(text)
}
override func deleteBackward() {
// Логирование прямого удаления
super.deleteBackward()
}
}
Обходные пути и решения
1. Контекстный анализ с обратными вызовами хранилища текста
Хотя обратные вызовы хранилища текста не предоставляют информацию об источнике, вы можете реализовать контекстный анализ для определения типа изменения:
func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) {
let currentText = textStorage.string
let changedText = (currentText as NSString).substring(with: editedRange)
// Обнаружение двойного пробела вместо точки
if delta == -1 && editedRange.length == 1 {
let beforeRange = NSRange(location: editedRange.location - 1, length: 1)
if beforeRange.location >= 0 {
let beforeChar = (currentText as NSString).character(at: beforeRange.location)
if beforeChar == 32 { // пробел
// Это может быть замена двойного пробела на точку
handlePossibleAutoPunctuation(editedRange)
}
}
}
// Обнаружение предиктивного текста (внезапная вставка большего количества текста)
if delta > 1 {
handlePredictiveTextReplacement(editedRange, insertedText: changedText)
}
}
2. Техники наблюдения за клавиатурой
Вы можете наблюдать за событиями клавиатуры для корреляции с изменениями текста:
class KeyboardObserver {
private var keyboardWillShowObserver: Any?
private var keyboardWillHideObserver: Any?
func setupObservation(in view: UIView) {
keyboardWillShowObserver = NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillShowNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.keyboardWillShow()
}
keyboardWillHideObserver = NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillHideNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.keyboardWillHide()
}
}
private func keyboardWillShow() {
// Сброс отслеживания изменений при появлении клавиатуры
resetChangeTracking()
}
private func keyboardWillHide() {
// Анализ недавних изменений при скрытии клавиатуры
analyzeRecentChanges()
}
}
3. Обеспечение контекста для предиктивного текста
Как обсуждалось на Stack Overflow, вы можете попытаться повлиять на систему предиктивного ввода, предоставляя контекст:
class PredictiveTextManager {
func setPredictiveContext(for textView: UITextView) {
// Получение текущего текста в качестве контекста для предиктивных предложений
let currentText = textView.text ?? ""
// Это упрощенный пример - реальная реализация была бы более сложной
if let textInput = textView as? UITextInput {
// Настройка контекста текстового ввода
// Примечание: Это может не помочь напрямую с обнаружением, но может повлиять на поведение
}
}
}
Альтернативные подходы
1. Расширение пользовательской клавиатуры
Для полного контроля над текстовым вводом рассмотрите возможность создания расширения пользовательской клавиатуры. Это дает вам прямой доступ к конвейеру текстового ввода и системе предиктивного ввода:
// Расширение пользовательской клавиатуры может захватывать все изменения текста
class CustomKeyboardViewController: UIInputViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupKeyboardInterface()
}
private func setupKeyboardInterface() {
// Создание интерфейса пользовательской клавиатуры
// Прямая обработка всего текстового ввода
}
}
2. Мониторинг замен текста
Реализуйте систему мониторинга, отслеживающую изменения текста со временем:
class TextChangeMonitor {
private var previousText: String = ""
private var changeHistory: [TextChange] = []
func monitorTextView(_ textView: UITextView) {
textView.delegate = self
// Сохранение начального текста
previousText = textView.text ?? ""
}
private func detectChangeType(_ newText: String, range: NSRange) -> ChangeType {
let changedText = (newText as NSString).substring(with: range)
// Логика определения типа изменения
if changedText.contains(".") && range.length == 1 {
return .autoPunctuation
}
if changedText.count > 1 && range.length == 1 {
return .predictiveText
}
return .unknown
}
}
enum ChangeType {
case userInput
case predictiveText
case autoPunctuation
case unknown
}
3. Анализ на основе времени
Поскольку замены клавиатуры происходят быстро, вы можете использовать временные метки для определения их источника:
class TimingBasedDetector {
private var lastUserInputTime: Date = .distantPast
private var textStorageObservationToken: Any?
func setup(in textStorage: NSTextStorage) {
textStorageObservationToken = textStorage.observe(\.string) { [weak self] _, _ in
self?.handleTextChange()
}
}
private func handleTextChange() {
let currentTime = Date()
let timeSinceLastInput = currentTime.timeIntervalSince(lastUserInputTime)
if timeSinceLastInput < 0.1 { // Очень недавнее изменение
// Вероятно, системная замена
handleSystemReplacement()
} else {
// Вероятно, ввод пользователя
lastUserInputTime = currentTime
}
}
}
Лучшие практики и рекомендации
1. Комбинирование нескольких методов обнаружения
Ни один отдельный метод не обеспечивает идеального обнаружения. Комбинируйте подходы для большей точности:
class ComprehensiveTextDetector {
private let textStorageMonitor = TextStorageMonitor()
private let timingDetector = TimingBasedDetector()
private let keyboardObserver = KeyboardObserver()
func setup(in textView: UITextView) {
textStorageMonitor.monitor(textView.textStorage)
timingDetector.setup(in: textView.textStorage)
keyboardObserver.setupObservation(in: textView)
}
}
2. Реализация классификации изменений
Создайте систему классификации для категоризации обнаруженных изменений:
enum TextChangeOrigin {
case userInput
case predictiveSuggestion
case autoPunctuation
case systemAutoCorrection
case unknown
}
class ChangeClassifier {
func classifyChange(_ change: TextChange) -> TextChangeOrigin {
// Реализация логики классификации на основе:
// - Содержимого текста
// - Временных меток
// - Контекста
// - Шаблонов
return .unknown
}
}
3. Обработка граничных случаев
Будьте готовы к граничным случаям и ложным срабатываниям:
class RobustTextHandler {
private var recentChanges: [TextChange] = []
private let changeHistoryLimit = 10
func handleTextChange(_ change: TextChange) {
// Добавление к недавним изменениям
recentChanges.append(change)
if recentChanges.count > changeHistoryLimit {
recentChanges.removeFirst()
}
// Проверка шаблонов
if isLikelyPredictiveText(change) {
handlePredictiveText(change)
} else if isLikelyAutoPunctuation(change) {
handleAutoPunctuation(change)
}
}
private func isLikelyPredictiveText(_ change: TextChange) -> Bool {
// Реализация распознавания шаблонов
return false
}
}
4. Использование возможностей TextKit 2
Воспользуйтесь улучшенными методами делегирования и возможностями TextKit 2:
class TextKit2TextView: UITextView {
override func layoutManager(_ layoutManager: NSLayoutManager,
textContainerContainerChangedGeometry textContainerContainer: NSTextContainer) {
// Обработка изменений макета, которые могут указывать на модификации текста
}
override func caretRect(for position: UITextPosition) -> CGRect {
// Мониторинг изменений позиции курсора
return super.caretRect(for: position)
}
}
Заключение
Обнаружение замен текста, инициированных клавиатурой, в TextKit 2 остается сложной задачей из-за системного характера этих изменений. Однако несколько подходов могут помочь вам эффективно захватывать и обрабатывать эти изменения:
- Обратные вызовы хранилища текста: Наиболее надежный метод, хотя и без контекста об источнике изменения
- Комбинированное обнаружение: Используйте несколько методов обнаружения (временные метки, шаблоны, наблюдение за клавиатурой) для большей точности
- Пользовательские решения: Реализуйте сложные системы отслеживания и классификации изменений
- Альтернативные подходы: Рассмотрите расширения пользовательской клавиатуры для полного контроля
Для наиболее комплексного решения реализуйте многоуровневую систему обнаружения, объединяющую мониторинг хранилища текста с анализом временных меток и распознаванием шаблонов. Этот подход поможет вам различать ввод пользователя и системно-инициированные замены, предоставляя необходимый контекст для применения пользовательских стилей.
Помните, что текстовая система Apple отдает приоритет пользовательскому опыту, а не видимости для разработчика каждого изменения текста, поэтому некоторые ограничения могут быть присущи дизайну платформы.
Источники
- Документация Apple - TextKit 2
- WWDC21: Знакомство с TextKit 2 - Видео от Apple Developer
- Stack Overflow - Контекст предиктивной клавиатуры iOS
- Фреймворк предиктивного ввода пользовательской клавиатуры - GitHub
- Обнаружение комбинаций клавиатуры - Stack Overflow
- Форумы разработчиков Apple - Обсуждения TextKit
- WWDC22: Что нового в TextKit и текстовых представлениях
- Пример приложения TextKit 2 - Christian Tietze