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.

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.

List vs ForEach
List
  • Scrollable list UI
  • Built-in row behavior
  • Platform styling
ForEach
  • 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 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

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

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

11. Practice Exercise

Build a SwiftUI view that displays a list of books.

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.