I found a solution. It's a hack, but I'll take what I can get.
I found that when a user taps a PDF text field, PDFKit hides the PDF text field and overlays a UITextView at the same location. It makes that UITextView the first responder and brings up the keyboard. That UITextView remains until the user taps elsewhere, when it is removed and replaced with a PDF text field containing the contents of the (now dead) UITextView.
The UITextView in question is buried deep inside PDFView, within private UIView subclasses.
Below is the code I'm using. It starts with a view (the PDFView) and deep-dives looking for any UITextView it can find. When found, it resigns as first responder, changes parameters, and becomes the first responder again. The user will see the typeahead buttons appear briefly then disappear. I haven't found a way around this, as we don't gain access to the UITextView until it is already the first responder.
The code here is called via a timer executing every 0.1 seconds. I'm sure there are more efficient ways to do this but this works, and barely registers on the CPU meter.
This code also sets the pasteDelegate of the UITextView because in my case I want to override and prevent pasting of text into the UITextView. The code to do that is simple; in textPasteConfigurationSupporting
just return [item setNoResult]
.
As with all hacks like this, be sure to test with all versions of iOS your app supports - including future versions. Apple could easily change their PDFKit implementation causing this to break or misbehave. Or better, they could add a supported means to do this.
-(void)lookForTextViewsInView:(UIView *)view
for (UIView *subview in view.subviews) {
if ([subview isKindOfClass:[UITextView class]]) {
UITextView *textView = (UITextView *)subview;
//NSLog(@"Found text field with contents: %@",textView.text);
if (textView.autocapitalizationType == UITextAutocapitalizationTypeNone &&
textView.autocorrectionType == UITextAutocorrectionTypeNo &&
textView.spellCheckingType == UITextSpellCheckingTypeNo &&
textView.pasteDelegate == self) {
//NSLog(@"textView %@ is already adjusted", textView.text);
return;
}
if (textView.isFirstResponder) {
//NSLog(@"Adjusting and resetting first responder of %@",textView.text);
[textView resignFirstResponder];
textView.autocapitalizationType = UITextAutocapitalizationTypeNone;
textView.autocorrectionType = UITextAutocorrectionTypeNo;
textView.spellCheckingType = UITextSpellCheckingTypeNo;
textView.pasteDelegate = self;
[textView becomeFirstResponder];
} else {
//I don't think this ever fires, but here for completion's sake
//NSLog(@"Adjusting without resetting first responder of %@",textView.text);
textView.autocapitalizationType = UITextAutocapitalizationTypeNone;
textView.autocorrectionType = UITextAutocorrectionTypeNo;
textView.spellCheckingType = UITextSpellCheckingTypeNo;
textView.pasteDelegate = self;
}
} else {
//NSLog(@"%@ is not a UITextView", [subview class]);
[self lookForTextViewsInView:subview];
}
}
}