Swift Loops: for-in, while, repeat-while, and What Senior Engineers Actually Use
Loops are everywhere in your app. Writing them wrong costs performance. Writing them right is an art.
Why Loops Matter More Than You Think
Loops touch nearly every layer of an iOS app rendering lists, processing data, animating frames, parsing responses. A poorly written loop in a hot path can tank your frame rate or freeze the main thread. A well-written one is invisible.
Swift gives you several looping tools. Knowing which one to reach for and why separates junior code from production-grade code.
for-in: Your Most-Used Loop
let fruits = ["apple", "banana", "cherry"]
for fruit in fruits {
print(fruit)
}
// apple
// banana
// cherryWorks on any Sequence arrays, sets, dictionaries, ranges, strings.
// Range loop
for i in 1...5 {
print(i) // 1 2 3 4 5
}
// String characters
for char in "Swift" {
print(char) // S w i f t
}
// Dictionary - order not guaranteed
let scores = ["Alice": 95, "Bob": 87]
for (name, score) in scores {
print("\(name): \(score)")
}When you don’t need the index
// ❌ Unnecessary index
for i in 0..<fruits.count {
print(fruits[i])
}// ✅ Direct iteration
for fruit in fruits {
print(fruit)
}Only use index-based loops when you actually need the index.
enumerated(): Index + Value Together
let players = ["Alice", "Bob", "Charlie"]
for (index, player) in players.enumerated() {
print("\(index + 1). \(player)")
}
// 1. Alice
// 2. Bob
// 3. CharlieProduction use: Perfect for numbered lists, position-dependent logic, and debugging output. Cleaner than managing a counter variable manually.
zip(): Loop Two Collections Together
let names = ["Alice", "Bob", "Charlie"]
let scores = [95, 87, 92]
for (name, score) in zip(names, scores) {
print("\(name) scored \(score)")
}
// Alice scored 95
// Bob scored 87
// Charlie scored 92zip stops at the shorter collection no index out of bounds risk. Use it whenever you're pairing two arrays.
stride: Precise Step Control
When you need a loop that doesn’t increment by 1:
// Count up by 2
for i in stride(from: 0, to: 10, by: 2) {
print(i) // 0 2 4 6 8
}
// Count down
for i in stride(from: 10, through: 0, by: -2) {
print(i) // 10 8 6 4 2 0
}stride(from:to:by:)excludes the end valuestride(from:through:by:)includes the end value
Real-world use: Animation frame stepping, audio sample processing, pagination with custom step sizes.
while: Loop Until a Condition Fails
Use while when you don't know how many iterations you need upfront.
var attempts = 0
var success = false
while !success && attempts < 3 {
success = tryNetworkRequest()
attempts += 1
}The condition is checked before each iteration. If it’s false from the start, the body never runs.
repeat-while: Always Runs At Least Once
var pin = ""
repeat {
pin = promptUserForPIN()
} while pin.count != 4The body runs first, then the condition is checked. Use this when the action must happen at least once before you can evaluate the condition like prompting for input.
break and continue: Controlling Flow
// break — exit the loop entirely
for i in 1...10 {
if i == 5 { break }
print(i) // 1 2 3 4
}// continue - skip this iteration, keep going
for i in 1...10 {
if i % 2 == 0 { continue }
print(i) // 1 3 5 7 9
}Labeled statements for nested loops
outer: for row in 0..<3 {
for col in 0..<3 {
if row == 1 && col == 1 {
break outer // exits BOTH loops
}
print("(\(row), \(col))")
}
}Without the label, break only exits the inner loop. Labels give you precise control over nested loop exits use them when the intent is clear.
Performance: What Actually Matters
Avoid repeated property access in loop conditions
let items = largeArray
// ❌ .count is evaluated every iteration
for i in 0..<items.count { ... }
// ✅ Captured once - compiler usually optimizes this, but being explicit is better
let count = items.count
for i in 0..<count { ... }Prefer forEach for functional style but know the tradeoff
fruits.forEach { fruit in
print(fruit)
}forEach is clean for side effects but has one critical difference from for-in: you cannot use break or continue inside forEach. It's a closure return just exits the closure, not the loop.
// ❌ This doesn't break the loop — it just returns from the closure
fruits.forEach { fruit in
if fruit == "banana" { return } // skips "banana", continues to "cherry"
print(fruit)
}// ✅ Use for-in if you need break/continue
for fruit in fruits {
if fruit == "banana" { break } // actually stops the loop
print(fruit)
}This catches a lot of developers off guard. Know the difference.
Real-World Pattern: Pagination
func fetchAllPages(startPage: Int = 1, maxPages: Int = 10) async throws -> [Item] {
var results: [Item] = []
var currentPage = startPage
while currentPage <= maxPages {
let page = try await api.fetch(page: currentPage)
results.append(contentsOf: page.items)
guard page.hasNextPage else { break }
currentPage += 1
}
return results
}while with a break on condition clean, readable, safe. This is production pagination logic.
Interview Question
Q: What’s the difference between break inside forEach and break inside for-in?
In a for-in loop, break exits the loop immediately. In forEach, there is no break forEach takes a closure, and return inside it only exits the current closure call (equivalent to continue). You cannot exit a forEach loop early. If you need early exit, use for-in with break, or use first(where:) / contains(where:) depending on the goal.
Summary
- Use
for-infor most loops it's clean, safe, and works on anySequence. - Use
enumerated()when you need both index and value. - Use
zip()to loop two collections in parallel no index out of bounds risk. - Use
stridewhen step size isn't 1. - Use
whilewhen iteration count is unknown upfront. - Use
repeat-whilewhen the body must execute at least once. forEachdoesn't supportbreakorcontinueusefor-inwhen you need flow control.- Labeled statements give precise control over nested loop exits.
Practice
- Beginner: Write a loop that prints the multiplication table for any number from 1 to 10 using
stride. - Intermediate: Given two arrays
let userIDs = [1, 2, 3, 4, 5]andlet userNames = ["Alice", "Bob", "Charlie", "Diana", "Eve"]usezipandenumeratedto print a numbered list of"1. Alice (ID: 1)". - Advanced: Write a function
chunked<T>(_ array: [T], size: Int) -> [[T]]that splits any array into chunks of a given size using awhileloop. Handle edge cases: empty array, chunk size larger than array, chunk size of 1.
What’s Next
Article 6: Swift Functions Parameters, Return Types, Overloading, and First-Class Functions
Loops move through data. Functions transform it. Next article covers everything about Swift functions default parameters, labeled arguments, variadic parameters, @discardableResult, inout parameters, and why functions are first-class citizens in Swift.
Part of Swift from Zero to Senior — a complete iOS engineering curriculum on Medium.
If the forEach vs for-in difference saved you a future bug, tap 👏 up to 50 times, It helps this series reach developers who need it.
Follow me so Article 6 lands in your feed automatically. Each article in this series builds directly on the last.
See you in the next one. 🚀
