Popular Searches
Popular Course Categories
Popular Courses

Building Dynamic Lists Using List and ForEach

Building Dynamic Lists Using List and ForEach

Modern iOS UI Development with SwiftUI

Building Dynamic Lists Using List and ForEach in SwiftUI

Course: iOS App Development Training

Topic: Building Dynamic Lists Using List and ForEach

Course Link: iOS Training Course

Demo Link: Register for a Course Demo

Official Website: JustAcademy


1. Introduction to Dynamic Lists in SwiftUI

Dynamic lists are an essential part of modern iOS applications. They allow developers to display collections of information such as contacts, products, messages, tasks, settings, and search results without manually creating a separate view for every item.

SwiftUI provides two important tools for creating dynamic lists: List and ForEach. The List container displays content in a vertically arranged, scrollable interface, while ForEach generates views by iterating over a collection of data.

For example, a shopping application may store hundreds of products in an array. Instead of writing a separate row for each product, a developer can use a single reusable list implementation to display all products automatically.

Learning Objectives

  • Understand dynamic data-driven interfaces in SwiftUI.
  • Learn the purpose and syntax of List and ForEach.
  • Display arrays of strings and custom model objects.
  • Understand the Identifiable protocol and unique identifiers.
  • Create reusable list rows and organize data with sections.
  • Add, delete, and update list items using SwiftUI state.
  • Apply list styling, navigation, and basic troubleshooting techniques.

2. Understanding the List View

List is a SwiftUI container that displays rows of content in a single vertical column. It provides platform-appropriate scrolling, row presentation, and standard list behavior. It is particularly useful when the number of items may change or exceed the available screen space.

Basic Syntax

import SwiftUI

struct ContentView: View {
    var body: some View {
        List {
            Text("Apple")
            Text("Banana")
            Text("Orange")
        }
    }
}

#Preview {
    ContentView()
}

Explanation

  • import SwiftUI imports the SwiftUI framework.
  • struct ContentView: View defines a view.
  • var body: some View describes the interface.
  • List creates the scrollable list container.
  • Each Text view represents one row.

This example uses static content because each row is explicitly written. In a dynamic list, the rows are generated from a collection of data.

3. Understanding the ForEach View

ForEach is a SwiftUI structure that creates views for elements in an underlying collection. Instead of manually repeating similar view code, developers define a single row template that SwiftUI uses for each element.

Basic Syntax

ForEach(items, id: \.self) { item in
    Text(item)
}

In this example, items is a collection, id: \.self tells SwiftUI to use each element's value as its identity, and item represents the current element being processed.

Using id: \.self is appropriate only when the values are unique and stable enough to identify their rows. For custom model objects, implementing Identifiable is often a better approach.

4. Difference Between List and ForEach

Feature List ForEach
Primary purpose Displays rows in a list container. Generates views from collection elements.
Scrolling Provides list scrolling behavior. Does not provide scrolling by itself.
Data iteration Can create rows directly from identifiable data. Iterates through a collection to produce views.
Row presentation Supports list-specific styling and behavior. Uses the views defined in its content closure.
Typical usage Contacts, settings, products, messages. Repeated rows, grouped content, grid or stack items.
Combination List and ForEach can be used together for flexible, data-driven interfaces.

5. Creating a Dynamic List from an Array of Strings

A simple way to understand dynamic lists is to begin with an array of strings. Each string can represent a task, category, product name, or other piece of text.

Example: Displaying Programming Languages

import SwiftUI

struct ContentView: View {
    let languages = [
        "Swift",
        "Kotlin",
        "Java",
        "Python",
        "JavaScript"
    ]

    var body: some View {
        NavigationStack {
            List {
                ForEach(languages, id: \.self) { language in
                    Text(language)
                }
            }
            .navigationTitle("Languages")
        }
    }
}

#Preview {
    ContentView()
}

How It Works

  1. The languages array stores the data.
  2. NavigationStack provides a navigation container.
  3. List creates the scrollable rows.
  4. ForEach iterates over the array.
  5. The closure creates a Text view for each language.
  6. The navigation title displays “Languages” at the top of the screen.

If another language is added to the array, the list can display it without requiring a new row to be written manually.

6. Understanding Identifiable and Unique IDs

