Positions in a string
Every character in a string has a position, counted from zero. In "Hello world!", H is at 0, e at 1, and the space is at 5, because spaces are characters too. The w is at 6.
Many languages let you write text[6]. Swift doesn't. A Swift String can't be subscripted with an Int, and this line:
let fruit = "Avocado"
print(fruit[3])
fails to compile with cannot subscript String with an Int, use a String.Index instead. The reason is that a Swift character is a user-perceived character, which can be made of several Unicode pieces and so takes different amounts of storage. Finding the 7th character means walking the string from the start, which isn't a constant-time lookup, so Swift makes you ask for it explicitly.
Option 1: convert to an Array
An Array of characters can be indexed with integers:
let message = "Hello world!"
let chars = Array(message)
print(chars[0]) // H
print(chars[1]) // e
print(chars[6]) // w
The array holds Character values, one per element. It is a copy, so it costs time and memory proportional to the string's length, but afterwards each lookup is fast. Printing three neighbours shows that the space has its own index:
print("\(chars[4])\(chars[5])\(chars[6])")
This prints o w.
Option 2: build a String.Index
The method index(_:offsetBy:) moves from a starting position by a number of characters and returns a String.Index, the type a string's subscript accepts:
let fruit = "Avocado"
let i = fruit.index(fruit.startIndex, offsetBy: 3)
print(fruit[i]) // c
fruit.startIndex is the position of the first character. An offset of 0 gives the first character, and an offset of fruit.count - 1 gives the last. The result is a Character, so printing it shows just the letter.
Handy shortcuts
fruit.firstandfruit.lastare optionals, because an empty string has neither.fruit.first!isA. Useif letor??for strings that may be empty.fruit[fruit.startIndex]is the first character with no offset.fruit.index(before: fruit.endIndex)is the last one, becauseendIndexpoints just past the end.fruit.index(after: fruit.startIndex)is the second character.
Out of range crashes
The offset must stay within the string. Using fruit[fruit.index(fruit.startIndex, offsetBy: 7)] on "Avocado", which has 7 characters, points past the end and the program stops with Fatal error: String index is out of bounds. The valid offsets run from 0 to count - 1. An array has the same limit: chars[7] traps with an index-out-of-range error.
count counts characters, not bytes. In "héllo", count is 5 even though the UTF-8 form takes 6 bytes, and offset 1 gives é.
The exercise
The starting code is let fruit = "Avocado", and both steps use the index method:
- The first character. An offset of
0fromstartIndex, then print it. - The last character. "Avocado" has 7 characters, so the last one is at offset
6.
let fruit = "Avocado"
print(fruit[fruit.index(fruit.startIndex, offsetBy: 0)])
print(fruit[fruit.index(fruit.startIndex, offsetBy: 6)])
The output is two lines, A and then o.
Common mistakes
- Writing
fruit[0]. It doesn't compile. - Counting from 1. The first character is at offset 0, so the last of 7 is at 6, not 7.
- Using an index from one string on another. A
String.Indexbelongs to the string that produced it. - Looping with
index(_:offsetBy:)for every position. Each call walks from the start, so the loop gets slow on long strings. Usefor c in fruitor convert to an array.
Key takeaways
- Positions start at 0 and spaces count.
- Swift strings can't be indexed with integers; use
Array(text)or aString.Index. text.index(text.startIndex, offsetBy: n)builds the index for positionn.- An index past the end crashes the program.
