Swift Substring: Fix ‘String’ has no member ‘substringWithRange’

Swift Substring: Fix 'String' has no member 'substringWithRange'

Written by

in

Quick answer: Swift 3 removed the NSString-style substringWithRange and replaced it with String.Index-based APIs. Use prefix(_:), suffix(_:), str.index(_:offsetBy:) with a range subscript, or range(of:) — and convert the resulting Substring back to String when you need to keep it.

Why this happens

substringWithRange(_:), substring(to:) and substring(from:) were never native Swift methods. They came from NSString, which Swift 2 bridged onto String for convenience. When Swift 3 reworked the String API, those bridged methods were dropped, so old code that compiled against Swift 2 now fails with:

Value of type 'String' has no member 'substringWithRange'

The replacement is index-based. A String is a collection of Character values, and a single character can be made of several Unicode scalars (a flag emoji, an accented letter, a family emoji). Because of that, a character’s position in memory is not a simple integer offset, so Swift makes you work with String.Index instead of Int.

Swift 4 added a second change on top: slicing a String now returns a Substring, not a String. A Substring shares storage with the original string, which makes slicing cheap, but it keeps the whole parent string alive while it exists. Convert it with String(_:) once you are done slicing.

How to fix it

All examples below use:

let str = "Hello, playground"

1. Use prefix and suffix when you know the length

This is the shortest replacement for the old substring(to:) and substring(from:) calls.

let head = str.prefix(5)   // "Hello"
let tail = str.suffix(10)  // "playground"

Both return a Substring. If you need a real String — to store it in a model, pass it to an API that expects String, or let the parent string be freed — wrap it:

let head: String = String(str.prefix(5))

There are also index-taking variants, prefix(upTo:) and suffix(from:), which are useful when the boundary comes from a search rather than a count.

2. Use index(_:offsetBy:) with a range subscript

For a slice in the middle of a string, build the two indices and subscript with a range. Note that advancedBy from Swift 2 is now offsetBy.

let start = str.index(str.startIndex, offsetBy: 7)
let end = str.index(str.endIndex, offsetBy: -6)
let middle = str[start..<end]  // "play"

One-sided ranges (Swift 4 and later) cover the common “everything before” and “everything after” cases without computing a second index:

let index = str.index(str.startIndex, offsetBy: 5)
let before = str[..<index]  // "Hello"

let fromEnd = str.index(str.endIndex, offsetBy: -10)
let after = str[fromEnd...] // "playground"

offsetBy walks the string, so it is linear in the offset. That is fine for occasional slicing and bad inside a tight loop over a long string.

3. Use range(of:) when you are searching, not counting

If the substring is delimited by text rather than by a position, do not compute offsets by hand. Search for the delimiter and slice with the range you get back.

if let range = str.range(of: "play") {
    let word = str[range]  // "play"
}

This is also the safe way to bridge to Foundation APIs that want an NSRange:

import Foundation

let nsRange = NSRange(location: 7, length: 4)
if let range = Range(nsRange, in: str) {
    let word = str[range]  // "play"
}

Range(_:in:) returns an optional because an NSRange built for UTF-16 can land in the middle of a character and has no valid String.Index equivalent. Always unwrap it.

4. Add an Int-based helper if you really want integer offsets

Many people find String.Index clumsy. A small extension gives you the old integer feel back while still going through the official API:

extension String {
    func index(from offset: Int) -> Index {
        index(startIndex, offsetBy: offset)
    }

    func substring(from offset: Int) -> String {
        String(self[index(from: offset)...])
    }

    func substring(to offset: Int) -> String {
        String(self[..<index(from: offset)])
    }

    func substring(with range: Range<Int>) -> String {
        let lower = index(from: range.lowerBound)
        let upper = index(from: range.upperBound)
        return String(self[lower..<upper])
    }
}

let str = "Hello, playground"
print(str.substring(from: 7))       // playground
print(str.substring(to: 5))         // Hello
print(str.substring(with: 7..<11))  // play

Two caveats. First, the offsets count Character values, not UTF-16 units, so they will not match the numbers you see in an NSRange or in a text editor that counts bytes. Second, an out-of-range offset traps at runtime just like an out-of-range array index — clamp the value or check count first if the number comes from outside your code.

If you only need one character, str[str.index(str.startIndex, offsetBy: i)] works, but Array(str)[i] is easier to read when you need several characters by position and the string is short.

How to prevent it

  • Treat substringWithRange, substring(to:) and substring(from:) as Swift 2 relics; if you see them in a migration, rewrite them rather than hunting for a shim.
  • Prefer prefix, suffix, split and range(of:) over manual index math — they express intent and avoid off-by-one mistakes.
  • Convert Substring to String before storing it in a property, returning it from a function, or handing it to anything that outlives the current scope.
  • Never assume a String.Index from one string is valid in another; indices are tied to the string that produced them.
  • When mixing with Foundation, keep NSRange and Range<String.Index> conversions in one place and always unwrap Range(_:in:).

FAQ

Why doesn’t str[0] compile in Swift?

Because String is not subscripted by Int. Its subscript takes a String.Index or a range of them, since a character can span several Unicode scalars and has no fixed integer position. Use str.index(str.startIndex, offsetBy: 0) or str.first instead.

Is Substring the same as String?

No. Substring is a separate type that shares storage with the original string, so slicing is cheap but keeps the parent alive. Most APIs that take text want String, so convert with String(mySubstring) when you are finished slicing.

Does the Int extension break with emoji or accented characters?

It counts Character values, so a family emoji or an accented letter counts as one offset, which is usually what you want. It will not match UTF-16 offsets from NSRange, and it traps if the offset is past the end of the string, so validate input that comes from outside your code.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.