SwiftUI needs to identify each element in a dynamic collection. Identity helps SwiftUI determine which rows have been inserted, removed, moved, or updated. When identity is incorrect, animations and updates can behave unexpectedly.

A custom data type can conform to the Identifiable protocol by providing an id property.

Example: Creating a Task Model

import SwiftUI

struct TaskItem: Identifiable {
    let id = UUID()
    var title: String
    var isCompleted: Bool
}

Explanation

  • TaskItem represents one task.
  • Identifiable indicates that each task has an identity.
  • UUID() generates a unique identifier for each newly created task.
  • title stores the task description.
  • isCompleted records whether the task has been completed.

For persisted data, such as records loaded from a database or API, prefer stable identifiers supplied by that data source when available. Avoid generating a new identifier every time a view is recomputed.

7. Displaying Custom Objects in a List

Real applications generally display more than one text value. A task row may show a title, a completion indicator, and additional information. SwiftUI allows you to create a custom row using views such as HStack, VStack, Image, and Text.

Complete Example: Dynamic Task List

import SwiftUI

struct TaskItem: Identifiable {
    let id = UUID()
    var title: String
    var isCompleted: Bool
}

struct ContentView: View {
    let tasks = [
        TaskItem(title: "Learn Swift", isCompleted: true),
        TaskItem(title: "Practice SwiftUI", isCompleted: false),
        TaskItem(title: "Build an iOS app", isCompleted: false)
    ]

    var body: some View {
        NavigationStack {
            List {
                ForEach(tasks) { task in
                    HStack {
                        Image(systemName: task.isCompleted
                              ? "checkmark.circle.fill"
                              : "circle")
                            .foregroundStyle(
                                task.isCompleted ? .green : .secondary
                            )

                        Text(task.title)

                        Spacer()
                    }
                }
            }
            .navigationTitle("My Tasks")
        }
    }
}

#Preview {
    ContentView()
}

Important Concepts

  • ForEach(tasks) works because TaskItem conforms to Identifiable.
  • HStack arranges the icon and title horizontally.
  • Image(systemName:) displays an SF Symbol.
  • The conditional expression chooses an icon based on completion status.
  • Spacer() pushes content toward the leading side of the row.
  • foregroundStyle applies a visual style to the icon.

8. Creating a Dynamic List with @State

When users can add or remove items, the collection must be stored in state or another observable data source. The @State property wrapper is useful for view-owned, local state. When the state changes, SwiftUI updates the relevant interface.

Example: Adding New Items

import SwiftUI

struct ContentView: View {
    @State private var fruits = ["Apple", "Banana", "Orange"]
    @State private var newFruit = ""

    var body: some View {
        NavigationStack {
            VStack {
                HStack {
                    TextField("Enter fruit name", text: $newFruit)
                        .textFieldStyle(.roundedBorder)

                    Button("Add") {
                        let trimmedFruit = newFruit.trimmingCharacters(
                            in: .whitespacesAndNewlines
                        )

                        guard !trimmedFruit.isEmpty else {
                            return
                        }

                        fruits.append(trimmedFruit)
                        newFruit = ""
                    }
                    .buttonStyle(.borderedProminent)
                }
                .padding()

                List {
                    ForEach(fruits, id: \.self) { fruit in
                        Text(fruit)
                    }
                }
            }
            .navigationTitle("Fruit List")
        }
    }
}

#Preview {
    ContentView()
}

How the Example Works

  1. @State stores the current fruit array and text field value.
  2. $newFruit passes a binding to the text field.
  3. The Add button trims leading and trailing whitespace.
  4. The guard statement prevents empty entries.
  5. append() adds a new item to the array.
  6. Setting newFruit to an empty string clears the text field.
  7. SwiftUI refreshes the list when the state changes.

Note: This example uses strings as row identities. Since users can enter duplicate fruit names, a more robust production implementation should use a model with a unique ID, as shown in the next section.

9. Adding and Deleting Items with Stable Identity

For editable lists, model objects with unique IDs make row updates safer. The following example demonstrates adding tasks, toggling their completion status, and deleting them using the standard list editing behavior.

import SwiftUI

struct TaskItem: Identifiable {
    let id = UUID()
    var title: String
    var isCompleted = false
}

