Sitemap

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.

--

Press enter or click to view image in full size

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
// cherry

Works 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. Charlie

Production 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 92

zip 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 value
  • stride(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 != 4

The 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-in for most loops it's clean, safe, and works on any Sequence.
  • Use enumerated() when you need both index and value.
  • Use zip() to loop two collections in parallel no index out of bounds risk.
  • Use stride when step size isn't 1.
  • Use while when iteration count is unknown upfront.
  • Use repeat-while when the body must execute at least once.
  • forEach doesn't support break or continue use for-in when you need flow control.
  • Labeled statements give precise control over nested loop exits.

Practice

  1. Beginner: Write a loop that prints the multiplication table for any number from 1 to 10 using stride.
  2. Intermediate: Given two arrays let userIDs = [1, 2, 3, 4, 5] and let userNames = ["Alice", "Bob", "Charlie", "Diana", "Eve"] use zip and enumerated to print a numbered list of "1. Alice (ID: 1)".
  3. Advanced: Write a function chunked<T>(_ array: [T], size: Int) -> [[T]] that splits any array into chunks of a given size using a while loop. 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. 🚀

--

--