Ranges in Swift allow us to select parts of Strings, collections, and other types. They’re the Swift variant of NSRange which we know from Objective-C, although they’re different in usage, as I’ll explain in this blog post.
Ranges allow us to write elegant Swift code by using the range operator. Your first time working with them might be because you needed to select a range of characters from a String, but there’s a lot more you can do with it!
Types of ranges
There are multiple types of ranges in Swift you can use. The easiest way of working with them is by making use of the range operator. Let’s go over the different types available in Swift.
Closed range operator going from a…b
let range: ClosedRange = 0...10
print(range.first!) // 0
print(range.last!) // 10
A closed range operator going from a...b
defines a range that includes both a and b in which a
must not be greater than b
.
The closed operator is useful if you’d like to use all the values. For example, if you’d like to iterate over all elements of a collection:
let names = ["Antoine", "Maaike", "Jaap"]
for index in 0...2 {
print("Name \(index) is \(names[index])")
}
// Name 0 is Antoine
// Name 1 is Maaike
// Name 2 is Jaap
The different types of operators can also be used to select elements from a collection. For this, however, we need to make use of the CountableClosedRange
type:
let names = ["Antoine", "Maaike", "Jaap"]
let range: CountableClosedRange = 0...2
print(names[range]) // ["Antoine", "Maaike", "Jaap"]
Obviously, Swift is smart enough to detect the countable variant by itself. Therefore, you could write the above code as follows:
let names = ["Antoine", "Maaike", "Jaap"]
print(names[0...2]) // ["Antoine", "Maaike", "Jaap"]
Half-open range operator going from a..<b
let range: Range = 0..<10
print(range.first!) // 0
print(range.last!) // 9
A half-open range defines a range from a
to b
but does not include b
. It’s named half-open as it contains its first value but not its final value. Like with the closed range, the value of a
must not be greater than b
.
The half-open operator can be used to iterate over zero-based lists such as arrays and collections in Swift in which you want to iterate up to but not including the length of the list. It’s the same as the earlier code example, but now we can make use of the count
property:
let names = ["Antoine", "Maaike", "Jaap"]
print(names[0..<names.count]) // ["Antoine", "Maaike", "Jaap"]
If we would’ve done the same with a closed operator we would run into the following error:
Fatal error: Array index is out of range
One-sided operator going from a…
A one-sided range operator only defines one side of the bounds, for example, a...
or ...b
. A one-sided range goes as far as possible in one direction—for example, taking all the elements of an array from the start of the array to index 2:
let names = ["Antoine", "Maaike", "Jaap"]
print(names[...2]) // ["Antoine", "Maaike", "Jaap"]
Or taking all the elements starting from index 1 till the end of the array:
let names = ["Antoine", "Maaike", "Jaap"]
print(names[1...]) // ["Maaike", "Jaap"]
A one-sided range can be used for iteration but only if used with a starting value a...
. Otherwise, it’s unclear where the iteration should start. Iterating over a one-sided range requires you to manually check where the loop should end, as it would otherwise continue indefinitely.
let names = ["Antoine", "Maaike", "Jaap"]
let neededNames = 2
var collectedNames: [String] = []
for index in 0... {
guard collectedNames.count != neededNames else { break }
collectedNames.append(names[index])
}
print(collectedNames) // ["Antoine", "Maaike"]
Converting a Range to an NSRange in Swift
Sooner or later you might run into an issue when you want to convert a Range
into a NSRange
type. For example, if you’re working with an NSAttributedString
in which you like to apply attributes to a specific range. In the following example, we’d like to apply an orange color to “Swift” in the title:
let title = "A Swift Blog"
let range = title.range(of: "Swift")
let attributedString = NSMutableAttributedString(string: title)
attributedString.setAttributes([NSAttributedString.Key.foregroundColor: UIColor.orange], range: range) // Cannot convert value of type 'Range<String.Index>?' to expected argument type 'NSRange' (aka '_NSRange')
As Range
can’t be converted to NSRange
we’re running into the following error:
Cannot convert value of type ‘Range?’ to expected argument type ‘NSRange’ (aka ‘_NSRange’)
We can fix this by making use of the available convenience initializer of an NSRange
that takes a Swift Range
:
let convertedRange = NSRange(range, in: title)
The final code will look as follows:
let title = "A Swift Blog"
let range = title.range(of: "Swift")!
let convertedRange = NSRange(range, in: title)
let attributedString = NSMutableAttributedString(string: title)
attributedString.setAttributes([NSAttributedString.Key.foregroundColor: UIColor.orange], range: convertedRange)
print(attributedString)
// A {
// }Swift{
// NSColor = "UIExtendedSRGBColorSpace 1 0.5 0 1";
// } Blog{
// }
Ranges and Strings
Strings and ranges are a bit more special. As you might know, a String
is a collection of characters. However, not every character is of the same size. We can demonstrate this by working with an NSRange
and an NSString
that contains an emoji:
let emojiText: NSString = "🚀launcher"
print(emojiText.substring(with: NSRange(location: 0, length: 7)))
// Expected: 🚀launch
// Actually returns: 🚀launc
// Missing the 'h'
As you can see, the rocket emoji is more than one character long. Therefore, our substring is not returning the expected outcome and misses the ‘h’.
Working with String indexes
The solution to this problem is to make use of Range<String.Index>
instead of Range<Int>
. The String.Index
takes into account the actual size of a character. We can only make use of a half-open Range
as that’s required by the String
subscript.
let emojiText = "🚀launcher"
let endIndex = emojiText.index(emojiText.startIndex, offsetBy: 7)
let range: Range<String.Index> = emojiText.startIndex..<endIndex
print(emojiText[range]) // 🚀launch
Conclusion
That was it! Hopefully, you’ve learned more about the possibilities that Swift gives us when working with ranges and collections. We used closed, half-open, and one-sided operators that all have their pros and cons.
If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.
Thanks!