Swift and NSScanner: Coping with Cocoa


An NSScanner is instantiated with a String and once instantiated it can be searched. This searching occurs via a stepping through of the string. For example, let's suppose we have the following string, "Swift 1.2", and want to set up a scan for the letters in this string:
// first we create an NSScanner instance
let scanner = NSScanner(string: "Swift 1.2")
// then we create a string into which the scan will output
var str:NSString?
// finally we perform the scan
scanner.scanCharactersFromSet(NSCharacterSet.letterCharacterSet(), intoString: &str) // true
// and here we have the result
str // "Swift"

Moving on

But suppose we ran the same scan again:
scanner.scanCharactersFromSet(NSCharacterSet.letterCharacterSet(), intoString: &str) // false
scanner.string // "Swift 1.2"
This time the result is false, not because the string has changed or been consumed but because the scanner has moved forwards, as we see when we examine the .scanLocation.
scanner.scanLocation // 5
And simply reseting or changing the scanLocation enables us to find the same string again.
scanner.scanLocation = 0

scanner.scanCharactersFromSet(NSCharacterSet.letterCharacterSet(), intoString: &str) // true

Double or nothing

The scanner can seek more than just strings containing characters from a set, it can also look for Doubles, Floats, Integers and so on:
var num = Double()
scanner.scanDouble(&num) // true
num // 1.2
However, let's reset to the original scanLocation and see how it fares then:
scanner.scanLocation = 0
scanner.scanDouble(&num) // false

Ignorance is bliss

You'll see that scanDouble now returns false, this is because as soon as a scanner doesn't match what it is looking for it stops. So if there's a letter before the number, it won't find the number. This is unless we ignore certain characters.
var num = Double()
scanner.scanLocation = 0

let skipCharacters = NSMutableCharacterSet()
skipCharacters.formUnionWithCharacterSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
skipCharacters.formUnionWithCharacterSet(NSCharacterSet.letterCharacterSet())

scanner.charactersToBeSkipped = skipCharacters
scanner.scanDouble(&num)
num // 1.2
The default is to skip whitespace and newline characters, and in order to change this we need to set the skipCharacters property.

I thank NSHipster here for a post on NSScanner for providing information on how to combine NSCharacterSets using formUnionWithCharacterSet().

Up to no good

If you don't know what you are looking for, but instead know what you are not looking for, you can search up to the characters that are not wanted instead:
scanner.charactersToBeSkipped  = nil
scanner.scanLocation = 0
scanner.scanUpToCharactersFromSet(NSCharacterSet.decimalDigitCharacterSet(), intoString: &str)
str // "Swift "
And you'll notice here that the white space at the end of the string is included when we search up to the first number, because the charactersToBeSkipped property was set to nil.

Conclusion

Aside from NSCharacterSet and NSScanner being Cocoa classes, you'll notice that the scanner's intoString must, at the time of writing, be an NSString and not a String. But apart from this NSScanner behaves nicely with Swift.

Endorse on Coderwall

Comments