Start learning free

LESSON 62 OF 415 · STRINGS I

Finding a Substring in Swift: range(of:) and distance

Lesson Read the full explanation

In Swift, you can find the range of a substring using range(of:), which comes from Foundation, so a standalone file needs import Foundation at the top:

s.range(of: sub)                                    // returns Range? of first occurrence, or nil if not found
s.range(of: sub, options: .backwards)              // returns Range? of last occurrence
s.range(of: sub, range: fromIdx..

Here are some examples:

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
}

print(sentence.range(of: "xyz"))  // nil  (not found)

The index returned is the position of the first character of the found substring.


Assignment
Follow the Coding Tutorial and find the index of "World" in sentence!


Hint
Use sentence.range(of: "World") to locate the substring (it returns an optional; ! unwraps it when you know the substring is there), then store the distance from sentence.startIndex to its lowerBound in index.

STEP 1 OF 1
    Ctrl + Enter to check

    SWIFT · STRINGS I

    String Find in Swift, explained

    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: .backwards searches from the end, so you get the last occurrence.
    • options: .caseInsensitive ignores 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:

    1. sentence.range(of: "World") returns an optional range.
    2. The ! unwraps it. That is safe only because "World" is known to be in the string; on nil the force-unwrap crashes the program.
    3. .lowerBound is the index of the first matching character, and distance(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 let or guard let.
    • Forgetting case. range(of: "world") doesn't match "World" unless you pass .caseInsensitive.
    • Using upperBound for the position. It points after the match, so here it would give 11, not 6.

    Key takeaways

    • range(of:) returns an optional range: nil means not found.
    • lowerBound is where the match starts. Use distance(from:to:) to get an integer.
    • range(of:) needs Foundation. contains and firstRange(of:) work without it.
    Keep going with SwiftAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free