Skip to content

Instantly share code, notes, and snippets.

@wearhere
Last active May 2, 2025 13:56
Show Gist options
  • Save wearhere/f46ab9d837acaeaabfa86a813c44ad25 to your computer and use it in GitHub Desktop.
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.
//
// 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
}
}
@danielsaidi
Copy link

I'm looking into a problem where the next keyboard button stops working when you replace the original textDocumentProxy with a custom one: KeyboardKit/KeyboardKit#671

Have you noticed this and managed to solve it in any way?

@wearhere
Copy link
Author

I haven't used this in awhile now, sorry. (Stopped working on my custom keyboard when iOS added its own emoji search bar.)

@danielsaidi
Copy link

Oh I see, thank you anyway! I managed to hack around this limitation, so it's working now :)

@saqlainjamil5
Copy link

in keyboardkitPro
Sometimes, when the cursor is placed in the middle of a large paragraph, textDocumentProxy returns the text in parts — for example, the first half, then the full paragraph, and then only the second half.

In another scenario, if the cursor is in the middle of the paragraph, it returns the full text correctly, but the cursor gets stuck and doesn't move as expected.

I’ve already implemented all the keyboard functionalities similar to the native iOS keyboard, but I'm still facing issues with large text input. It only returns about 300 characters — the rest is skipped.

How can I resolve this issue?
@danielsaidi

@danielsaidi
Copy link

Hi @saqlainjamil5

Are you trying to use KeyboardKit Pro's fullDocumentContext feature?

@saqlainjamil5
Copy link

saqlainjamil5 commented May 2, 2025

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?

@danielsaidi
Copy link

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

@saqlainjamil5
Copy link

@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