Predictive text / suggest next word in sentence on iOS?

On an iOS device, using the native keyboard (US english and "Predictive" turned on), if you write eg:

"Have a " (with a space at the end)

It will suggest three contextually relevant next-word-predictions on the bar at the top of the keyboard:

good great nice


I want to make use of this next-word prediction logic on iOS, but it turns out that I can only get it to work on OS X, where it's super simple.

So here is a tiny command line app that does exactly what I want (allthough only on OS X):


import AppKit

let str = "Have a "

let rangeForEndOfStr = NSMakeRange(str.utf16.count, 0)
let spellChecker = NSSpellChecker.sharedSpellChecker()
let completions = spellChecker.completionsForPartialWordRange(
    rangeForEndOfStr,
    inString: str,
    language: "en",
    inSpellDocumentWithTag: 0)
print(completions)


Running that program will print:

Optional(["good", "great", "nice", "lot", "problem", "new", "feeling", "chance", "few", "wonderful", "look", "big", "boyfriend", "better", "very", "job", "bad", "lovely", "crush", "blessed"])

Note that the first three words are exactly the same as those displayed by the native (predictive) iOS keyboard. So it works (at least on OS X)!

(It works by giving .completionsForPartialWordRange an zero-length range located at the end of the string (where the next word would go), rather than a range containing a partial word, allthough I guess you could say that a non-existing word is also a partial word.)


But trying the same approach on iOS (using UITextChecker instead of NSSpellChecker, etc.) does not work:


let str = "Have a "

let rangeForEndOfStr = NSMakeRange(str.utf16.count, 0)
let spellChecker = UITextChecker()
print(UITextChecker.availableLanguages())
let completions = spellChecker.completionsForPartialWordRange(
    rangeForEndOfStr,
    inString: str,
    language: "en_US") // ( <-- .availableLanguages() says "en_US" on iOS and "en" on OS X. )
print(completions)

(this code can be put in the viewDidLoad of the ViewController of an otherwise empty Single View iOS app.)


Run the iOS app and it will just print nil. : (


Turns out that UITextChecker's .completionsForPartialWordRange (contrary to NSSpellChecker's) simply returns nil if the range has zero length. I have tried all day to find any other way to get next-word-of-sentence-predictions/-suggestions/-completions on iOS but failed.


(NOTE: I have no problem getting UITextChecker to return completions of partially entered wods, ie where the range is not empty, but contains partially written word(prefixe)s, allthough the resulting completions are not sorted so that the more probable comes first, as the documentation says. They are actually just sorted alphabetically ... Anyway, what I want is something else: Given a partially written sentence containing only complete words, I want a list of probable-next-word-in-sentence-completions, exactly as examplified by my OS X example and the native iOS keyboard above.)


So, how do I write a working iOS-version of the OS X example above?

Post not yet marked as solved Up vote post of Jens Down vote post of Jens
5.1k views

Replies

Any ideas, anyone?

Would be great if anyone has an answer to this ...

So more than two years later and no answer ...


By extensive experimentation, I've found that it is possible to "trick" UITextChecker into working like NSSpellChecker:


let str = "Have a"
let augmentedStr = str + " *"
let tc = UITextChecker()
let range = NSRange(location: (str as NSString).length, length: -1)
let maybeCompletions = tc.completions(forPartialWordRange: range,
                                 in: augmentedStr,
                                 language: "en_US")
if let completions = maybeCompletions {
    if completions.isEmpty {
        print("Result was an empty Array.")
    } else {
        for c in completions { print(str + " " + c) }
    }
} else {
    print("Result was nil")
}


So it turns out that the only way to get UITextChecker to return next-word-of-a-sentence completions (as described above in my initial post, more than two years ago), is if you give it a range with location at the end of the sentence-string, and a length -1 ...


And what's more, it seems like the sentence have to contain a non-letter character, so I'm adding a space and an asterisk there.


Conclusion: The reason why UITextChecker differ in behavior from NSSpellChecker (as described above) seems to be because of a bug.


(As it is impossible to get UITextChecker to return the next word of a sentence, which is possible with NSSpellChecker, as described in my original post of this thread, UNLESS you use this ugly trick).


And also: This trick should have stopped working already, according to the warning the above code will result in:


2018-06-08 18:32:56.308401+0200 Xc10b1iOS_00[14606:952578] *** -[__NSCFString substringWithRange:]: Range {6, 18446744073709551615} out of bounds; string length 8. This will become an exception for apps linked after 10.10 and iOS 8. Warning shown once per app execution.


Please answer this question, I've waited for more than two years, and I've tried to send emails to people etc.

Post not yet marked as solved Up vote reply of Jens Down vote reply of Jens

I post here the answer I posted in the new thread : h ttps://forums.developer.apple.com/thread/104045


You had an error when building the NSRange: you inverted the 2 parameters.


The following works (adapted to Swift 4)


        let str = "hipst" // "Have a " 
       
        let rangeForEndOfStr = NSMakeRange(0, str.utf16.count)     // You had inverted parameters ; could also use NSRange(0..<str.utf16.count) 
        let spellChecker = UITextChecker() 
        print(UITextChecker.availableLanguages) 
        let completions = spellChecker.completions(forPartialWordRange: rangeForEndOfStr, in: str, language: "en_US") 
        print(completions ?? "No completion found")

you get :

["hipster", "hipsters", "hipster\'s"]


However, does not find the completion with "have a"


This reading is interesting :

h ttp://nshipster.com/uitextchecker/


If you look at documentation, you'll see completions are a bit different between IOS and OSX.

In IOS, it is indicated that it only completes incompete words ("hipst" -> "hipster"…

in MacOS, it provides new words to complete an existing one (beginning of sentence).

I think that is exactly what you observe here.



for MacOS

func completions(forPartialWordRange range: NSRange, in string: String, language: String?, inSpellDocumentWithTag tag: Int) -> [String]?

Provides a list of complete words that the user might be trying to type based on a partial word in a given string.

For IOS

func completions(forPartialWordRange range: NSRange, in string: String, language: String) -> [String]?

Returns an array of strings that are possible completions for a partially entered word.

As I said in the other thread, you misread my post(s). The range is correct (for my purposes).


For anyone else reading this thread, please see my detailed restatement of the issue and continue the discussion here.

Hi Jens,


I tried out your solution, which is a great find, but I'm experiencing an issue and wondered if you've seen the same thing.


If I use the solution in a UITextField in a normal iOS application, I type "Have a ", and the completions are populated as expected: "great", "good", "nice".


However, if I use the soluton in a keyboard extension, where I use UITextDocumentProxy.documentContentBeforeInput (or even a combination of documentContentBeforeInput and documentContentAfterInput to get the full text), I don't immediately get any suggestions, and I have to make a certain type of special change, like moving the caret out of the way and back into position, or deleting a character using UITextDocumentProxy.deleteBackward() and then typing the space character back in again, before the suggestions appear. It also doesn't seem like I can just do deleteBackward() followed by insertText(" ") in one pass, to force the same thing to happen.

Were you able to figure it out on newer iOS versions? I tried to give it a go but it returns nil now.

@MaksGal This unfortunately doesn't work in iOS 16 and later. If anyone finds a solution, I'd love to hear about it.