All articles
iOS7 min read

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

K
Karan Pal
Author
Image Generated by AI
Image Generated by AI

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 !isValid

The 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:

Use while when:

Use repeat-while when:

🚀 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:

  1. Do I know the iteration count? → Consider for-in or range-based loops
  2. Is the condition complex? → Maybe while is cleaner
  3. Do I need at least one execution?repeat-while might be perfect
  4. Am I modifying the collection? → Be extra careful or use alternative approaches
  5. Is performance critical? → Profile and optimize accordingly

Red Flags to Watch For:

Performance Optimization Priorities:

  1. Algorithmic complexity (O(n) vs O(n²)) — This matters most
  2. Memory allocations — Reuse objects when possible
  3. Function call overhead — Cache expensive computations
  4. 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! 🚀

● The newsletter

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