Ranges in Swift Explained

Written by Reinder de Vries on September 9 2020 in App Development, Swift

Ranges in Swift Explained

You use ranges in Swift to define values between a lower and upper limit. Ranges are useful for creating slices of arrays, checking if a value is contained in a range, and much more.

In this tutorial, we’ll discuss how you can work with ranges in Swift. We’ll discuss ranges vs. arrays, types of ranges, working with ranges and strings, and tips and tricks for practical iOS development.

Ready? Let’s go.

  1. Types of Ranges in Swift
  2. Working with Ranges and Arrays
  3. Using Strings with Ranges
  4. Pro Tips and Tricks
  5. Further Reading

Types of Ranges in Swift

First things first. What’s a range? A range in Swift defines stuff – usually intgers – between a lower and upper limit. It’s the same as that we’re saying: “A zoo has animals ranging from aardvarks to zebras.” Tigers and dolphins fit in that range.

Here’s an example of a range in Swift:

let allAges = 0...99
print(allAges.contains(42))

In the above code, we’ve used the closed range operator ... to define a range from 0 to 99, including 99. The range contains the number 42, for example.

Swift has a few range operators:

  1. Closed ranges with a...b
  2. Half-open ranges with a..<b
  3. One-sided ranges with a..., ...b and ..<b

Closed Ranges

The closed range operator a...b is the simplest. It contains every item between the lower bound, often called a, and the upper bound, often called b, including b. It’s what we mean when we say: “0 to 99, including 99.”

Here’s an example:

let numbers = 0...10

for number in numbers {
print(number)
}

The above code uses the ... operator to print every number from 0 to 10 in a for loop. The type of numbers in the above example is ClosedRange<Int>. This is a generic; we’ve created a value of type ClosedRange that uses integer numbers.

Behind the scenes, the above for loop uses a different type called CountableClosedRange. This is an alias for ClosedRange<Bound>, which makes the range “strideable”. It’s just a fancy way of saying: you can iterate over this range. Good to know!

Half-Open Ranges

The half-open range operator a..<b is the other range operator you’ll often work with. Instead of ..., this operator does not include its upper bound in the range. So a range of 0..<99 does not include 99 in the range.

Here’s an example:

let numbers = 0..<5
print(Array(numbers))

The above code prints [0, 1, 2, 3, 4], so you’ll see that the 0..<5 range does not include the number 5. That’s because of the half-open range operator ..<.

Perhaps you’re wondering why we have 2 different operators for essentially the same thing. If you don’t want the last item of a range, why don’t you stop the range one item earlier? A range 0...99 is the same as 0..<100, right?

As it turns out, much of the work with ranges happens around the edges of things. For example, an array’s last index number is the same as its length minus one. We can either limit the range with a calculation, i.e. 0...array.count - 1, or we can use the half-open range operator: 0..<array.count. The latter is less error-prone and easier to read, which often saves us from off-by-one errors.

You can find a few examples of one-sided ranges in the next section about arrays.

Learn how to build iOS apps

Get started with iOS 13 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 13 apps with Swift 5 and Xcode 11.

Working with Ranges and Arrays

Let’s put those ranges to use! The first use case we’ll discuss is the most common one: ranges and arrays. In practical app development, you often use ranges in conjunction with arrays.

let names = ["Bernard", "Dolores", "Maeve", "Lee"]

for i in 0..<names.count {
print(names[i])
}

In the above example, we’re iterating over the names array of strings by using a for loop. Instead of iterating the array directly, we’re creating a range and use that to access individual array items by their key.

The range we’re using is 0..<names.count, so from 0 to 4, not including; 0, 1, 2, 3. The integers in the range correspond to the indices present in the array.

Inside the for loop, we’re using the array index – the integer from within the range – to access individual array items using subscript syntax. At a point in the loop, i equals 2, so we print out "Maeve", for example.

