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), andForEach(data, id:\.self) - Why
ForEachis not the same as Swift'sfor-inloop — and why it can't be used outside a view context - What the
idparameter 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:
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.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)").ForEach(data, id: \.self)— Iterates over the array directly, using the element's own value as the identifier. Works well for arrays ofString,Int, or anyHashabletype. For custom model objects, conform them toIdentifiableand useForEach(data)without an explicitid.
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
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
let data: [String]— A simple string array as the data source. In a real app, this would be a@Stateor@ObservedObjectproperty so the view updates when data changes.ForEach(data.indices) { index in—data.indicesis an integerRangefrom0todata.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.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".ForEach(0..<100) { index in— The0..<100range literal creates an integerRange. 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 aLazyVStackto avoid materializing all views at startup.Circle().frame(height: 50)— Each iteration produces a circle with 50pt height that expands to the full available width. Theindexclosure 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
// 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
ForEachis a SwiftUI view that generates child views from a collection — it's not a Swiftfor-instatement and must live inside a view builder context- Always provide a stable, unique
idfor each item so SwiftUI can correctly animate insertions, deletions, and reorders - For large datasets (>20 items), use
LazyVStackorLazyHStackinside aScrollViewto defer view creation until items are near the visible viewport
Last updated: June 27, 2026