Last active
May 2, 2025 13:56
-
-
Save wearhere/f46ab9d837acaeaabfa86a813c44ad25 to your computer and use it in GitHub Desktop.
A generic implementation of the `UITextDocumentProxy` protocol that should work for anything that conforms to `UIResponder` and `UITextInput`. Useful to put text fields inside custom keyboards and then reuse your keyboard's regular handling logic with this text field. See https://github.com/danielsaidi/KeyboardKit/issues/45 for more info.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// documentProxy.swift | |
// KeyboardKitDemoKeyboard | |
// | |
// Created by Jeffrey Wear on 4/28/20. | |
// | |
import UIKit | |
class TextDocumentProxy<TextDocument: UIResponder & UITextInput>: NSObject, UITextDocumentProxy { | |
init(document: TextDocument) { | |
self.document = document | |
super.init() | |
} | |
private unowned let document: TextDocument | |
// MARK: - UITextDocumentProxy | |
var documentInputMode: UITextInputMode? { | |
document.textInputMode | |
} | |
var documentContextAfterInput: String? { | |
guard let selectedTextRange = document.selectedTextRange else { | |
return nil | |
} | |
guard let rangeAfterInput = document.textRange(from: selectedTextRange.end, to: document.endOfDocument) else { | |
return nil | |
} | |
return document.text(in: rangeAfterInput) | |
} | |
var documentContextBeforeInput: String? { | |
guard let selectedTextRange = document.selectedTextRange else { | |
return nil | |
} | |
guard let rangeBeforeInput = document.textRange(from: document.beginningOfDocument, to: selectedTextRange.start) else { | |
return nil | |
} | |
return document.text(in: rangeBeforeInput) | |
} | |
// https://stackoverflow.com/a/41023439/495611 suggests adjusting the text | |
// position (i.e. moving the cursor) by adjusting the selected text range. | |
func adjustTextPosition(byCharacterOffset offset: Int) { | |
guard let selectedTextRange = document.selectedTextRange else { return } | |
// Not sure what's supposed to happen if the range is non-empty. Let's | |
// abort if it is. | |
guard selectedTextRange.isEmpty else { return } | |
// Now that it's empty, the start and end should be the same. Move that position. | |
// The guard is a bounds check. | |
guard let newPosition = document.position(from: selectedTextRange.start, offset: offset) else { return } | |
document.selectedTextRange = document.textRange(from: newPosition, to: newPosition) | |
} | |
var selectedText: String? { | |
guard let selectedTextRange = document.selectedTextRange else { | |
return nil | |
} | |
return document.text(in: selectedTextRange) | |
} | |
let documentIdentifier: UUID = UUID() | |
func setMarkedText(_ markedText: String, selectedRange: NSRange) { | |
document.setMarkedText(markedText, selectedRange: selectedRange) | |
} | |
func unmarkText() { | |
document.unmarkText() | |
} | |
// MARK: - UIKeyInput | |
func insertText(_ text: String) { | |
document.insertText(text) | |
} | |
func deleteBackward() { | |
document.deleteBackward() | |
} | |
var hasText: Bool { | |
document.hasText | |
} | |
} | |
I see, yes the native text document proxy APIs have always been very lacking.
The full document context implementation is a Pro feature with a lot of time and effort put into it. You need a Silver license to use it. Without KeyboardKit Pro, you have to implement the logic yourself.
Best,
Daniel
@danielsaidi I respect your effort and time.
However, I am a student and don't have the budget to purchase a Silver license.
I only need this one feature to implement in my code, as it's currently blocking my app's progress.
If you could offer some support, I would be very grateful.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks @danielsaidi for Reply
No, I didn't use KeyboardKit Pro.
I just tested your demo code.
In my custom keyboard, I used textDocumentProxy.documentContextBeforeInput, but it can't read more than 300 characters — unlike your fullDocumentContext, which reads all the text.
How can I handle large text like that? without using KeyboardKit Pro?