BLACK FRIDAY: Save 50% on all my Swift books and bundles! >>

Validating words with UITextChecker

Paul Hudson    @twostraws   

Now that our game is all set up, the last part of this project is to make sure the user can’t enter invalid words. We’re going to implement this as four small methods, each of which perform exactly one check: is the word original (it hasn’t been used already), is the word possible (they aren’t trying to spell “car” from “silkworm”), and is the word real (it’s an actual English word).

If you were paying attention you’ll have noticed that was only three methods – that’s because the fourth method will be there to make showing error messages easier.

Anyway, let’s start with the first method: this will accept a string as its only parameter, and return true or false depending on whether the word has been used before or not. We already have a usedWords array, so we can pass the word into its contains() method and send the result back like this:

func isOriginal(word: String) -> Bool {
    !usedWords.contains(word)
}

That’s one method down!

The next one is slightly trickier: how can we check whether a random word can be made out of the letters from another random word?

There are a couple of ways we could tackle this, but the easiest one is this: if we create a variable copy of the root word, we can then loop over each letter of the user’s input word to see if that letter exists in our copy. If it does, we remove it from the copy (so it can’t be used twice), then continue. If we make it to the end of the user’s word successfully then the word is good, otherwise there’s a mistake and we return false.

So, here’s our second method:

func isPossible(word: String) -> Bool {
    var tempWord = rootWord

    for letter in word {
        if let pos = tempWord.firstIndex(of: letter) {
            tempWord.remove(at: pos)
        } else {
            return false
        }
    }

    return true
}

The final method is harder, because we need to use UITextChecker from UIKit. In order to bridge Swift strings to Objective-C strings safely, we need to create an instance of NSRange using the UTF-16 count of our Swift string. This isn’t nice, I know, but I’m afraid it’s unavoidable until Apple cleans up these APIs.

So, our last method will make an instance of UITextChecker, which is responsible for scanning strings for misspelled words. We’ll then create an NSRange to scan the entire length of our string, then call rangeOfMisspelledWord() on our text checker so that it looks for wrong words. When that finishes we’ll get back another NSRange telling us where the misspelled word was found, but if the word was OK the location for that range will be the special value NSNotFound.

So, here’s our final method:

func isReal(word: String) -> Bool {
    let checker = UITextChecker()
    let range = NSRange(location: 0, length: word.utf16.count)
    let misspelledRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en")

    return misspelledRange.location == NSNotFound
}

Before we can use those three, I want to add some code to make showing error alerts easier. First, we need some properties to control our alerts:

@State private var errorTitle = ""
@State private var errorMessage = ""
@State private var showingError = false

Now we can add a method that sets the title and message based on the parameters it receives, then flips the showingError Boolean to true:

func wordError(title: String, message: String) {
    errorTitle = title
    errorMessage = message
    showingError = true
}

We can then pass those directly on to SwiftUI by adding an alert() modifier below .onAppear():

.alert(errorTitle, isPresented: $showingError) {
    Button("OK") { }
} message: {
    Text(errorMessage)
}

We’ve done that several times now, so hopefully it’s becoming second nature! In fact, now that you're familiar with basic alerts like that one, here's a pro tip for you: if you don't include any buttons in the code, you automatically get a simple "OK" button that dismisses the alert.

So, if you prefer you can write this:

.alert(errorTitle, isPresented: $showingError) { } message: {
    Text(errorMessage)
}

Yes, that looks a bit odd – choose whichever style you prefer.

At long last it’s time to finish our game: replace the // extra validation to come comment in addNewWord() with this:

guard isOriginal(word: answer) else {
    wordError(title: "Word used already", message: "Be more original")
    return
}

guard isPossible(word: answer) else {
    wordError(title: "Word not possible", message: "You can't spell that word from '\(rootWord)'!")
    return
}

guard isReal(word: answer) else {
    wordError(title: "Word not recognized", message: "You can't just make them up, you know!")
    return
}

If you run the app now you should find that it will refuse to let you use words if they fail our tests – trying a duplicate word won’t work, words that can’t be spelled from the root word won’t work, and gibberish words won’t work either.

That’s another app done – good job!

Save 50% in my WWDC sale.

SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.8/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.