Finding text inside a string
To find where a piece of text appears in a string, Swift gives you range(of:). It comes from Foundation, so a standalone file needs import Foundation at the top. The method returns the range the match occupies, as an optional: a range if the text is found, and nil if it isn't.
import Foundation
let sentence = "Hello World"
print(sentence.range(of: "xyz") as Any)
print(sentence.range(of: "World") != nil)
nil
true
A range has two ends, lowerBound and upperBound. The lower bound is the index of the match's first character, and the upper bound is the index just after its last character.
From an index to a number
The bounds are String.Index values, not integers. A String.Index can be used to read a character or to cut out a slice, but it doesn't print as a number. To turn one into a position, count the characters from the start with distance(from:to:):
import Foundation
let sentence = "Hello World"
if let range = sentence.range(of: "World") {
let idx = sentence.distance(from: sentence.startIndex, to: range.lowerBound)
print(idx)
}
6
The match starts at character 6: H e l l o and the space take positions 0 to 5, and W comes next. Here if let unwraps the optional safely, so the block runs only when the text is found.
Options for the search
Two extra arguments change the search:
options: .backwardssearches from the end, so you get the last occurrence.options: .caseInsensitiveignores the difference between upper and lower case.
import Foundation
let text = "Hello World Hello"
if let first = text.range(of: "Hello") {
print(text.distance(from: text.startIndex, to: first.lowerBound))
}
if let last = text.range(of: "Hello", options: .backwards) {
print(text.distance(from: text.startIndex, to: last.lowerBound))
}
print(text.range(of: "hello") as Any)
print(text.range(of: "hello", options: .caseInsensitive) != nil)
0
12
nil
true
You can also limit the search to part of the string with the range: argument. For example, text.range(of: "Hello", range: from..<text.endIndex), with from an index in text, skips everything before from. Matching is case-sensitive by default.
Without Foundation
The standard library has its own search methods. contains("World") answers a yes-or-no question, and firstRange(of:) returns the same optional range as range(of:):
let sentence = "Hello World"
print(sentence.contains("World"))
if let r = sentence.firstRange(of: "World") {
print(sentence.distance(from: sentence.startIndex, to: r.lowerBound))
}
true
6
To find a single character rather than a piece of text, use firstIndex(of:), or lastIndex(of:) for the last one. Both return an optional index.
A reusable helper
When the text may be missing, wrap the search in a function that returns an optional Int. guard let leaves early on nil:
import Foundation
func position(of piece: String, in s: String) -> Int? {
guard let r = s.range(of: piece) else { return nil }
return s.distance(from: s.startIndex, to: r.lowerBound)
}
let sentence = "Hello World"
print(position(of: "World", in: sentence) ?? -1)
print(position(of: "xyz", in: sentence) ?? -1)
6
-1
The caller decides what a missing match means, here by using -1 as a default with ??.
The exercise
The constant sentence holds "Hello World", and you create a constant index that holds where "World" starts. Three parts fit together:
sentence.range(of: "World")returns an optional range.- The
!unwraps it. That is safe only because"World"is known to be in the string; onnilthe force-unwrap crashes the program. .lowerBoundis the index of the first matching character, anddistance(from:to:)turns it into an integer.
let sentence = "Hello World"
let index = sentence.distance(from: sentence.startIndex, to: sentence.range(of: "World")!.lowerBound)
print(index)
6
Common mistakes
- Printing the range directly. It shows internal index data, not a position. Convert with
distance(from:to:). - Force-unwrapping an unknown result. When the text may be missing, use
if letorguard let. - Forgetting case.
range(of: "world")doesn't match"World"unless you pass.caseInsensitive. - Using
upperBoundfor the position. It points after the match, so here it would give 11, not 6.
Key takeaways
range(of:)returns an optional range:nilmeans not found.lowerBoundis where the match starts. Usedistance(from:to:)to get an integer.range(of:)needs Foundation.containsandfirstRange(of:)work without it.
