Swift Loops in 2025: The Hidden Performance Tricks That Will Make Your Code 10x Faster
Complete guide to Swift’s loop arsenal: when to use each type, common pitfalls, and performance optimizations that matter

In my initial programming journey, I thought loops were just… loops. You know, those basic for and while things you learn in CS 101? Boy, was I wrong.
It wasn’t until I was debugging a performance nightmare in a production app — one that was taking 3+ seconds to render a simple list — that I realized Swift’s loop system is way more nuanced than most tutorials let on. That innocent-looking for-in loop I'd been copy-pasting everywhere? Yeah, it was the bottleneck.
So here’s the thing: Swift gives us three main loop types, but knowing when to use each one can literally make or break your app’s performance. We’re talking about the difference between buttery smooth 60fps scrolling and users rage-deleting your app.
📺 Coming soon to Swift Pal: A comprehensive video walkthrough of these loop optimization techniques at _https://youtube.com/@swift-pal_
**Swift Pal** _👨💻 Welcome to Swift Pal - Your iOS Dev Companion! Whether you're just starting your journey with Swift and SwiftUI…_youtube.comhttps://youtube.com/@swift-pal
🔄 The for-in Loop: Your Everyday Workhorse (But With Hidden Gotchas)
Let’s start with everyone’s favorite — the for-in loop. It's clean, readable, and feels natural. But here's where most developers mess up...
Basic for-in Loop Syntax
let numbers = [1, 2, 3, 4, 5]
for number in numbers {
print("Number: \(number)")
}Simple enough, right? But wait — there’s actually three different ways to write for-in loops, and choosing the wrong one can tank your performance.
Range-Based Loops: The Unsung Hero
Most developers default to iterating over arrays, but range-based loops are often faster:
// Instead of this (creating an array):
for i in [0, 1, 2, 3, 4] {
print("Index: \(i)")
}
// Do this (using ranges):
for i in 0...4 {
print("Index: \(i)")
}
// Or for half-open ranges:
for i in 0..<5 {
print("Index: \(i)")
}The performance difference? Ranges don’t create intermediate collections in memory. For small loops, it’s negligible. For large iterations (think 10,000+ items), it’s the difference between smooth performance and your app stuttering.
Iterating with Indices (When You Need Both Index and Value)
Here’s where I see developers make the biggest mistake. Don’t do this:
let items = ["Apple", "Banana", "Cherry"]
var index = 0
for item in items {
print("Item \(index): \(item)")
index += 1 // This is... not great
}Instead, use enumerated():
let items = ["Apple", "Banana", "Cherry"]
for (index, item) in items.enumerated() {
print("Item \(index): \(item)")
}Much cleaner, and Swift can optimize this pattern better than manual index tracking.
The Stride Function: For When You Need Custom Steps
Sometimes you need to skip elements or go backwards. Enter stride:
// Counting by 2s
for i in stride(from: 0, to: 10, by: 2) {
print(i) // Prints: 0, 2, 4, 6, 8
}
// Going backwards
for i in stride(from: 10, through: 0, by: -2) {
print(i) // Prints: 10, 8, 6, 4, 2, 0
}
// Working with array indices (backwards)
let data = ["first", "second", "third", "fourth"]
for i in stride(from: data.count - 1, through: 0, by: -1) {
print("Item \(i): \(data[i])")
}Notice the difference between to (exclusive) and through (inclusive). I still mix these up sometimes... 😅
⚡ while Loops: The Conditional Powerhouse
Now, here’s where things get interesting. While loops aren’t just “for when you don’t know how many iterations you need.” They’re actually the most performance-friendly option in specific scenarios.
Basic while Loop
var countdown = 5
while countdown > 0 {
print("T-minus \(countdown)")
countdown -= 1
}
print("Blast off! 🚀")Real-World Example: Processing Data Until a Condition
Here’s a practical scenario I hit recently. I needed to process network responses until I got a specific result:
import Foundation
class DataProcessor {
private var attempts = 0
private let maxAttempts = 5
func processUntilSuccess() -> Bool {
var success = false
while !success && attempts < maxAttempts {
attempts += 1
print("Attempt \(attempts)...")
// Simulate some processing
success = Bool.random() // 50% chance of success
if !success {
print("Failed, retrying...")
Thread.sleep(forTimeInterval: 0.1) // Brief delay
}
}
return success
}
}
// Usage
let processor = DataProcessor()
let result = processor.processUntilSuccess()
print("Final result: \(result ? "Success!" : "Failed after max attempts")")Performance Tip: while vs for-in for Unknown Iterations
When you don’t know how many iterations you’ll need, while loops often outperform for-in with break conditions:
// Less efficient (creates sequence, then breaks)
for i in 0...Int.max {
if someCondition(i) { break }
process(i)
}
// More efficient
var i = 0
while !someCondition(i) {
process(i)
i += 1
}🔁 repeat-while: The Underrated Loop That Guarantees Execution
Okay, real talk — repeat-while is probably the most underused loop in Swift. But it's perfect for specific scenarios, especially validation and user input.
Basic repeat-while Syntax
var userInput: String
var isValid = false
repeat {
print("Enter a number between 1 and 10:")
userInput = readLine() ?? ""
if let number = Int(userInput), 1...10 ~= number {
isValid = true
print("Valid input: \(number)")
} else {
print("Invalid input. Please try again.")
}
} while !isValidThe key difference? The loop body executes at least once, regardless of the condition.
Practical Example: Game Loop
Here’s a simple game loop structure that guarantees at least one round:
import Foundation
class SimpleGame {
private var playerHealth = 100
private var round = 1
func startGame() {
print("🎮 Game Started!")
repeat {
print("\n--- Round \(round) ---")
print("Health: \(playerHealth)")
// Simulate game round
let damage = Int.random(in: 10...30)
let heal = Int.random(in: 5...15)
playerHealth -= damage
print("💥 Took \(damage) damage!")
if playerHealth > 0 {
playerHealth += heal
print("💚 Healed \(heal) points!")
playerHealth = min(playerHealth, 100) // Cap at 100
}
round += 1
// Add some drama
if playerHealth <= 20 {
print("⚠️ Critical health!")
}
} while playerHealth > 0
print("\n💀 Game Over after \(round - 1) rounds!")
}
}
// Usage
let game = SimpleGame()
game.startGame()🎯 Loop Control: break, continue, and Labeled Statements
Sometimes you need more control over your loops. Swift gives us some powerful tools here, including one that most developers don’t even know exists.
Basic break and continue
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print("Even numbers only:")
for number in numbers {
if number % 2 != 0 {
continue // Skip odd numbers
}
print(number)s
}
print("\nNumbers until we hit 7:")
for number in numbers {
if number == 7 {
break // Stop when we reach 7
}
print(number)
}Labeled Statements: The Secret Weapon
Here’s something most Swift developers don’t know about — labeled statements. They’re incredibly useful for nested loops:
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
outerLoop: for (rowIndex, row) in matrix.enumerated() {
for (colIndex, value) in row.enumerated() {
if value == 5 {
print("Found 5 at position (\(rowIndex), \(colIndex))")
break outerLoop // Breaks out of BOTH loops
}
}
}Without the label, break would only exit the inner loop. With it, we can break out of the outer loop directly. Game changer for search algorithms!
Practical Example: Finding Data in Nested Structures
struct User {
let name: String
let posts: [Post]
}
struct Post {
let title: String
let likes: Int
}
let users = [
User(name: "Alice", posts: [
Post(title: "Swift Tips", likes: 45),
Post(title: "iOS Design", likes: 67)
]),
User(name: "Bob", posts: [
Post(title: "Performance", likes: 123),
Post(title: "Architecture", likes: 89)
])
]
// Find the first post with over 100 likes
searchLoop: for user in users {
for post in user.posts {
if post.likes > 100 {
print("Found viral post: '\(post.title)' by \(user.name) with \(post.likes) likes")
break searchLoop
}
}
}Without the label, break would only exit the inner loop. With it, we can break out of the outer loop directly. Game changer for search algorithms!
🚨 Common Loop Mistakes That Kill Performance
Let’s talk about the mistakes I see constantly in code reviews. These aren’t just style issues — they’re performance killers.
Mistake #1: Modifying Collections While Iterating
// ❌ Don't do this - it's undefined behavior
var numbers = [1, 2, 3, 4, 5]
for (index, number) in numbers.enumerated() {
if number % 2 == 0 {
numbers.remove(at: index) // 💥 Crash waiting to happen
}
}
// ✅ Do this instead
var numbers = [1, 2, 3, 4, 5]
numbers = numbers.filter { $0 % 2 != 0 }
// Or if you need indices:
var numbers = [1, 2, 3, 4, 5]
for i in stride(from: numbers.count - 1, through: 0, by: -1) {
if numbers[i] % 2 == 0 {
numbers.remove(at: i)
}
}Mistake #2: Unnecessary Array Creation in Loops
// ❌ Creates a new array every iteration
for i in 0..<1000 {
let data = Array(repeating: 0, count: 100) // Wasteful!
// process data...
}
// ✅ Create once, reuse
var reusableData = Array(repeating: 0, count: 100)
for i in 0..<1000 {
// Reset data if needed
for j in reusableData.indices {
reusableData[j] = 0
}
// process reusableData...
}Mistake #3: Force Unwrapping in Loops
// ❌ Risky - will crash on nil
let optionalNumbers: [Int?] = [1, 2, nil, 4, 5]
for number in optionalNumbers {
print(number!) // 💥 Crash on the third iteration
}
// ✅ Safe unwrapping
for number in optionalNumbers {
if let unwrapped = number {
print(unwrapped)
}
}
// ✅ Or use compactMap for filtering
for number in optionalNumbers.compactMap({ $0 }) {
print(number) // Only prints non-nil values
}🔧 Advanced Loop Patterns for Real-World Apps
Now that we’ve covered the basics, let’s dive into some advanced patterns I use regularly in production apps.
Pattern #1: Batch Processing with Progress Tracking
import Foundation
class BatchProcessor {
func processBatches<T>(data: [T], batchSize: Int = 100,
processor: (ArraySlice<T>) -> Void) {
let totalBatches = (data.count + batchSize - 1) / batchSize
var currentBatch = 1
for startIndex in stride(from: 0, to: data.count, by: batchSize) {
let endIndex = min(startIndex + batchSize, data.count)
let batch = data[startIndex..<endIndex]
print("Processing batch \(currentBatch)/\(totalBatches) (\(batch.count) items)")
processor(batch)
currentBatch += 1
// Add small delay to prevent UI blocking
if currentBatch % 10 == 0 {
Thread.sleep(forTimeInterval: 0.001)
}
}
}
}
// Usage
let processor = BatchProcessor()
let largeDataSet = Array(1...10000)
processor.processBatches(data: largeDataSet, batchSize: 500) { batch in
// Process each batch
let sum = batch.reduce(0, +)
print("Batch sum: \(sum)")
}Pattern #2: Retry Logic with Exponential Backoff
import Foundation
func performWithRetry<T>(maxAttempts: Int = 3,
operation: () throws -> T) -> T? {
var attempt = 1
while attempt <= maxAttempts {
do {
return try operation()
} catch {
print("Attempt \(attempt) failed: \(error)")
if attempt == maxAttempts {
print("All attempts failed")
return nil
}
// Exponential backoff: 1s, 2s, 4s, etc.
let delay = pow(2.0, Double(attempt - 1))
print("Waiting \(delay) seconds before retry...")
Thread.sleep(forTimeInterval: delay)
attempt += 1
}
}
return nil
}
// Example usage
let result = performWithRetry(maxAttempts: 3) {
// Simulate a network call that might fail
if Bool.random() {
return "Success!"
} else {
throw NSError(domain: "NetworkError", code: 1, userInfo: nil)
}
}
print("Final result: \(result ?? "Failed")")Pattern #3: Infinite Loop with Break Conditions
import Foundation
class StreamProcessor {
private var isRunning = false
func startProcessing() {
isRunning = true
var messageCount = 0
// Infinite loop with multiple exit conditions
while true {
// Check if we should stop
if !isRunning {
print("Graceful shutdown requested")
break
}
// Check for max messages processed
if messageCount >= 1000 {
print("Processed maximum messages, shutting down")
break
}
// Simulate processing a message
if let message = getNextMessage() {
process(message)
messageCount += 1
} else {
// No messages available, brief pause
Thread.sleep(forTimeInterval: 0.01)
}
// Periodic status update
if messageCount % 100 == 0 && messageCount > 0 {
print("Processed \(messageCount) messages so far...")
}
}
print("Stream processor stopped. Total messages: \(messageCount)")
}
func stop() {
isRunning = false
}
private func getNextMessage() -> String? {
// Simulate getting messages from a queue
return Bool.random() ? "Message \(Int.random(in: 1...1000))" : nil
}
private func process(_ message: String) {
// Simulate message processing
print("Processing: \(message)")
}
}
// Usage
let streamProcessor = StreamProcessor()
// Start processing in background
DispatchQueue.global().async {
streamProcessor.startProcessing()
}
// Simulate running for a bit, then stopping
Thread.sleep(forTimeInterval: 2.0)
streamProcessor.stop()🎯 When to Use Which Loop Type: The Decision Matrix
After years of Swift development, here’s my mental decision tree for choosing loop types:
Use for-in when:
- ✅ Iterating over all elements in a collection
- ✅ You need clean, readable code
- ✅ Performance isn’t the absolute top priority
- ✅ Working with ranges or sequences
Use while when:
- ✅ You don’t know how many iterations you’ll need
- ✅ The loop condition is complex
- ✅ Maximum performance is critical
- ✅ Implementing algorithms with varying iteration counts
Use repeat-while when:
- ✅ You need to execute the loop body at least once
- ✅ Validating user input
- ✅ Implementing game loops or state machines
- ✅ Retry mechanisms where you always try once
🚀 Performance Optimization: The Pro Tips
Here are the optimization techniques that actually move the needle in real apps:
Tip #1: Minimize Function Calls in Loops
// ❌ Calls count property every iteration
for i in 0..<array.count {
process(array[i])
}
// ✅ Cache the count
let arrayCount = array.count
for i in 0..<arrayCount {
process(array[i])
}
// ✅ Even better - use for-in when possible
for element in array {
process(element)
}Tip #2: Use inout Parameters for Large Data Modifications
// ❌ Creates new arrays
func processNumbers(_ numbers: [Int]) -> [Int] {
var result = numbers
for i in result.indices {
result[i] *= 2
}
return result
}
// ✅ Modifies in place
func processNumbers(_ numbers: inout [Int]) {
for i in numbers.indices {
numbers[i] *= 2
}
}Tip #3: Consider Parallel Processing for Large Datasets
import Foundation
// For CPU-intensive operations on large datasets
func parallelProcess<T>(_ array: [T],
operation: @escaping (T) -> T) -> [T] {
let queue = DispatchQueue.global(qos: .userInitiated)
let group = DispatchGroup()
let chunkSize = max(1, array.count / ProcessInfo.processInfo.processorCount)
var results = Array<T?>(repeating: nil, count: array.count)
for startIndex in stride(from: 0, to: array.count, by: chunkSize) {
let endIndex = min(startIndex + chunkSize, array.count)
group.enter()
queue.async {
for i in startIndex..<endIndex {
results[i] = operation(array[i])
}
group.leave()
}
}
group.wait()
return results.compactMap { $0 }
}
// Usage for computationally expensive operations
let numbers = Array(1...100000)
let squared = parallelProcess(numbers) { $0 * $0 }🎬 Wrapping Up: The Loop Mastery Checklist
Alright, let’s wrap this up with a practical checklist you can reference when writing loops:
Before Writing Any Loop, Ask:
- Do I know the iteration count? → Consider
for-inor range-based loops - Is the condition complex? → Maybe
whileis cleaner - Do I need at least one execution? →
repeat-whilemight be perfect - Am I modifying the collection? → Be extra careful or use alternative approaches
- Is performance critical? → Profile and optimize accordingly
Red Flags to Watch For:
- ❌ Force unwrapping optionals in loops
- ❌ Creating objects unnecessarily inside loops
- ❌ Modifying collections while iterating
- ❌ Nested loops without considering algorithmic complexity
- ❌ Missing break conditions in while/repeat-while loops
Performance Optimization Priorities:
- Algorithmic complexity (O(n) vs O(n²)) — This matters most
- Memory allocations — Reuse objects when possible
- Function call overhead — Cache expensive computations
- Loop choice — Use the right tool for the job
The truth is, most loop performance issues aren’t about choosing for-in vs while. They're about algorithm design and avoiding unnecessary work inside the loop body. But when you do need that extra performance edge, knowing these patterns can make all the difference.
Remember: write clear code first, optimize when you actually need to. Your future self (and your teammates) will thank you for readable loops over prematurely optimized ones.
Now go forth and loop efficiently! 🚀
New articles, straight to your inbox.
No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.
Keep reading
Why I'm Rebuilding My Blog From Scratch (and Leaving Medium)
After years of publishing on someone else's platform, I'm moving my writing to a home I actually own. Here's the reasoning, and what I'm building instead.
ReadData Is the Model: The Most Ignored Part of AI
A beginner-friendly guide to why data quality beats model hype.
Read