NeuroAgent

Detecting Auto-Replaced Text in TextKit 2

Learn reliable methods to detect keyboard-triggered text replacements in TextKit 2. Discover workarounds for predictive text and auto-punctuation that bypass standard delegate methods. Apply custom styles to auto-replaced text effectively.

Question

Missing Delegate Callbacks for Auto-Replaced Text in TextKit 2

I’ve built a custom text editor using TextKit 2, and I’m encountering an issue where certain text changes triggered by the iOS keyboard don’t trigger the usual delegate methods or text editing lifecycle.

Problem Description

Word Suggestions Replacement

When I type a letter like “t” and select a suggested word (“the”) from the iOS keyboard suggestions, the replacement happens instantly without any delegate callbacks capturing the change. I need to detect these replacements to apply custom styles to the replaced words.

Double-Space “.” Shortcut

When the user types two spaces (which the keyboard converts to a period), this also occurs silently without triggering:

  • insertText(_:)
  • textView(_:shouldChangeTextIn:replacementText:)
  • textViewDidChange(_:)

Current Delegate Methods Not Working

The standard delegate methods don’t provide information about these automatic changes:

swift
func insertText(_ text: String)
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool
func textViewDidChange(_ textView: UITextView)

Limited Workaround

The only place where changes can be observed is in the TextKit storage callbacks:

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

However, these callbacks don’t clarify the origin or cause of the change.

Question

Has anyone found a reliable way to detect or trace keyboard-triggered text replacements (such as predictive text or auto-punctuation) in TextKit 2?

NeuroAgent

TextKit 2’s text storage callbacks are the primary mechanism for detecting keyboard-triggered text replacements, but they lack context about the origin of changes. The iOS keyboard system operates at a level that bypasses traditional delegate methods for predictive text and auto-punctuation features, making it challenging to detect these specific replacements programmatically.


Contents


Understanding the Text Replacement Challenge

The iOS keyboard system operates independently of app-level text editing delegates, creating a fundamental disconnect between system-level text replacements and app-level notification mechanisms. When users interact with predictive text suggestions or auto-punctuation features, the keyboard makes direct modifications to the text storage without following the standard delegate callback chain.

This separation is intentional by design - Apple’s text system prioritizes performance and user experience over granular notification of every text modification. The keyboard needs to make instantaneous replacements to maintain typing flow, which would be disrupted if each replacement had to wait for delegate method calls and potential user overrides.

Key Insight: The keyboard’s predictive text system works by intercepting text input before it reaches your app’s delegate methods, making these modifications invisible to the standard text editing lifecycle.


Why Standard Delegate Methods Fail

The standard delegate methods you’ve implemented are designed to capture user-initiated text changes, but they don’t account for system-level modifications that occur at the keyboard level:

swift
func insertText(_ text: String) // Only captures direct user input
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool // Bypassed for auto-replacements
func textViewDidChange(_ textView: UITextView) // Not triggered for predictive text

According to Apple’s documentation and developer discussions, these methods are intentionally bypassed for certain system text modifications to ensure smooth typing experience. The Apple Developer Forums confirm that the system makes optimizations that skip the normal delegate chain for performance reasons.


Available Detection Methods

Text Storage Callbacks

As you’ve discovered, the most reliable method for detecting these changes is through the NSTextStorage delegate callbacks:

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

These callbacks will fire for all text modifications, including keyboard-triggered replacements. However, as you noted, they don’t provide context about the origin of the change.

Custom Text Input Processing

You can implement custom text input processing to capture changes at different stages:

swift
class CustomTextView: UITextView {
    override func insertText(_ text: String) {
        // Log direct user input
        super.insertText(text)
    }
    
    override func deleteBackward() {
        // Log direct deletion
        super.deleteBackward()
    }
}

Workarounds and Solutions

1. Contextual Analysis with Text Storage Callbacks

While the text storage callbacks don’t provide origin information, you can implement contextual analysis to infer the type of change:

swift
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)
    
    // Detect double-space to period replacement
    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 { // space character
                // This might be a double-space period replacement
                handlePossibleAutoPunctuation(editedRange)
            }
        }
    }
    
    // Detect predictive text (sudden larger text insertion)
    if delta > 1 {
        handlePredictiveTextReplacement(editedRange, insertedText: changedText)
    }
}

2. Keyboard Observation Techniques

You can observe keyboard events to correlate with text changes:

swift
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() {
        // Reset change tracking when keyboard appears
        resetChangeTracking()
    }
    
    private func keyboardWillHide() {
        // Analyze changes when keyboard disappears
        analyzeRecentChanges()
    }
}

3. Predictive Text Context Feeding

As discussed in Stack Overflow, you can attempt to influence the predictive text system by providing context:

