NEW! Pre-order my latest book, Testing Swift! >>

< Previous: Prepare for submission: lowercased() and IndexPath   Next: Or else what? >

Returning values: contains

As you’ve seen, the return keyword exits a method at any time it's used. If you use return by itself, it exits the method and does nothing else. But if you use return with a value, it sends that value back to whatever called the method. We’ve used it previously to send back the number of rows in a table, for example.

Before you can send a value back, you need to tell Swift that you expect to return a value. Swift will automatically check that a value is returned and it's of the right data type, so this is important. We just put in stubs (empty methods that do nothing) for three new methods, each of which returns a value. Let's take a look at one in more detail:

func isOriginal(word: String) -> Bool {
    return true
}

The method is called isOriginal(), and it takes one parameter that's a string. But before the opening brace there's something important: -> Bool. This tells Swift that the method will return a boolean value, which is the name for a value that can be either true or false.

The body of the method has just one line of code: return true. This is how the return statement is used to send a value back to its caller: we're returning true from this method, so the caller can use this method inside an if statement to check for true or false.

This method can have as much code as it needs in order to evaluate fully whether the word has been used or not, including calling any other methods it needs. We're going to change it so that it calls another method, which will check whether our usedWords array already contains the word that was provided. Replace its current return true code with this:

return !usedWords.contains(word)

There are two new things here. First, contains() is a method that checks whether the array specified in parameter 1 (usedWords) contains the value specified in parameter 2 (word). If it does contain the value, contains() returns true; if not, it returns false. Second, the ! symbol. You've seen this before as the way to force unwrap optional variables, but here it's something different: it means not.

The difference is small but important: when used before a variable or constant, ! means "not" or "opposite". So if contains() returns true, ! flips it around to make it false, and vice versa. When used after a variable or constant, ! means "force unwrap this optional variable."

This is used because our method is called isOriginal(), and should return true if the word has never been used before. If we had used return usedWords.contains(word), then it would do the opposite: it would return true if the word had been used and false otherwise. So, by using ! we're flipping it around so that the method returns true if the word is new.

That's one method down. Next is the isPossible(), which also takes a string as its only parameter and returns a Bool – true or false. This one is more complicated, but I've tried to make the algorithm as simple as possible.

How can we be sure that "cease" can be made from "agencies", using each letter only once? The solution I've adopted is to loop through every letter in the player's answer, seeing whether it exists in the eight-letter start word we are playing with. If it does exist, we remove the letter from the start word, then continue the loop. So, if we try to use a letter twice, it will exist the first time, but then get removed so it doesn't exist the next time, and the check will fail.

You already met the range(of:) method in project 4, so this should be straightforward:

func isPossible(word: String) -> Bool {
    var tempWord = title!.lowercased()

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

    return true
}

Our usage of range(of:) is a little different than from project 4. Remember, range(of:) returns an optional position of where the item was found – meaning that it might be nil. So, we wrap the call into an if let to safely unwrap the optional.

The usage is also different because we use String(letter) rather than just letter. This is because our for loop is used on a string, and it pulls out every letter in the string as a new data type called Character – i.e., a single letter. range(of:) expects a string, not a character, so we need to create a string from the character using String(letter).

If the letter was found in the string, we use remove(at:) to remove the used letter from the tempWord variable. This is why we need the tempWord variable at all: because we'll be removing letters from it so we can check again the next time the loop goes around.

The method ends with return true, because this line is reached only if every letter in the user's word was found in the start word no more than once. If any letter isn't found, or is used more than possible, one of the return false lines would have been hit, so by this point we're sure the word is good.

Important: we have told Swift that we are returning a boolean value from this method, and it will check every possible outcome of the code to make sure a boolean value is returned no matter what.

Time for the final method. Replace the current isReal() method with this:

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

    return misspelledRange.location == NSNotFound
}

There's a new class here, called UITextChecker. This is an iOS class that is designed to spot spelling errors, which makes it perfect for knowing if a given word is real or not. We're creating a new instance of the class and putting it into the checker constant for later.

There's a new function call here too, called NSMakeRange(). This is used to make a string range, which is a value that holds a start position and a length. We want to examine the whole string, so we use 0 for the start position and the string's length for the length.

Next, we call the rangeOfMisspelledWord(in:) method of our UITextChecker instance. This wants five parameters, but we only care about the first two and the last one: the first parameter is our string, word, the second is our range to scan (the whole string), and the last is the language we should be checking with, where en selects English.

Parameters three and four aren't useful here, but for the sake of completeness: parameter three selects a point in the range where the text checker should start scanning, and parameter four lets us set whether the UITextChecker should start at the beginning of the range if no misspelled words were found starting from parameter three. Neat, but not helpful here.

Calling rangeOfMisspelledWord(in:) returns an NSRange structure, which tells us where the misspelling was found. But what we care about was whether any misspelling was found, and if nothing was found our NSRange will have the special location NSNotFound. Usually location would tell you where the misspelling started, but NSNotFound is telling us the word is spelled correctly – i.e., it's a valid word.

Note: In case you were curious, NSRange pre-dates Swift, and therefore doesn’t have access to optionals – NSNotFound is effectively a magic number that means “not found”, assigned to a constant to make it easier to use.

Here the return statement is used in a new way: as part of an operation involving ==. This is a very common way to code, and what happens is that == returns true or false depending on whether misspelledRange.location is equal to NSNotFound. That true or false is then given to return as the return value for the method.

We could have written that same line across multiple lines, but it's not common:

if misspelledRange.location == NSNotFound {
    return true
} else {
    return false
}

That completes the third of our missing methods, so the project is almost complete. Run it now and give it a thorough test!

Before we continue, there’s one small thing I want to touch on briefly. In the isPossible() method we looped over each letter by treating the word as an array, but in this new code we use word.utf16 instead. Why?

The answer is an annoying backwards compatibility quirk: Swift’s strings natively store international characters as individual characters, e.g. the letter “é” is stored as precisely that. However, UIKit was written in Objective-C before Swift’s strings came along, and it uses a different character system called UTF-16 – short for 16-bit Unicode Transformation Format – where the accent and the letter are stored separately.

It’s a subtle difference, and often it isn’t a difference at all, but it’s becoming increasingly problematic because of the rise of emoji – those little images that are frequently used in messages. Emoji are actually just special character combinations behind the scenes, and they are measured differently with Swift strings and UTF-16 strings: Swift strings count them as 1-letter strings, but UTF-16 considers them to be 2-letter strings. This means if you use count with UIKit methods, you run the risk of miscounting the string length.

I realize this seems like pointless additional complexity, so let me try to give you a simple rule: when you’re working with UIKit, SpriteKit, or any other Apple framework, use utf16.count for the character count. If it’s just your own code - i.e. looping over characters and processing each one individually – then use count instead.

Love Hacking with Swift?

Get all 40 projects in PDF and HTML: buy the Hacking with Swift book! It contains over 1300 pages of hands-on Swift coding, and will really help boost your iOS career

< Previous: Prepare for submission: lowercased() and IndexPath   Next: Or else what? >
MASTER SWIFT NOW
Buy Practical iOS 12 Buy Pro Swift Buy Swift Design Patterns Buy Practical iOS 11 Buy Swift Coding Challenges Buy Server-Side Swift (Vapor Edition) Buy Server-Side Swift (Kitura Edition) Buy Hacking with macOS Buy Advanced iOS Volume One Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with Swift Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let me know!

Click here to visit the Hacking with Swift store >>