SwiftUI Lists and ForEach: Displaying Dynamic Data Clearly
SwiftUI apps often need to show repeated data such as contacts, tasks, messages, or settings rows. This article explains how List and ForEach work in SwiftUI, how they relate to each other, how to bind them to data safely, and how to avoid the identity mistakes that commonly cause updates, warnings, or confusing bugs.
Quick answer: Use List when you want a scrollable, platform-style list UI, and use ForEach when you want to repeat views from a collection. In practice, you often use ForEach inside a List so SwiftUI can build one row per data item.
Difficulty: Beginner
Helpful to know first: You'll understand this better if you know basic Swift syntax, simple SwiftUI views, and how arrays store multiple values.
1. What Is Lists & ForEach?
In SwiftUI, List and ForEach solve related but different problems.
- List creates a scrollable list interface with rows, separators, selection behavior, and platform-appropriate styling.
- ForEach repeats a view for each element in a collection.
- ForEach does not automatically create a list UI by itself. It only generates repeated child views.
- SwiftUI needs a stable way to identify each item so it can update the correct row when data changes.
- You can use ForEach inside List, VStack, LazyVStack, and other containers.
A simple way to think about the relationship is this: List is the container that looks and behaves like a list, while ForEach is the mechanism that repeats views from data.
- Scrollable list UI
- Built-in row behavior
- Platform styling
- Repeats child views
- Needs a container
- Uses item identity
You often combine both: List provides the list UI, and ForEach builds each row.
Because these two concepts are commonly confused, keep this rule in mind: if your goal is to show rows from data, you will often use both together.
2. Why Lists & ForEach Matters
Repeated content is one of the most common patterns in app development. A to-do app shows tasks, a music app shows tracks, a shopping app shows products, and a settings screen shows options. Without List and ForEach, you would have to manually create every row, which would not scale and would be hard to maintain.
These tools matter because they let SwiftUI connect your interface directly to your data model. When the data changes, SwiftUI can redraw only the views that need updating, as long as item identity is correct.
You should use them when:
- You need to display many similar items from an array or collection.
- Your UI should update automatically when data changes.
- You want cleaner, more maintainable SwiftUI code.
- You need built-in list interactions such as swipe actions, deletion, or selection.
You should not force List everywhere. If you only need a few repeated views inside a custom layout, a ForEach inside a VStack or LazyVStack may be a better fit.
3. Basic Syntax or Core Idea
The core idea is that SwiftUI reads a collection and creates one child view per element. Each element must have stable identity.
Using List with static rows
This example shows a basic List with rows written by hand.
import SwiftUI
struct ContentView: View {
var body: some View {
List {
Text("Home")
Text("Profile")
Text("Settings")
}
}
}This works, but the rows are fixed. It is fine for short static content, but not ideal for dynamic data.
Using ForEach to repeat rows
Now the rows come from an array. Because strings can be uniquely identified here, the example uses id: \ .self.
import SwiftUI
struct ContentView: View {
let items = ["Home", "Profile", "Settings"]
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
}
}
}SwiftUI creates one Text row for each string in the array.
Using Identifiable models
For real apps, it is usually better to make your model conform to Identifiable. That removes the need to manually pass an id in many cases.
import SwiftUI
struct Task: Identifiable {
let id = UUID()
let title: String
}
struct ContentView: View {
let tasks = [
Task(title: "Buy milk"),
Task(title: "Reply to email"),
Task(title: "Walk the dog")
]
var body: some View {
List {
ForEach(tasks) { task in
Text(task.title)
}
}
}
}This is a common and recommended pattern because each task has a stable unique identifier.
4. Step-by-Step Examples
Example 1: Repeating simple values in a List
This is the most basic dynamic example. It works well for unique primitive values such as distinct strings.
import SwiftUI
struct FruitListView: View {
let fruits = ["Apple", "Banana", "Orange"]
var body: some View {
List {
ForEach(fruits, id: \.self) { fruit in
Text(fruit)
}
}
}
}The list shows one row per fruit. Using id: \ .self is acceptable here because each value is unique and hashable.
Example 2: Using custom row views
Real projects often use a custom row view instead of plain text. This keeps the code cleaner.
import SwiftUI
struct Contact: Identifiable {
let id = UUID()
let name: String
let jobTitle: String
}
struct ContactRow: View {
let contact: Contact
var body: some View {
VStack(alignment: .leading) {
Text(contact.name)
Text(contact.jobTitle)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
struct ContactsView: View {
let contacts = [
Contact(name: "Maya", jobTitle: "Designer"),
Contact(name: "Jordan", jobTitle: "iOS Developer")
]
var body: some View {
List {
ForEach(contacts) { contact in
ContactRow(contact: contact)
}
}
}
}This approach is easier to maintain because row layout is separated into its own reusable view.
Example 3: Using ForEach without List
ForEach is not limited to list rows. You can use it anywhere repeated child views make sense.
import SwiftUI
struct TagCloudView: View {
let tags = ["Swift", "SwiftUI", "Xcode"]
var body: some View {
VStack(alignment: .leading, spacing: 8) {
ForEach(tags, id: \.self) { tag in
Text(tag)
.padding(8)
.background(Color.blue.opacity(0.1))
.cornerRadius(8)
}
}
.padding()
}
}Here, ForEach is simply repeating views in a vertical stack. There is no list-style container involved.
Example 4: Editable data with state
Dynamic lists become more useful when they are driven by state. In this example, tapping a button adds a new row.
import SwiftUI
struct Note: Identifiable {
let id = UUID()
let title: String
}
struct NotesView: View {
@State private var notes = [
Note(title: "First note"),
Note(title: "Second note")
]
var body: some View {
VStack {
List {
ForEach(notes) { note in
Text(note.title)
}
}
Button("Add Note") {
notes.append(Note(title: "New note"))
}
.padding()
}
}
}Because the array is stored in @State, the view updates when a new note is added.
5. Practical Use Cases
- Showing a to-do list where each task is a row with a title, due date, and completion state.
- Building a settings screen with sections and repeated options.
- Displaying chat conversations, notifications, or activity items from an array of models.
- Rendering search results returned from a network request.
- Showing reusable rows in a sidebar on iPad or macOS.
- Displaying repeated content inside custom layouts, such as tags, cards, or simple dashboards.
6. Common Mistakes
Mistake 1: Using non-unique values with id: .self
Beginners often use id: \ .self for everything. That is only safe when each value is truly unique and stable.
Problem: If duplicate values appear, SwiftUI cannot reliably tell rows apart. That can cause warnings about duplicate IDs or unexpected UI updates.
let names = ["Alex", "Alex", "Sam"]
List {
ForEach(names, id: \.self) { name in
Text(name)
}
}Fix: Use a model with a unique identifier, or provide a truly unique property as the item ID.
struct Person: Identifiable {
let id = UUID()
let name: String
}
let people = [
Person(name: "Alex"),
Person(name: "Alex"),
Person(name: "Sam")
]
List {
ForEach(people) { person in
Text(person.name)
}
}The corrected version works because each row now has a stable unique identity even when names repeat.
Mistake 2: Forgetting identity for custom data
When you pass custom structs to ForEach, SwiftUI must know how to identify each one.
Problem: If the data does not conform to Identifiable and you do not provide an id, the code will not compile because SwiftUI cannot determine item identity.
struct Product {
let name: String
}
let products = [
Product(name: "Keyboard"),
Product(name: "Mouse")
]
List {
ForEach(products) { product in
Text(product.name)
}
}Fix: Make the type conform to Identifiable or pass an id key path.
struct Product: Identifiable {
let id = UUID()
let name: String
}
let products = [
Product(name: "Keyboard"),
Product(name: "Mouse")
]
List {
ForEach(products) { product in
Text(product.name)
}
}The corrected version works because SwiftUI now knows how to track each product across updates.
Mistake 3: Expecting the list to update without state
Another common issue is changing data that is not stored in a property wrapper such as @State or managed by an observable object.
Problem: The data changes in code, but SwiftUI does not know it should redraw the view, so the list appears not to update.
struct ContentView: View {
var items = ["One", "Two"]
var body: some View {
Button("Add") {
// cannot safely drive UI updates this way
}
}
}Fix: Store changing view data in @State so SwiftUI can observe updates.
struct ContentView: View {
@State private var items = ["One", "Two"]
var body: some View {
VStack {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
}
Button("Add") {
items.append("Three")
}
}
}
}The corrected version works because changing a @State property triggers a view update.
Mistake 4: Using the wrong collection shape
ForEach expects collection types it can iterate in the ways SwiftUI requires. Some beginners pass data in a form that does not match the initializer they are using.
Problem: This often leads to compiler messages such as Generic struct 'ForEach' requires that 'Data' conform to 'RandomAccessCollection' because the provided data does not match the expected collection requirements.
let numbers = 1...5
List {
ForEach(numbers) { number in
Text("\(number)")
}
}Fix: Use the initializer that matches the data, or convert the data to an array when needed.
let numbers = Array(1...5)
List {
ForEach(numbers, id: \.self) { number in
Text("\(number)")
}
}The corrected version works because SwiftUI now receives a collection shape and identity information it can use correctly.
7. Best Practices
Prefer Identifiable models for app data
While id: \ .self is convenient, model types with explicit IDs are safer for real data because names, titles, and other visible properties can repeat.
struct Message: Identifiable {
let id: UUID
let text: String
}This makes updates, deletions, and row animations more reliable.
Keep row views small and reusable
If a list row contains multiple labels, icons, or layout rules, move it into a dedicated view type instead of writing everything inline.
struct TaskRow: View {
let title: String
var body: some View {
Text(title)
}
}This improves readability and makes your rows easier to test and reuse.
Use List for list behavior and ForEach for repetition
Avoid treating them as interchangeable. Pick the container based on the UI behavior you want.
List {
ForEach(tasks) { task in
TaskRow(title: task.title)
}
}This is preferred when you want standard scrolling rows instead of a custom stack layout.
8. Limitations and Edge Cases
- id: \ .self is only safe when values are unique and stable. Duplicate strings or changing values can lead to incorrect row identity.
- If your list is not updating, the problem is often not List itself but how the data is stored or identified.
- ForEach is not a lazy layout by itself. The laziness and scrolling behavior depend on the container you place it in, such as List or LazyVStack.
- Some visual behavior of List differs by platform and OS version, especially row styling and grouped appearance.
- Using indices as IDs can work for simple read-only data, but it becomes risky when items are inserted, deleted, or reordered because indices change.
- A common “list not updating” scenario happens when the visible text changes but the underlying identity is unstable, causing SwiftUI to reuse the wrong row.
9. Practical Mini Project
This mini project builds a small task list with dynamic rows and an add button. It uses List for the list UI and ForEach to generate rows from state.
import SwiftUI
struct TaskItem: Identifiable {
let id = UUID()
let title: String
let isImportant: Bool
}
struct TaskListView: View {
@State private var tasks = [
TaskItem(title: "Finish SwiftUI lesson", isImportant: true),
TaskItem(title: "Review pull request", isImportant: false)
]
var body: some View {
NavigationStack {
VStack {
List {
ForEach(tasks) { task in
HStack {
Text(task.title)
Spacer()
if task.isImportant {
Text("Important")
.font(.caption)
.foregroundColor(.red)
}
}
}
}
Button("Add Sample Task") {
let newTask = TaskItem(
title: "New task \(tasks.count + 1)",
isImportant: tasks.count % 2 == 0
)
tasks.append(newTask)
}
.padding()
}
.navigationTitle("Tasks")
}
}
}This example shows a complete working pattern you can reuse: an identifiable model, state-driven data, and a row generated for each item. As you expand it, you could add deletion, editing, sorting, or navigation to a detail screen.
10. Key Points
- List creates a scrollable list-style container.
- ForEach repeats views from a collection of data.
- You often use ForEach inside List to build dynamic rows.
- SwiftUI needs stable identity for each item to update rows correctly.
- Identifiable models are usually the safest choice for app data.
- id: \ .self is fine only when values are unique and stable.
- If a list does not update, check your state management and item identity first.
11. Practice Exercise
Build a SwiftUI view that displays a list of books.
- Create a Book model with a title and author.
- Make the model conform to Identifiable.
- Store at least three books in an array.
- Use List and ForEach to show one row per book.
- Display the title on the first line and the author in a smaller secondary style.
Expected output: A scrollable SwiftUI list where each row shows a book title and author.
Hint: Create a custom row layout with VStack inside the ForEach closure.
import SwiftUI
struct Book: Identifiable {
let id = UUID()
let title: String
let author: String
}
struct BooksView: View {
let books = [
Book(title: "Swift Programming", author: "Taylor"),
Book(title: "iOS Patterns", author: "Morgan"),
Book(title: "UI Design Basics", author: "Casey")
]
var body: some View {
List {
ForEach(books) { book in
VStack(alignment: .leading) {
Text(book.title)
Text(book.author)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
}
}12. Final Summary
List and ForEach are a core part of building data-driven SwiftUI interfaces. List gives you the visual and interactive behavior of a list, while ForEach repeats views from a collection. Once you understand that difference, SwiftUI list code becomes much easier to read and write.
The most important concept to remember is identity. If each item has a stable unique ID and your data is stored in the right kind of state, SwiftUI can update rows correctly and efficiently. A strong next step is to learn list editing features such as onDelete, onMove, and navigation from list rows to detail views.