struct ContentView: View {
    @State private var tasks = [
        TaskItem(title: "Study Swift"),
        TaskItem(title: "Practice List and ForEach"),
        TaskItem(title: "Build a project")
    ]

    @State private var newTask = ""

    var body: some View {
        NavigationStack {
            VStack {
                HStack {
                    TextField("New task", text: $newTask)
                        .textFieldStyle(.roundedBorder)

                    Button("Add") {
                        let title = newTask.trimmingCharacters(
                            in: .whitespacesAndNewlines
                        )

                        guard !title.isEmpty else { return }

                        tasks.append(TaskItem(title: title))
                        newTask = ""
                    }
                }
                .padding()

                List {
                    ForEach($tasks) { $task in
                        Toggle(task.title, isOn: $task.isCompleted)
                    }
                    .onDelete { offsets in
                        tasks.remove(atOffsets: offsets)
                    }
                }
            }
            .navigationTitle("Task Manager")
            .toolbar {
                EditButton()
            }
        }
    }
}

#Preview {
    ContentView()
}

Understanding the Binding Syntax

  • ForEach($tasks) iterates through a mutable collection using bindings.
  • ForEach($tasks) { $task in ... } provides a binding to each task element.
  • Toggle updates the task's completion value through its binding.
  • .onDelete receives the index positions of the rows selected for deletion.
  • remove(atOffsets:) removes the selected elements from the array.
  • EditButton() allows users to enter and leave list editing mode.

This example is designed for a modern SwiftUI environment. If you use an older deployment target or a different data model, check the API availability and adapt the implementation accordingly.

10. Organizing Dynamic Content with Sections

Sections group related rows and make long lists easier to navigate. For example, a settings screen may separate account options from privacy options, while a task manager may separate active tasks from completed tasks.

Example: Grouping Categories

import SwiftUI

struct ContentView: View {
    let categories = [
        ("Programming", ["Swift", "Java", "Python"]),
        ("Design", ["Figma", "Sketch", "Illustrator"]),
        ("Database", ["MySQL", "PostgreSQL", "SQLite"])
    ]

    var body: some View {
        NavigationStack {
            List {
                ForEach(categories, id: \.0) { category in
                    Section(category.0) {
                        ForEach(category.1, id: \.self) { item in
                            Text(item)
                        }
                    }
                }
            }
            .navigationTitle("Learning Topics")
        }
    }
}

#Preview {
    ContentView()
}

Why Use Section?

  • Separates related data into logical groups.
  • Improves readability in large lists.
  • Supports section headers and footers.
  • Provides a natural structure for settings, directories, and catalog screens.

In this example, each category name is used as the outer identifier. This is appropriate because the category names are unique in the sample data. For data that can contain duplicate category names, use a dedicated model with a unique ID.

11. Styling Dynamic Lists

SwiftUI supports list styles and row modifiers that help align a list with an application's design. The exact appearance can differ by platform and surrounding view hierarchy.

Common Styling Modifiers

Modifier Purpose Example
.listStyle(.plain) Uses a simple list presentation. Basic data lists.
.listStyle(.insetGrouped) Uses an inset grouped appearance on supported platforms. Settings-style screens.
.listRowSeparator(.hidden) Hides row separators. Custom-designed rows.
.listRowBackground(Color.clear) Uses a clear row background. Rows with a custom visual background.
.navigationTitle("Title") Sets the navigation title. Contacts or task screens.

Styling Example

List {
    ForEach(["Profile", "Notifications", "Privacy"], id: \.self) { item in
        HStack {
            Text(item)
            Spacer()
            Image(systemName: "chevron.right")
                .foregroundStyle(.secondary)
        }
        .listRowSeparator(.hidden)
    }
}
.listStyle(.plain)
.navigationTitle("Settings")

Choose modifiers based on the intended design and test them on the target iOS versions. Avoid excessive styling that makes rows inconsistent with familiar iOS interaction patterns.

12. Creating Reusable List Row Views

When list rows contain several elements or appear in multiple screens, move the row design into a separate SwiftUI view. This improves readability and makes the row easier to maintain.

import SwiftUI

struct Product: Identifiable {
    let id = UUID()
    let name: String
    let price: Double
}

struct ProductRow: View {
    let product: Product

    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(product.name)
                    .font(.headline)