swift
class PredictiveTextManager {
    func setPredictiveContext(for textView: UITextView) {
        // Get current text as context for predictive suggestions
        let currentText = textView.text ?? ""
        
        // This is a simplified example - actual implementation would be more complex
        if let textInput = textView as? UITextInput {
            // Set up text input context
            // Note: This may not directly help with detection but can influence behavior
        }
    }
}

Alternative Approaches

1. Custom Keyboard Extension

For complete control over text input, consider creating a custom keyboard extension. This gives you direct access to the text input pipeline and predictive text system:

swift
// Custom keyboard extension can capture all text changes
class CustomKeyboardViewController: UIInputViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        setupKeyboardInterface()
    }
    
    private func setupKeyboardInterface() {
        // Create custom keyboard interface
        // Handle all text input directly
    }
}

2. Text Replacement Monitoring

Implement a monitoring system that tracks text changes over time:

swift
class TextChangeMonitor {
    private var previousText: String = ""
    private var changeHistory: [TextChange] = []
    
    func monitorTextView(_ textView: UITextView) {
        textView.delegate = self
        
        // Store initial text
        previousText = textView.text ?? ""
    }
    
    private func detectChangeType(_ newText: String, range: NSRange) -> ChangeType {
        let changedText = (newText as NSString).substring(with: range)
        
        // Logic to determine change type
        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. Time-Based Analysis

Since keyboard replacements happen quickly, you can use timing to infer their origin:

swift
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 { // Very recent change
            // Likely a system replacement
            handleSystemReplacement()
        } else {
            // Likely user input
            lastUserInputTime = currentTime
        }
    }
}

Best Practices and Recommendations

1. Combine Multiple Detection Methods

No single method provides perfect detection. Combine approaches for better accuracy:

swift
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. Implement Change Classification

Create a classification system to categorize detected changes:

swift
enum TextChangeOrigin {
    case userInput
    case predictiveSuggestion
    case autoPunctuation
    case systemAutoCorrection
    case unknown
}

class ChangeClassifier {
    func classifyChange(_ change: TextChange) -> TextChangeOrigin {
        // Implement logic to classify based on:
        // - Text content
        // - Timing
        // - Context
        // - Patterns
        return .unknown
    }
}

3. Handle Edge Cases

Be prepared for edge cases and false positives:

swift
class RobustTextHandler {
    private var recentChanges: [TextChange] = []
    private let changeHistoryLimit = 10
    
    func handleTextChange(_ change: TextChange) {
        // Add to recent changes
        recentChanges.append(change)
        if recentChanges.count > changeHistoryLimit {
            recentChanges.removeFirst()
        }
        
        // Check for patterns
        if isLikelyPredictiveText(change) {
            handlePredictiveText(change)
        } else if isLikelyAutoPunctuation(change) {
            handleAutoPunctuation(change)
        }
    }
    
    private func isLikelyPredictiveText(_ change: TextChange) -> Bool {
        // Implement pattern recognition
        return false
    }
}

4. Leverage TextKit 2 Features

Take advantage of TextKit 2’s improved delegate methods and features:

swift
class TextKit2TextView: UITextView {
    override func layoutManager(_ layoutManager: NSLayoutManager, 
                               textContainerContainerChangedGeometry textContainerContainer: NSTextContainer) {
        // Handle layout changes that might indicate text modifications
    }
    
    override func caretRect(for position: UITextPosition) -> CGRect {
        // Monitor caret position changes
        return super.caretRect(for: position)
    }
}

Conclusion

Detecting keyboard-triggered text replacements in TextKit 2 remains challenging due to the system-level nature of these modifications. However, several approaches can help you capture and handle these changes effectively:

  1. Text Storage Callbacks: The most reliable method, though lacking context about change origin
  2. Combined Detection: Use multiple detection methods (timing, patterns, keyboard observation) for better accuracy
  3. Custom Solutions: Implement sophisticated change tracking and classification systems
  4. Alternative Approaches: Consider custom keyboard extensions for complete control

For the most comprehensive solution, implement a multi-layered detection system that combines text storage monitoring with timing analysis and pattern recognition. This approach will help you distinguish between user input and system-triggered replacements while providing the context needed to apply custom styles appropriately.

Remember that Apple’s text system prioritizes user experience over developer visibility into every text modification, so some limitations may be inherent to the platform design.


Sources

  1. Apple Developer - TextKit 2 Documentation
  2. WWDC21: Meet TextKit 2 - Apple Developer Video
  3. Stack Overflow - iOS Predictive Keyboard Context
  4. Custom Keyboard Prediction Framework - GitHub
  5. Detecting Keyboard Combinations - Stack Overflow
  6. Apple Developer Forums - TextKit Discussions
  7. WWDC22: What’s new in TextKit and text views
  8. TextKit 2 Example App - Christian Tietze