String.Index values with str.index(str.startIndex, offsetBy: n), then subscript with a range such as str[start..<end]. Wrap the result in String(...) if you need a String instead of a Substring. For simple cases, prefix(_:), suffix(_:), dropFirst(_:) and dropLast(_:) are shorter and safer.Older Swift tutorials and Objective-C habits point you to substringWithRange. That method doesn’t exist in current Swift. Code like str[0..<3] also won’t compile, and the compiler error isn’t much help:
'subscript(_:)' is unavailable: cannot subscript String with an Int, use a String.Index instead.
This guide covers how ranges actually work on Swift strings and the cleanest way to cut out a piece of text.
Why this happens
A Swift String is a collection of Character values. Each Character is an extended grapheme cluster, which is what a user sees as one character. One visible character can be made of several Unicode scalars and a different number of bytes. For example, "👍🏽" has a count of 1 but takes 4 UTF-16 code units.
Characters have variable width, so Swift can’t jump to “character number 5” in constant time. It has to walk the string from the start. Swift doesn’t hide that cost behind an Int subscript. It makes you use String.Index, an opaque position that’s only valid for the string it came from.
The old substringWithRange(_:) and advancedBy(_:) APIs came from Swift 1 and 2. Swift 3 renamed them. Swift 4 deprecated substring(from:) and substring(to:) and replaced them with range subscripts that return a Substring.
How to fix it
1. Build String.Index values and subscript with a range
This is the direct replacement for substringWithRange. Use index(_:offsetBy:) to move from a known index, then pass a range to the subscript:
let str = "Hello, playground"
let start = str.index(str.startIndex, offsetBy: 2)
let end = str.index(str.endIndex, offsetBy: -1)
let middle = str[start..<end] // "llo, playgroun" (Substring)
let asString = String(middle) // convert when you need a String
All the usual range operators work:
let text = "My String"
let a = text.index(text.startIndex, offsetBy: 3)
let b = text.index(text.startIndex, offsetBy: 7)
String(text[a...b]) // "Strin" closed range
String(text[..<a]) // "My " up to, not including a
String(text[a...]) // "String" from a to the end
The one-sided ranges ..<a and a... replace the deprecated substring(to:) and substring(from:).
2. Use prefix, suffix, dropFirst and dropLast for simple cuts
If you only need the start or end of a string, skip index math entirely:
let str = "Hello, playground"
str.prefix(5) // "Hello"
str.suffix(10) // "playground"
str.dropFirst(7) // "playground"
str.dropLast(12) // "Hello"
These methods don’t crash when the count is larger than the string. "Hi".prefix(10) returns "Hi". That makes them the safest choice for things like truncating user input.
3. Use offsets safely with limitedBy
index(_:offsetBy:) crashes at runtime if you move past the end. When offsets come from user data or a server, use the limitedBy: overload, which returns nil instead:
extension StringProtocol {
/// Returns `length` characters starting at `offset`, or nil if out of bounds.
func slice(from offset: Int, length: Int) -> SubSequence? {
guard offset >= 0, length >= 0,
let start = index(startIndex, offsetBy: offset, limitedBy: endIndex),
let end = index(start, offsetBy: length, limitedBy: endIndex)
else { return nil }
return self[start..<end]
}
}
"Hello, playground".slice(from: 7, length: 4) // "play"
"Hello".slice(from: 3, length: 10) // nil
4. Find the range instead of counting characters
Often you don’t need numeric offsets at all. You want the text around a known character or word. Search for the index directly:
import Foundation
let str = "Hello, playground"
// Standard library
if let comma = str.firstIndex(of: ",") {
let greeting = str[..<comma] // "Hello"
let rest = str[str.index(after: comma)...] // " playground"
}
// Foundation
if let range = str.range(of: "play") {
let afterPlay = str[range.upperBound...] // "ground"
}
This works with Unicode and survives changes to the text better than hard-coded numbers.
5. Convert between NSRange and Range
Some APIs, such as NSRegularExpression, NSAttributedString and many UIKit text delegate methods, still return NSRange. Don’t cast to NSString and slice with the NSRange. Convert it instead:
import Foundation
let str = "Hello, playground"
let nsRange = NSRange(location: 0, length: 5)
if let range = Range(nsRange, in: str) {
let piece = str[range] // "Hello"
}
// And back again, for APIs that want NSRange:
let whole = NSRange(str.startIndex..<str.endIndex, in: str)
You can still do (str as NSString).substring(with: NSRange(location: 0, length: 3)). Keep in mind that NSString counts UTF-16 code units, not characters. With emoji, flags or combined accents, a hand-built NSRange can split a character in half. Only use NSRange values that came from an NSString-based API on the same text.
6. Optional: an Int-range subscript for quick scripts
If you want str[2..<5] syntax for prototypes or playgrounds, you can add it yourself:
extension StringProtocol {
subscript(offsets range: Range<Int>) -> SubSequence {
let start = index(startIndex, offsetBy: range.lowerBound)
let end = index(start, offsetBy: range.count)
return self[start..<end]
}
}
"Hello, playground"[offsets: 7..<11] // "play"
This helper is O(n) on every call and crashes on out-of-bounds offsets. Using it inside a loop can quietly turn linear code into quadratic code. In production code, prefer options 1 to 5.
Substring vs String: when to convert
Range subscripts return Substring, not String. A Substring shares storage with the original string, so slicing is cheap. It also keeps the whole original string in memory for as long as the slice exists.
Keep Substring for short-lived local work. Convert with String(slice) when you store the value in a property, return it from a public API, or pass it to a function that takes String. If you write your own helpers against StringProtocol, they accept both types.
How to prevent it
- Treat
String.Indexas tied to one string. Don’t reuse an index from one string on another, even if their contents look identical. - Reach for
prefix,suffix,firstIndex(of:)andrange(of:)before you write offset math. - Use
index(_:offsetBy:limitedBy:)whenever offsets come from outside your code. - Convert
NSRangewithRange(_:in:)instead of slicing throughNSString. - Test string code with emoji and accented characters such as
"café"and"👍🏽", not just ASCII. - Wrap slices in
String(...)before you store them long term.
FAQ
Why doesn’t Swift let me write str[0..<3]?
Characters in a Swift string have variable width. Finding the Nth character means walking the string from the start. Swift requires String.Index so that this cost is visible and indexing never splits a character.
Is str.count the same as (str as NSString).length?
No. count gives the number of user-visible characters. NSString.length gives the number of UTF-16 code units. They match for plain ASCII but differ for emoji and many non-Latin scripts. That difference is why mixing Int offsets between the two causes bugs.
Do I always need to convert a Substring to String?
No. Many APIs accept StringProtocol, and Substring supports most String methods. Convert when a function requires a String or when you keep the value around. A retained Substring also keeps its parent string’s memory alive.
Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.
