键盘扩展调整表情符号的文本位置问题

KeyboardExtension adjustTextPosition issues with emojis

我正在帮助构建一个键盘扩展,我最近 运行 遇到了 Swift 4 和表情符号的问题。 Swift 4 的新 UTF-16 表情符号支持非常好,但 UIInputViewController 中的 adjustTextPosition 存在问题。

如果我们调用 adjustTextPosition 跨过一个表情符号,它只会跨得不够远,似乎 UIInputViewController 使用的字符偏移量与系统使用的字符数不匹配。

要测试简单地写一个带有表情符号的文本,每当点击某个键时调用:

super.textDocumentProxy.adjustTextPosition(byCharacterOffset: 1)

可以观察到我们点击它的次数超过了预期。

Swift 5、看来下面的代码在iOS 12.

上运行良好
let count: Int = String(text).utf16.count
textDocumentProxy.adjustTextPosition(byCharacterOffset: count)

调整字素簇(Swift 个字符)中测量的插入符位置:

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:一个混乱的 hack,试图修复 Safari 的不一致。由于无法区分 safari 和非 safari,因此根据结果采取行动似乎是唯一的解决方案。

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

试试这个

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