                Text("Product price")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            Spacer()

            Text(product.price, format: .currency(code: "USD"))
                .fontWeight(.semibold)
        }
        .padding(.vertical, 4)
    }
}

struct ContentView: View {
    let products = [
        Product(name: "Keyboard", price: 49.99),
        Product(name: "Mouse", price: 24.50),
        Product(name: "Monitor", price: 199.00)
    ]

    var body: some View {
        NavigationStack {
            List(products) { product in
                ProductRow(product: product)
            }
            .navigationTitle("Products")
        }
    }
}

#Preview {
    ContentView()
}

The example uses List(products) directly because Product conforms to Identifiable. SwiftUI creates a row for each product, and ProductRow controls how that product appears.

The currency format uses USD for demonstration. Replace the currency code with the appropriate code for your application.

13. List Navigation and Row Selection

Dynamic lists often act as navigation menus. A user can select a row to open a detail screen, view a product, or inspect a contact. SwiftUI provides navigation components that work naturally with lists.

Example: NavigationLink Inside a List

import SwiftUI

struct Course: Identifiable {
    let id = UUID()
    let name: String
    let description: String
}

struct ContentView: View {
    let courses = [
        Course(
            name: "Swift Fundamentals",
            description: "Learn Swift syntax and core programming concepts."
        ),
        Course(
            name: "SwiftUI Development",
            description: "Build interfaces using declarative UI."
        ),
        Course(
            name: "iOS Projects",
            description: "Apply your skills to practical applications."
        )
    ]

    var body: some View {
        NavigationStack {
            List(courses) { course in
                NavigationLink {
                    CourseDetailView(course: course)
                } label: {
                    VStack(alignment: .leading) {
                        Text(course.name)
                            .font(.headline)

                        Text(course.description)
                            .font(.subheadline)
                            .foregroundStyle(.secondary)
                    }
                }
            }
            .navigationTitle("Courses")
        }
    }
}

struct CourseDetailView: View {
    let course: Course

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text(course.name)
                .font(.title)

            Text(course.description)

            Spacer()
        }
        .padding()
        .navigationTitle("Course Details")
    }
}

#Preview {
    ContentView()
}

NavigationLink connects each row to a destination view. Passing the selected model object to the detail view allows the destination to display the correct information.

14. Handling Empty Lists and Empty States

Applications should handle cases where no data is available. This can happen when a user has not created any tasks, a search returns no results, or a server returns an empty response.

Example

if tasks.isEmpty {
    ContentUnavailableView(
        "No Tasks",
        systemImage: "checklist",
        description: Text("Add a task to get started.")
    )
} else {
    List(tasks) { task in
        Text(task.title)
    }
}

For projects targeting an iOS version where ContentUnavailableView is unavailable, build a custom empty-state view using VStack, Image, and Text. A useful empty state explains what happened and gives the user a clear next step.

15. Best Practices for Dynamic Lists

  • Use stable identifiers: Prefer a model's unique ID rather than relying on a potentially duplicated title.
  • Keep row views focused: Move complicated row layouts into reusable view structures.
  • Use state appropriately: Store view-owned changing data in @State, and use an appropriate observable model for shared or larger data sources.
  • Handle empty data: Provide helpful messaging when the collection contains no elements.
  • Keep rows accessible: Use clear labels, readable text, and suitable controls.
  • Avoid unnecessary work: Do not perform expensive network requests or computations directly inside row-building closures.
  • Use the correct container: Prefer List for standard list behavior and consider ScrollView with lazy stacks for more customized layouts.
  • Test state changes: Check how the list behaves when items are inserted, edited, removed, or reordered.
  • Preserve identity: Do not recreate identifiers during every view update.
  • Validate user input: Prevent accidental blank items and handle invalid or duplicate data when appropriate.

16. Common Errors and Troubleshooting

