Skip to content

How to use ForEach loops in SwiftUI | SwiftUI Bootcamp #14

ForEach is how you generate a series of views from a collection of data — it's the foundation of every list, grid, and repeated UI element in SwiftUI. After this lesson you'll understand the two main ForEach patterns and know exactly when to use each one.

What You'll Learn

  • The difference between ForEach(data.indices), ForEach(0..<N), and ForEach(data, id:\.self)
  • Why ForEach is not the same as Swift's for-in loop — and why it can't be used outside a view context
  • What the id parameter does and why SwiftUI needs it to track view identity

Mental Model

Think of ForEach like a stamping machine on a factory line. You feed it a roll of data (your array), and it stamps out one view for each item that passes through. The machine needs to label each stamped view with a unique identifier so that if the conveyor belt reorders items later (e.g., a sort or filter), it knows which stamp belongs to which item. That's the id parameter — it's the label system.

Swift's regular for-in loop is for computation — it runs code imperatively. ForEach is for view generation — it's a SwiftUI view itself that produces a group of child views. You use for-in in your app logic, and ForEach inside view bodies.

Detailed Explanation

ForEach in SwiftUI is a view — not a control flow statement. It conforms to View and can appear anywhere a View is expected inside a stack, list, or grid.

There are three common forms:

  1. ForEach(0..<N) — Iterates over a fixed integer range. Useful for generating N identical or patterned views (e.g., 100 circles). SwiftUI uses the index as the identifier automatically.

  2. ForEach(data.indices) — Iterates using integer indices of an array. Gives access to both the index and the element. Useful when you need the index for display or calculations ("\(data[index]): \(index)").

  3. ForEach(data, id: \.self) — Iterates over the array directly, using the element's own value as the identifier. Works well for arrays of String, Int, or any Hashable type. For custom model objects, conform them to Identifiable and use ForEach(data) without an explicit id.

The id parameter tells SwiftUI how to uniquely identify each generated view. SwiftUI uses this for efficient diffs — when data changes, it needs to know which view corresponds to which item to apply insertions, deletions, and reorders with the right animations. If the id is unstable (e.g., an array index on a sorted list), animations can look wrong.

Code Structure

The sample is in ForEachBootcamp.swift and demonstrates both ForEach(data.indices) (for a string array with index display) and ForEach(0..<100) (for generating 100 circles). Both loops run inside a VStack, though in a real app the 100-circle loop would be inside a ScrollView or LazyVStack to avoid rendering all views at once.

Complete Code

ForEachBootcamp.swift

swift
import SwiftUI

struct ForEachBootcamp: View {
    
    let data: [String] = ["Hi", "Hello", "Hey everyone"] // the data source for the first loop
    let myString: String = "Hello"
    
    var body: some View {
        VStack {
            ForEach(data.indices) { index in // iterates using integer indices (0, 1, 2)
                Text("\(data[index]): \(index)") // shows both the value and its position in the array
            }
            ForEach(0..<100) { index in // generates 100 views from a plain integer range
                Circle()
                    .frame(height: 50) // each circle is 50pt tall and full-width
            }
        }
    }
}

struct ForEachBootcamp_Previews: PreviewProvider {
    static var previews: some View {
        ForEachBootcamp()
    }
}

Code Walkthrough

  1. let data: [String] — A simple string array as the data source. In a real app, this would be a @State or @ObservedObject property so the view updates when data changes.
  2. ForEach(data.indices) { index indata.indices is an integer Range from 0 to data.count - 1. This gives you the integer index on every iteration, useful for displaying position numbers or for accessing related arrays at the same index.
  3. Text("\(data[index]): \(index)") — Uses the index both to access the string value (data[index]) and to display the numeric position. This output would be: "Hi: 0", "Hello: 1", "Hey everyone: 2".
  4. ForEach(0..<100) { index in — The 0..<100 range literal creates an integer Range. SwiftUI uses the index as the implicit id. This generates exactly 100 child views. For ranges larger than ~20 items, wrap the containing stack in a LazyVStack to avoid materializing all views at startup.
  5. Circle().frame(height: 50) — Each iteration produces a circle with 50pt height that expands to the full available width. The index closure parameter is available but unused here — useful if you needed varying sizes or colors per index.

Common Mistakes

Mistake: Using ForEach with a non-Hashable type and forgetting the id parameterForEach(data, id: \.self) requires each element to be Hashable. For custom structs, either conform to Hashable, conform to Identifiable (and provide an id property), or use ForEach(data.indices) as a workaround. Using a non-unique or non-stable id (like an index on a sortable list) causes SwiftUI to apply animations incorrectly.

Mistake: Using a regular Swift for-in loop inside a view body

swift
// This does NOT work:
for item in data {
    Text(item) 
}

Swift's for-in is a statement that produces no value — it can't generate views. ForEach is a view that returns a group of views. Always use ForEach inside view bodies when you need to generate multiple views from a collection.

Mistake: Putting ForEach(0..<1000) directly inside a VStack without a lazy container A regular VStack renders all its children immediately, even if they're offscreen. ForEach(0..<1000) inside a VStack creates 1000 views at launch, hurting performance. Replace the VStack with a LazyVStack inside a ScrollView — lazy containers only create views when they're about to appear on screen.

Key Takeaways

  • ForEach is a SwiftUI view that generates child views from a collection — it's not a Swift for-in statement and must live inside a view builder context
  • Always provide a stable, unique id for each item so SwiftUI can correctly animate insertions, deletions, and reorders
  • For large datasets (>20 items), use LazyVStack or LazyHStack inside a ScrollView to defer view creation until items are near the visible viewport

Last updated: June 27, 2026

Released under the MIT License.