Warpzit
Warpzit

Reputation: 28162

KeyboardExtension adjustTextPosition issues with emojis

I'm helping build a keyboardextension and I've recently run into an issue with Swift 4 and emojis. The new UTF-16 emoji support for Swift 4 is really nice but there is an issue with adjustTextPosition in UIInputViewController.

If we call adjustTextPosition to step over an emoji it will simply not step far enough, it seems like the characteroffset used by UIInputViewController doesn't match the character count used by the system.

To test simply write a text with emojis and whenever some key is clicked call:

super.textDocumentProxy.adjustTextPosition(byCharacterOffset: 1)

What can be observed is that we have to click it more than what is to be expected.

Upvotes: 2

Views: 515

Answers (3)

Leonif
Leonif

Reputation: 482

try this

let correctedOffset = adjust(offset: offset)
textDocumentProxy.adjustTextPosition(byCharacterOffset: correctedOffset)

private func adjust(offset: Int) -> Int {
    if offset > 0, let after = textDocumentProxy.documentContextAfterInput {
        let offsetStringIndex = after.index(after.startIndex, offsetBy: offset)
        let chunk = after[..<offsetStringIndex]
        let characterCount = chunk.utf16.count
        return characterCount
        
    } else if  offset < 0, let before = textDocumentProxy.documentContextBeforeInput {
        let offsetStringIndex = before.index(before.endIndex, offsetBy: offset)
        let chunk = before[offsetStringIndex...]
        let characterCount = chunk.utf16.count
        return -1*characterCount
    } else {
        return offset
    }
}

Upvotes: 0

nikans
nikans

Reputation: 2555

Adjusting caret position measured in grapheme clusters (Swift Characters):

func adjustCaretPosition(offset: Int) {
    guard let textAfterCaret = textDocumentProxy.documentContextAfterInput else { return }
    
    if let offsetIndex = offset > 0 ?  textAfterCaret.index(textAfterCaret.startIndex, offsetBy: offset, limitedBy: textAfterCaret.endIndex) : textBeforeCaret.index(textBeforeCaret.endIndex, offsetBy: offset, limitedBy: textAfterCaret.startIndex),
        let offsetIndex_utf16 = offsetIndex.samePosition(in: offset > 0 ? textAfterCaret.utf16 : textBeforeCaret.utf16)
    {
        let offset = offset > 0 ? textAfterCaret.utf16.distance(from: textAfterCaret.utf16.startIndex, to: offsetIndex_utf16) : textBeforeCaret.utf16.distance(from: textBeforeCaret.utf16.endIndex, to: offsetIndex_utf16)
        textDocumentProxy.adjustTextPosition(byCharacterOffset: offset)
    }
    else {
        textDocumentProxy.adjustTextPosition(byCharacterOffset: offset)
    }
}

UPD: A messy hack, to try and fix Safari inconsistency. Since there's no way to discriminate between safari and not safari, to act based on result looks like the only solution.

func adjustCaretPosition(offset: Int) {
    // for convenience
    let textAfterCaret = textDocumentProxy.documentContextAfterInput ?? ""
    let textBeforeCaret = textDocumentProxy.documentContextBeforeInput ?? ""
    
    if let offsetIndex = offset > 0 ?  textAfterCaret.index(textAfterCaret.startIndex, offsetBy: offset, limitedBy: textAfterCaret.endIndex) : textBeforeCaret.index(textBeforeCaret.endIndex, offsetBy: offset, limitedBy: textAfterCaret.startIndex),
        let offsetIndex_utf16 = offsetIndex.samePosition(in: offset > 0 ? textAfterCaret.utf16 : textBeforeCaret.utf16)
    {
        // part of context before caret adjustment
        let previousText = offset > 0 ? textAfterCaret : textBeforeCaret
        
        // what we expect after adjustment
        let expectedText = offset > 0 ? String(textAfterCaret[offsetIndex..<textAfterCaret.endIndex]) : String(textBeforeCaret[textBeforeCaret.startIndex..<offsetIndex])
        
        // offset in UTF-16 characters
        let offset_utf16 = offset > 0 ? textAfterCaret.utf16.distance(from: textAfterCaret.utf16.startIndex, to: offsetIndex_utf16) : textBeforeCaret.utf16.distance(from: textBeforeCaret.utf16.endIndex, to: offsetIndex_utf16)
        
        // making adjustment
        textDocumentProxy.adjustTextPosition(byCharacterOffset: offset)
        
        // part of context after caret adjustment
        let compareText = offset > 0 ? textAfterCaret : textBeforeCaret
        
        // rollback if got unwanted results
        // then adjust by grapheme clusters offset
        if compareText != "", expectedText != compareText, compareText != previousText {
            textDocumentProxy.adjustTextPosition(byCharacterOffset: -offset_utf16)
            textDocumentProxy.adjustTextPosition(byCharacterOffset: offset)
        }
    }
    else {
        // we probably stumbled upon a textDocumentProxy inconsistency, i.e. context got divided by an emoji
        // adjust by grapheme clusters offset
        textDocumentProxy.adjustTextPosition(byCharacterOffset: offset)
    }
}

Upvotes: 0

Mitsuaki Ishimoto
Mitsuaki Ishimoto

Reputation: 3181

Swift 5, it seems that the following code works well on iOS 12.

let count: Int = String(text).utf16.count
textDocumentProxy.adjustTextPosition(byCharacterOffset: count)

Upvotes: 4

Related Questions