Problem Possible Cause Solution
ForEach shows an identity-related error. The elements do not have a valid identity. Conform the model to Identifiable or provide a unique id key path.
Rows display incorrectly after changes. Identifiers are duplicated or unstable. Use a stable, unique identifier for each element.
New items do not appear. The array is not connected to observable state. Use @State or the appropriate observable data source.
Deleting rows does not work. The deletion handler is missing or targets the wrong collection. Use .onDelete and remove the supplied offsets from the source array.
Duplicate string entries behave unexpectedly. id: \.self is used for repeated values. Use identifiable model objects with unique IDs.
Rows appear crowded. The row layout lacks spacing or hierarchy. Use HStack, VStack, padding, and clear text styles.
The empty list looks unfinished. No empty-state interface is provided. Add helpful instructions or a suitable empty-state view.
A styling modifier has no expected effect. The modifier depends on the platform, list style, or view hierarchy. Check API availability and test the list on the target device.

17. Practical Exercises

Exercise 1: Programming Language List

  • Create an array containing at least five programming languages.
  • Display each language using List and ForEach.
  • Add a navigation title.
  • Experiment with a different list style.

Exercise 2: Shopping List

  • Create a model with a unique identifier and an item name.
  • Display the items in a dynamic list.
  • Add a text field and button to insert new items.
  • Validate input to prevent blank entries.
  • Implement row deletion.

Exercise 3: Course Directory

  • Create a course model containing a name and description.
  • Display courses using a reusable row view.
  • Group courses into sections such as Programming, Design, and Database.
  • Use NavigationLink to open a course detail screen.

Mini Project: Personal Task Manager

Build a task management application using SwiftUI. The application should display a dynamic list of tasks, allow users to add and delete tasks, toggle completion status, and display an empty state when no tasks remain.

Suggested features:

  • Task title and completion status.
  • Stable identifiers for task records.
  • Add, edit, and delete operations.
  • Visual distinction between completed and pending tasks.
  • Navigation title and organized row layout.
  • Validation for empty task titles.

18. Interview Questions and Answers

Q1. What is List in SwiftUI?

Answer: List is a container that displays rows of views in a vertical, scrollable interface. It is commonly used for contacts, settings, tasks, and product collections.

Q2. What is ForEach in SwiftUI?

Answer: ForEach generates views from the elements of a collection. It allows developers to create repeated interface elements without manually writing each row.

Q3. Can ForEach be used without List?

Answer: Yes. It can be used in containers such as VStack, HStack, LazyVStack, and LazyVGrid. However, ForEach does not provide scrolling by itself.

Q4. What is the Identifiable protocol?

Answer: Identifiable requires a model to provide an identity through an id property. SwiftUI uses identity to track collection elements as the interface changes.

Q5. Why are unique IDs important in dynamic lists?

Answer: Unique IDs help SwiftUI distinguish individual rows. They support correct updates, insertion and deletion animations, and reliable tracking when data changes.

Q6. What does id: \.self mean?

Answer: It tells SwiftUI to use each element's value as its identity. It is useful for simple collections when every value is unique, but it is unsuitable for collections containing duplicate values.

Q7. How can a user add an item to a dynamic list?

Answer: Store the collection in an appropriate state or observable data source and append the new item. SwiftUI updates the interface when the relevant state changes.

Q8. How do you delete rows from a SwiftUI list?

Answer: Use the .onDelete modifier on a suitable ForEach and remove the provided index offsets from the backing collection.

Q9. What is the purpose of Section?

Answer: Section groups related rows and can provide headers and footers to organize information into meaningful categories.

Q10. When should you create a reusable row view?

Answer: Create a reusable row view when list items have a complex layout, when the same row design is used in multiple places, or when separating presentation from the parent view improves readability.

19. Quick Revision Checklist

  • Understand the purpose of List.
  • Understand how ForEach generates repeated views.
  • Know the difference between static and dynamic lists.
  • Use unique and stable identifiers.
  • Implement Identifiable for custom model types.
  • Use @State for suitable view-owned mutable data.
  • Display custom row layouts with SwiftUI views.
  • Organize data with Section.
  • Support adding, updating, and deleting items.
  • Handle empty states and validate input.
  • Test list behavior on supported iOS devices and versions.

20. Conclusion

Building dynamic lists with List and ForEach is a fundamental skill in SwiftUI development. List provides a scrollable container for rows, while ForEach generates views from collections of data. Together, they help developers build maintainable interfaces for tasks, products, contacts, courses, settings, and many other application features.

By learning stable identity, state management, reusable row views, sections, navigation, and editing operations, you can move beyond static layouts and create responsive data-driven iOS applications.

21. Additional Learning Resources

Previous Next
whatsapp