# SwiftUI Lists and ForEach: Displaying Dynamic Data Clearly

Source: https://devdocs10.com/articles/swift/swift-lists-foreach  
By Murat Bo · Published 2026-05-12 · DevDocs10 (https://devdocs10.com)

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.

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

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.

```swift
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`.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.