For the sake of this example, we’re not using the easier for name in names { ··· syntax here, of course. You’ll find that, in practical iOS development, iterating collections and ranges using indices is quite common. Take for example binary search or insertion sort, which directly works with array indices and the ranges that encapsulate them.

Array Slicing with Ranges

That’s not all ranges are good for. We can use range operators to extract sections of an array, called a slice. Check this out:

let names = ["Ford", "Arthur", "Zaphod", "Marvin", "Eddie"]
let slice = names[0...2]
print(slice)

In the above code, we’ve defined an array of strings called names. With the closed range operator ... we’re “selecting” a subsection of that array, namely the items 0, 1 and 2. The resulting slice is printed out: ["Ford", "Arthur", "Zaphod"]. Neat!

It’s worth noting here that the type of the slice constant is ArraySlice<String>. It’s not an array, nor is it a copy of the names array. Instead, a slice is an ephemeral “view” into the source names array.

You could see a slice as an index-only copy of the array, which references the old names array. Swift automatically transforms the slice to an actual Array type if needed, or you can use the Array(···) initializer yourself.

One-Sided Ranges

Let’s take that one step further with the third type of range: a one-sided range. They work the same as the closed and half-open ranges, except that they either don’t have a lower bound, or don’t have an upper bound.

This results in the following one-sided range operators:

  • a... includes everything from a to the end of the range, including a itself
  • ...b includes everything from the beginning of the range to b
  • ..<b includes everything from the beginning of the range to b, not including b

See how that works? You only define one limit — and everything up to or from that limit. Here’s an example:

let names = ["Ford", "Arthur", "Zaphod", "Marvin", "Eddie"]
print(names[3...]) // ["Marvin", "Eddie"]
print(names[...2]) // ["Ford", "Arthur", "Zaphod"]
print(names[..<2]) // ["Ford", "Arthur"]

Keep in mind that slices and one-sided ranges are just constructs. They define a view into a dataset that does not include a hardcopy of that dataset. For example, if you were to a define a freeform range like 1..., it doesn’t include every hard-coded integer number from 1 to infinity. Instead, you apply it onto your dataset, and it gives you every item from 1 to the end of your dataset.

Using Strings with Ranges

Strings in Swift are funky. In many programming languages, you can deal with strings as if they are arrays of characters. That doesn’t work in Swift. Check this out:

let name = "Reinder"
print(name[2])

You’d expect the above code to output "i", which is the 3rd character in the string. It doesn’t work that way, for a few reasons, the most important being that strings in Swift are encoded as UTF-8 and that’s a variable width encoding.

In short, a character in Swift doesn’t have a fixed width so you can’t calculate the index of an arbitrary character in a string. When you say “Get me the 7th character!” you might end up between characters 5 and 9, depending on the length of the individual characters in the string.

Swift solves this problem by providing APIs for strings that work with indices. You can “walk” or “stride” these indices, increasing them in steps. Swift figures out whether a next index involves stepping over one or multiple bytes. Once you’ve created indices, you can use regular range operators.

Take a look at this example:

let text = "The lazy dog jumped over the quick brown fox"

let start = text.index(text.startIndex, offsetBy: 4)
let end = text.index(text.startIndex, offsetBy: 8)

print(text[start..<end])

In the above code, we’ve defined a string called text. We’re using the index(_:offsetBy:) function to calculate indices of the string. This works by setting as start index, and counting n number of steps from that point. We then use the half-open range operator ..< to select the characters “lazy” from the string. Neat!

Try to avoid mixing NSString and NSRange (Objective-C), and String and Range (Swift). If you end up needing them both, make sure to explicitly convert between the two.

Pro Tips and Tricks

Let’s discuss a few things to keep in mind when working with ranges. First, it’s important to be mindful of out-of-bounds errors that might occur. Then, we’ll take a look at the ~= pattern matching operator.

Off-by-One Errors

Check this out:

let names = ["Ford", "Arthur", "Zaphod", "Marvin", "Eddie"]

for i in 0...names.count {
    print(names[i])
}

Looks like OK Swift code, right? It contains a sneaky error though, called off-by-one (OBOE). When you run it, you get this output:

Fatal error: Index out of range

See that 0...names.count code? This will iterate from 0 to the end of the array, except… the index of the last item of the array is equal to names.count - 1. The above range will loop from 0 to 5, including 5, and 5 is not a valid index of the array. The last index is 4, of course!

It’s easy to make off-by-one errors when working with ranges. You can deal with the errors as they occur, or be mindful of the edges of your ranges and collections as you work with them.

Pattern Matching Operator with Ranges

Here’s one last cool thing you can do with ranges:

let statusCode = 403

if 400..<500 ~= statusCode {
    print("Oops!")
}

In the above code, we’ve defined a statusCode constant with a value 403. Then, we’re checking if this value is contained within the range 400..<500 by using the ~= pattern matching operator in the conditional. It’s essentially the same as (400..<500).contains(statusCode), but it’s more concise.

You can use the pattern matching operator in switch statements too, to select a range of values instead of a single value. Neat!

Learn how to build iOS apps

Get started with iOS 13 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 13 apps with Swift 5 and Xcode 11.

Further Reading

We’ve looked at how you can use ranges in Swift in this tutorial. Here’s what we discussed:

  • Types of ranges and range operators in Swift, like ..<
  • Working with ranges and arrays, like creating a slice
  • Working with ranges and strings, like creating indices
  • Why you should watch out for off-by-one errors
  • Cool stuff like pattern matching with ranges and ~=

Want to learn more? Check out these resources:

Reinder de Vries

Hi, I'm Reinder.
I help developers play with code.

Get the Weekly

Get iOS/Swift tutorials and insights in your inbox, every Monday.
  • This field is for validation purposes and should be left unchanged.

Most Popular

Browse Topics

Swift Sandbox

Code Swift right in your browser!
Go to the Swift Sandbox

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.

×

Build great iOS apps
Learn how in my free 7-day course

  • This field is for validation purposes and should be left unchanged.

No spam, ever. Unsubscribe anytime. Privacy Policy