This error means you can't use an Int in the subscript format – you have to use a String.Index, which you can initialize with an encodedOffset Int.
let myString: String = "foobar"
let lowerBound = String.Index.init(encodedOffset: 1)
let upperBound = String.Index.init(encodedOffset: 3)
let mySubstring: Substring = myString[lowerBound..<upperBound]
Where is "the documentation comment for discussion" that was referred to in the error?
It's on GitHub in the Swift Standard Library repository in a file called UnavailableStringAPIs.swift.gyb in the bottom of a locked filing cabinet stuck in a disused lavatory with a sign on the door saying 'Beware of the Leopard'. link
Subscripting a string with Int has never been available in Swift's Standard Library. This code has been invalid for as long as Swift exists:
let mySubstring: Substring = myString[1..<3]
The new String.Index(encodedOffset: ) returns an index in UTF-16 (16-bit) encoding. Swift's string uses Extended Grapheme Cluster, which can take between 8 and 64 bits to store a character. Emojis make for very good demonstration:
let myString = "🇺🇸🇨🇦🇬🇧🇫🇷"
let lowerBound = String.Index(encodedOffset: 1)
let upperBound = String.Index(encodedOffset: 3)
let mySubstring = myString[lowerBound..<upperBound]
// Expected: Canadian and UK flags
// Actual : gibberish
print(mySubstring)
In fact, getting the String.Index has not changed at all in Swift 4, for better or worse:
let myString = "🇺🇸🇨🇦🇬🇧🇫🇷"
let lowerBound = myString.index(myString.startIndex, offsetBy: 1)
let upperBound = myString.index(myString.startIndex, offsetBy: 3)
let mySubstring = myString[lowerBound..<upperBound]
print(mySubstring)
If you want to use subscripts on Strings like "palindrome"[1..<3] and "palindrome"[1...3], use these extensions.
Swift 4
extension String {
subscript (bounds: CountableClosedRange<Int>) -> String {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return String(self[start...end])
}
subscript (bounds: CountableRange<Int>) -> String {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return String(self[start..<end])
}
}
Swift 3
For Swift 3 replace with return self[start...end] and return self[start..<end].
Apple didn't build this into the Swift language because the definition of a 'character' depends on how the String is encoded. A character can be 8 to 64 bits, and the default is usually UTF-16. You can specify other String encodings in String.Index.
extension StringProtocol {
subscript(bounds: CountableClosedRange<Int>) -> SubSequence {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(start, offsetBy: bounds.count)
return self[start..<end]
}
subscript(bounds: CountableRange<Int>) -> SubSequence {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(start, offsetBy: bounds.count)
return self[start..<end]
}
}
Notable changes:
Now an extension of StringProtocol. This allows adopters such as Substring to also gain these subscripts.
End indices are offset from the start index of the bounds rather than the start of the string. This prevents traversing from the start of the String twice. The index method is O(n) where n is the offset from i.
Building on both p-sun's and Justin Oroz's answers, here are two extensions that protect against invalid indexes beyond the start and end of a string (these extensions also avoid rescanning the string from the beginning just to find the index at the end of the range):
extension String {
subscript(bounds: CountableClosedRange<Int>) -> String {
let lowerBound = max(0, bounds.lowerBound)
guard lowerBound < self.count else { return "" }
let upperBound = min(bounds.upperBound, self.count-1)
guard upperBound >= 0 else { return "" }
let i = index(startIndex, offsetBy: lowerBound)
let j = index(i, offsetBy: upperBound-lowerBound)
return String(self[i...j])
}
subscript(bounds: CountableRange<Int>) -> String {
let lowerBound = max(0, bounds.lowerBound)
guard lowerBound < self.count else { return "" }
let upperBound = min(bounds.upperBound, self.count)
guard upperBound >= 0 else { return "" }
let i = index(startIndex, offsetBy: lowerBound)
let j = index(i, offsetBy: upperBound-lowerBound)
return String(self[i..<j])
}
}