Find index of Nth instance of substring in string in Swift

Xcode 11 • Swift 5 or later

let sentence = "hey hi hello, hey hi hello"
let query = "hello"
var searchRange = sentence.startIndex..<sentence.endIndex
var indices: [String.Index] = []

while let range = sentence.range(of: query, options: .caseInsensitive, range: searchRange) {
    searchRange = range.upperBound..<searchRange.upperBound
    indices.append(range.lowerBound)
}

print(indices)   // "[7, 21]\n"

Another approach is NSRegularExpression which is designed to easily iterate through matches in an string. And if you use the .ignoreMetacharacters option, it will not apply any sophisticated wildcard/regex logic, but will just look for the string in question. So consider:

let string = "hey hi hello, hey hi hello"  // string to search within
let searchString = "hello"                 // string to search for
let matchToFind = 2                        // grab the second occurrence

let regex = try! NSRegularExpression(pattern: searchString, options: [.caseInsensitive, .ignoreMetacharacters])

You could use enumerateMatches:

var count = 0
let range = NSRange(string.startIndex ..< string.endIndex, in: string)
regex.enumerateMatches(in: string, range: range) { result, _, stop in
    count += 1
    if count == matchToFind {
        print(result!.range.location)
        stop.pointee = true
    }
}

Or you can just find all of them with matches(in:range:) and then grab the n'th one:

let matches = regex.matches(in: string, range: range)
if matches.count >= matchToFind {
    print(matches[matchToFind - 1].range.location)
}

Obviously, if you were so inclined, you could omit the .ignoreMetacharacters option and allow the user to perform regex searches, too (e.g. wildcards, whole word searches, start of word, etc.).

For Swift 2, see previous revision of this answer.