Indexing strings in Swift iOS 14.07.2019

In Swift String are collections of Character types. A Character is grapheme cluster and is made up of one or more code points.

Indexing into a String to get a certain character (grapheme cluster) is not as simple as using an integer subscript. Swift wants you to be aware of what’s going on under the hood, and so it requires syntax that is a bit more verbose.

You have to operate on the specific string index type in order to index into strings. For example, you obtain the index that represents the start of the string like so:

let str = "Hello Swift"
let firstIndex = str.startIndex

If you option-click on firstIndex in a playground, you’ll notice that it is of type String.Index and not an integer.

You can then use this value to obtain the Character (grapheme cluster) at that index, like so:

let firstChar = str[firstIndex]

In this case, firstChar will of course be H. The type of this value is Character which is a grapheme cluster. Similarly, you can obtain the last grapheme cluster like so:

let lastIndex = str.endIndex
let lastChar = str[lastIndex]

But if you do this, you’ll get a fatal error on the console (and a EXC_BAD_INSTRUCTION error in the code):

Fatal error: String index is out of bounds

This error happens because the endIndex is actually 1 past the end of the string.

You need to do this to obtain the last character:

let lastIndex = str.index(before: str.endIndex)
let lastChar = str[lastIndex]

Here you’re obtaining the index just before the end index then obtaining the character at that index. Alternatively, you could offset from the first character like so:

let fourthIndex = str.index(str.startIndex, offsetBy: 3)
let fourthChar = str[fourthIndex]

Substrings

Another thing that you often need to do when manipulating strings is to generate substrings. That is, pull out a part of the string into its own value. This can be done in Swift using a subscript that takes a range of indices.

For example, consider the following code:

let spaceIndex = str.index(of: " ")!
let firstWord = str[str.startIndex..<spaceIndex]

This code finds the index that represents the first space (using a force unwrap here because you know one exists). Then it uses a range to find the grapheme clusters between the start index and the index of the space (not including the space).

Now is a good time to introduce a new type of range that you haven’t seen before: the open-ended range. This type of range only takes one index and assumes the other is either the start or the end of the collection.

That last line of code can be rewritten by using an open-ended range:

let firstWord = str[..<spaceIndex]

This time we omit the str.startIndex and Swift will infer that this is what you mean.

Similarly, you can also use a one-sided range to start at a certain index and go to the end of the collection, like so:

let secondWord = str[str.index(after: spaceIndex)...]

There’s something interesting to point out with substrings. If you look at their type, then you will see they are of type String.SubSequence rather than String. This String.SubSequence is actually just a typealias of Substring, which means that Substring is the actual type, and String.SubSequence is an alias.

Just like with the reversed string, you can force this Substring into a String by doing the following:

let secondWordString = String(secondWord)

The reason for this extra Substring type is a cunning optimization. A Substring shares the storage with its parent String that it was sliced from. This means that when you’re in the process of slicing a string, you use no extra memory. Then, when you want the substring as a String you explicitly create a new string and the memory is copied into a new buffer for this new string.

Extension

extension String {
    subscript(characterIndex: Int) -> Self {
        return String(self[index(startIndex, offsetBy: characterIndex)])
    }
}

Useful links