Swift Range Operators (... and ..<): Complete Guide
Swift range operators let you describe a continuous span of values with compact, readable syntax. In this tutorial, you will learn how ... and ..< work in Swift 5+, how to use them with loops, arrays, slices, pattern matching, and one-sided ranges, and how to avoid the most common off-by-one errors.
1. What Are Swift Range Operators?
Swift range operators create values that represent a sequence or interval between two bounds. They are commonly used when looping over numbers, selecting part of a collection, checking whether a value falls inside a range, or matching values in a switch statement.
Swift has two main range operator forms:
- a...b creates a closed range that includes both a and b.
- a..<b creates a half-open range that includes a but excludes b.
- a..., ...b, and ..<b create one-sided ranges, often used with collection slicing or pattern matching.
- Ranges are not just syntax; they are real Swift standard library types such as ClosedRange<Int>, Range<Int>, and PartialRangeFrom<Int>.
2. Why Swift Range Operators Matter
Range operators matter because many programming tasks require boundaries. You might need to loop from 1 through 5, take the first 3 items from an array, validate whether an age is in an allowed interval, or classify a score as a grade.
Use range operators when your code naturally talks about a span of values. They make boundary rules explicit: 1...5 clearly means “1 through 5,” while 0..<items.count clearly means “valid integer indexes before count.”
Do not reach for ranges when the values are not ordered, when the bounds are unclear, or when you need custom stepping such as every third number. For custom stepping, Swift provides stride(from:to:by:) and stride(from:through:by:), which are often safer than trying to force a range into doing something it was not designed to do.
3. Basic Syntax or Core Idea
The two most important forms are the closed range and the half-open range. The difference is whether the upper bound is included.
Closed range with ...
A closed range includes both its lower and upper bound. The following loop prints every integer from 1 through 5.
for number in 1...5 {
print(number)
}
This prints 1, 2, 3, 4, and 5. Use this form when the ending value is part of the desired result.
Half-open range with ..<
A half-open range includes the lower bound but stops before the upper bound. This is especially useful with array indexes because valid indexes start at 0 and end before count.
let names = ["Ava", "Ben", "Chloe"]
for index in 0..<names.count {
print("\(index): \(names[index])")
}
This visits indexes 0, 1, and 2, but not 3. Since names[3] would be out of bounds, the half-open range matches array indexing perfectly.
One-sided ranges
Swift also lets you omit one side of a range. These are called one-sided ranges and are most often used with collections or comparisons.
let scores = [72, 85, 91, 64, 100]
let fromThirdScore = scores[2...]
let upToThirdScore = scores[..<2]
let throughThirdScore = scores[...2]
2... means “from index 2 through the end,” ..<2 means “before index 2,” and ...2 means “through index 2.”
4. Step-by-Step Examples
Example 1: Looping through an inclusive numeric range
Use a closed range when the final value should be processed. This is common for countdowns, page numbers, rating scales, and numbered steps.
let maxStars = 5
for star in 1...maxStars {
print("Star \(star)")
}
The loop runs exactly five times because 1...maxStars includes 5. This reads naturally: “from 1 through the maximum number of stars.”
Example 2: Iterating over array indexes safely with ..<
When iterating by index, the upper bound is usually the collection’s count. Since count is not itself a valid index, use a half-open range.
let tasks = ["Design", "Build", "Test"]
for index in 0..<tasks.count {
print("Step \(index + 1): \(tasks[index])")
}
This prints every task without trying to access tasks[3]. In real Swift code, tasks.indices is often even better because it uses the collection’s actual index type.
Example 3: Slicing an array with closed and half-open ranges
Ranges are often used to create slices. In Swift, slicing an array returns an ArraySlice, not a new Array.
let temperatures = [18, 20, 22, 21, 19, 17]
let firstThree = temperatures[0..<3]
let middleThree = temperatures[1...3]
print(firstThree)
print(middleThree)
0..<3 selects indexes 0, 1, and 2. 1...3 selects indexes 1, 2, and 3. The operator you choose communicates whether the ending index is included.
Example 4: Classifying values with ranges in a switch
Ranges work well in switch cases because Swift can test whether the value falls inside a range.
let score = 87
switch score {
case 90...100:
print("A")
case 80..<90:
print("B")
case 70..<80:
print("C")
case 0..<70:
print("Needs improvement")
default:
print("Invalid score")
}
The half-open ranges prevent overlap at grade boundaries. For example, 90 belongs only to 90...100, not also to the previous range.
Example 5: Using one-sided ranges for slices
One-sided ranges are concise when one boundary is the start or end of a collection.
let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
let firstQuarter = months[..<3]
let throughApril = months[...3]
let fromApril = months[3...]
print(firstQuarter)
print(throughApril)
print(fromApril)
..<3 gives January through March, ...3 gives January through April, and 3... gives April through June.
5. Practical Use Cases
- Looping through a known inclusive sequence, such as pages 1...totalPages.
- Iterating over valid integer array indexes using 0..<array.count.
- Slicing an array into a prefix, middle segment, or suffix with ..<, ..., and index....
- Defining score, age, temperature, or price bands in switch statements.
- Checking validation rules with range.contains(value).
- Splitting work into batches, such as processing items from start..<end.
- Creating user-facing labels, such as “available from 18...” or “up to ..<65,” while keeping the comparison logic precise.
6. Common Mistakes
Mistake 1: Using a closed range with array.count
Warning: array.count is one past the last valid integer index, so 0...array.count will eventually crash.
// Bad: tries to access index array.count
let colors = ["Red", "Green", "Blue"]
for index in 0...colors.count {
print(colors[index])
}
// Good: stops before colors.count
let colors = ["Red", "Green", "Blue"]
for index in 0..<colors.count {
print(colors[index])
}
The corrected version uses ..< because valid integer indexes are less than colors.count.
Mistake 2: Creating an invalid closed range when the lower bound is greater
A closed range like 5...1 is invalid and causes a runtime error. If your bounds come from variables, validate them before creating the range.
// Bad: crashes because lowerBound is greater than upperBound
let start = 5
let end = 1
for number in start...end {
print(number)
}
// Good: choose a safe strategy for reversed bounds
let start = 5
let end = 1
if start <= end {
for number in start...end {
print(number)
}
} else {
for number in stride(from: start, through: end, by: -1) {
print(number)
}
}
The corrected version checks the order first and uses stride(from:through:by:) for a descending sequence.
Mistake 3: Assuming array slices start at index 0
An ArraySlice keeps the original array’s indexes. If you slice from index 2, the slice’s first valid index is still 2, not 0.
// Bad: the slice does not necessarily have index 0
let values = [10, 20, 30, 40]
let tail = values[2...]
print(tail[0])
// Good: use the slice's own startIndex
let values = [10, 20, 30, 40]
let tail = values[2...]
print(tail[tail.startIndex])
If you need a new zero-based array, convert the slice with Array(tail).
Mistake 4: Using integer ranges directly on String
Swift strings are indexed by String.Index, not Int, because characters can have variable-length Unicode representations.
// Bad: Swift String does not support integer subscripting
let word = "Swift"
let prefix = word[0..<2]
// Good: create String.Index bounds first
let word = "Swift"
let endIndex = word.index(word.startIndex, offsetBy: 2)
let prefix = word[word.startIndex..<endIndex]
print(prefix)
The range operator still works, but the bounds must be the correct index type for the collection.
7. Best Practices
Practice 1: Prefer indices over 0..<count for collections
0..<array.count is common for arrays, but collection.indices is more general and safer. It respects the collection’s real index type and works with slices.
// Good: uses the collection's valid indexes
let items = ["Keyboard", "Mouse", "Monitor"]
for index in items.indices {
print("\(index): \(items[index])")
}
This avoids assuming indexes always start at 0. That assumption is false for many collection slices.
Practice 2: Use half-open ranges for adjacent intervals
Half-open ranges make boundaries easy to combine without gaps or overlaps. This is useful for bins, grades, time windows, and pagination.
let speed = 55
switch speed {
case 0..<30:
print("Slow")
case 30..<60:
print("Normal")
case 60..<90:
print("Fast")
default:
print("Out of expected range")
}
Every boundary belongs to exactly one interval. For example, 30 is “Normal,” not both “Slow” and “Normal.”
Practice 3: Convert long-lived slices to arrays when needed
Array slices are efficient views into an original array, but they can keep the original storage alive. If you need to store a slice long term, convert it to an Array.
let allEvents = ["Login", "View", "Tap", "Purchase"]
let recentEventsSlice = allEvents[1...]
let recentEvents = Array(recentEventsSlice)
print(recentEvents)
This keeps the convenient range-based slicing while producing a standalone array for storage or API return values.
Practice 4: Use contains for readable range validation
When checking whether a value is inside a range, contains often reads better than writing two comparisons by hand.
let age = 21
let allowedAgeRange = 18...64
if allowedAgeRange.contains(age) {
print("Allowed")
} else {
print("Not allowed")
}
The range states the policy in one place, and contains clearly states the operation being performed.
8. Limitations and Edge Cases
- A closed range such as 5...1 is invalid because the lower bound must not be greater than the upper bound.
- A half-open range such as 5..<5 is valid but empty; it contains no values.
- Range operators do not define a custom step size. Use stride(from:to:by:) or stride(from:through:by:) for steps other than 1 or for descending sequences.
- Array slices created with ranges are ArraySlice values and preserve the original indexes.
- String ranges must use String.Index bounds, not integer bounds.
- One-sided ranges can represent unbounded logic, but when used for collection slicing they are still limited by the collection’s valid indexes.
- Floating-point ranges can be used for containment checks, but they are not suitable for simple for-in iteration the way integer ranges are.
- Ranges used in switch cases should be ordered carefully when cases overlap, because Swift uses the first matching case.
9. Practical Mini Project
In this mini project, you will build a small order batching utility. It uses half-open ranges for safe array slicing, one-sided ranges for the final batch, and a closed range for validating a requested batch size.
let orders = [
"ORD-1001", "ORD-1002", "ORD-1003",
"ORD-1004", "ORD-1005", "ORD-1006",
"ORD-1007"
]
let requestedBatchSize = 3
let allowedBatchSize = 1...5
guard allowedBatchSize.contains(requestedBatchSize) else {
fatalError("Batch size must be between 1 and 5.")
}
var start = 0
var batchNumber = 1
while start < orders.count {
let end = min(start + requestedBatchSize, orders.count)
let batch = Array(orders[start..<end])
print("Batch \(batchNumber): \(batch)")
start = end
batchNumber += 1
}
This project demonstrates several realistic range choices. 1...5 validates an inclusive business rule. start..<end safely selects each batch without including the next batch’s first index. min prevents the final range from exceeding orders.count.
10. Key Points
- a...b creates a closed range that includes both bounds.
- a..<b creates a half-open range that excludes the upper bound.
- Half-open ranges are ideal for array indexes because valid indexes are less than count.
- One-sided ranges are useful for slicing from the start, through an index, or to the end.
- Array slices preserve original indexes, so do not assume a slice starts at 0.
- Use String.Index, not Int, when applying ranges to strings.
- Use stride when you need descending movement or a custom step size.
- Use contains and switch cases to express range-based rules clearly.
11. Practice Exercise
Create a Swift program that groups customer ages into categories using range operators.
- Start with the ages [4, 12, 17, 18, 29, 64, 65, 82].
- Classify each age as Child, Teen, Adult, or Senior.
- Use ranges in a switch statement.
- Expected output: each age printed with its category, such as 18: Adult.
- Hint: use adjacent half-open ranges for categories that meet at a boundary.
let ages = [4, 12, 17, 18, 29, 64, 65, 82]
for age in ages {
let category: String
switch age {
case 0..<13:
category = "Child"
case 13..<18:
category = "Teen"
case 18..<65:
category = "Adult"
case 65...:
category = "Senior"
default:
category = "Invalid age"
}
print("\(age): \(category)")
}
This solution uses half-open ranges for adjacent age groups and a one-sided closed range for ages 65 and above.
12. Final Summary
Swift range operators are small pieces of syntax with a large impact on everyday code. The closed range operator ... includes both bounds, while the half-open range operator ..< excludes the upper bound. That single difference determines whether your loops, slices, and validation rules include the final value.
You also learned how one-sided ranges simplify collection slicing and pattern matching, why 0..<array.count is safer than 0...array.count, and why strings and slices require extra care. These details help prevent crashes, off-by-one bugs, and confusing boundary behavior.
As a next step, practice rewriting boundary-heavy code using explicit ranges. Look for loops, validation checks, and switch statements where ... or ..< can make the intended limits clearer.