The Ops Community ⚙️

Cover image for How to Create Dynamic Table View in SwiftUI
Nandani Sharma
Nandani Sharma

Posted on • Originally published at canopas.com

How to Create Dynamic Table View in SwiftUI

Background

SwiftUI’s evolution continues to simplify app development, and with SwiftUI 4.0, the new Table view brings an intuitive way to display tabular data.

Whether you’re building a productivity tool, managing records, or presenting structured lists, the Table API offers powerful capabilities with minimal code.

In this post, we’ll explore how to build dynamic tables using SwiftUI’s Table view, covering key features like multi-row selection, sorting, filtering, and expanding rows with the DisclosureTableRow.

By the end, you’ll be well-equipped to harness these new table features in your own SwiftUI apps.

Here is the GitHub Repo with full implementations and a demonstration of all functionality.

Introduction to Table in SwiftUI

Before SwiftUI 4.0, developers had to rely on List views or custom solutions to display tabular data. The introduction of Table in macOS, iOS, and iPadOS provides a more intuitive and powerful way to handle data in a grid-like structure.

A SwiftUI Table allows you to:

  • Display multiple rows and columns of data.
  • Easily define column headers and content for each row.
  • Enable multi-row selection.
  • Apply sorting, filtering, and expandable rows for richer user experiences.

Setting up the Data Models for the Table

We’ll start by creating a Student model, along with associated data for each student’s grade history. Our model will include a student’s name, ID, and grades for subjects like maths, science, and more.


// MARK: - Student Model
struct Student: Codable, Identifiable {
    let id: String
    let name: String
    let gradeHistory: GradeHistory
    var students: [Student] = []

    enum CodingKeys: String, CodingKey {
        case id, name
        case gradeHistory = "grade_history"
        case students
    }
}

// MARK: - GradeHistory Model
struct GradeHistory: Codable, Identifiable{
    let id: String?
    let semester: String
    let subjects: Subjects

    init(
        id: String? = UUID().uuidString,
        semester: String,
        subjects: Subjects
    ) {
        self.id = id ?? UUID().uuidString
        self.semester = semester
        self.subjects = subjects
    }
}

// MARK: - Subjects Model
struct Subjects: Codable, Identifiable {
    let id: String?
    let math: Int
    let science: Int
    let english: Int
    let physics: Int
    let computer: Int
    let socialScience: Int

    init(
        id: String? = nil,
        math: Int,
        science: Int,
        english: Int,
        physics: Int,
        computer: Int,
        socialScience: Int
    ) {
        self.id = id ?? UUID().uuidString
        self.math = math
        self.science = science
        self.english = english
        self.physics = physics
        self.computer = computer
        self.socialScience = socialScience
    }

    enum CodingKeys: String, CodingKey {
        case id = "id"
        case math = "Math"
        case science = "Science"
        case english = "English"
        case physics = "Physics"
        case computer = "Computer"
        case socialScience = "Social Science"
    }
}
Enter fullscreen mode Exit fullscreen mode

To populate our table, we’ll create a list of sample student data, To do that I have added a JSON file that contains a long list of Student data. and reading local files with one helper class that reads data from it and returns a decoded [Student] struct.

Here is the StudentRepository that uses that helper and gets our model.

class StudentRepository {

    init() {
    }

    func getStudents() async -> Result<Students, Error> {
        var students: Students = Students(students: [])

        do {
            students = try await JSONHelper
                .readJSONFromFile(fileName: JSONHelper.templateName,
                                                                      type: Students.self)
            return Result.success(students)
        } catch {
            return Result.failure(error)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we will use this repository everywhere where we need students.

Creating a Basic Table

Now let’s create a simple table that displays the student’s name, ID, and their grades for each subject.

struct PlainTableView: View {

    @State var viewModel: PlainTableViewModel

    var body: some View {
        Group {
            Table(viewModel.students) {
                TableColumn("Index") { student in
                    let index = (viewModel.students.firstIndex(
                        where: { $0.id == student
                            .id }) ?? 0)
                    Text("No. \(index + 1)")
                }

                TableColumn("Id", value: \.id)

                TableColumn("Name", value: \.name)
                    .width(min: 150)

                TableColumn("Math") { student in
                    Text("\(student.gradeHistory.subjects.math)")
                        .foregroundStyle(gradeColor(for: student.gradeHistory.subjects.math))
                }
                TableColumn("Science") { student in
                    Text("\(student.gradeHistory.subjects.science)")
                        .foregroundStyle(gradeColor(for: student.gradeHistory.subjects.science))
                }
                TableColumn("English") { student in
                    Text("\(student.gradeHistory.subjects.english)")
                        .foregroundStyle(gradeColor(for: student.gradeHistory.subjects.english))
                }
                TableColumn("Physics") { student in
                    Text("\(student.gradeHistory.subjects.physics)")
                        .foregroundStyle(gradeColor(for: student.gradeHistory.subjects.physics))
                }
                TableColumn("Computer") { student in
                    Text("\(student.gradeHistory.subjects.computer)")
                        .foregroundStyle(gradeColor(for: student.gradeHistory.subjects.computer))
                }
                TableColumn("Social Science") { student in
                    Text("\(student.gradeHistory.subjects.socialScience)")
                        .foregroundStyle(gradeColor(for: student.gradeHistory.subjects.socialScience))
                }
            }
            .tint(Color.purple.opacity(0.7))
            .navigationTitle("Plain Table")
            .task {
                await viewModel.fetchStudents()
            }
        }
    }

    // Helper function to set color based on grade
    private func gradeColor(for grade: Int) -> Color {
        switch grade {
            case 90...100:
                return .green
            case 75..<90:
                return .yellow
            default:
                return .red
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • The Table view takes an array of students and defines columns using TableColumn.
  • Each column displays relevant data, like the student’s name, ID, and grades according to subjects.

Adding Multiple/Single Selection Support

SwiftUI allows multi-row selection in tables, which is useful when users want to select multiple students for bulk actions.

Step 1: Add a Set To track Selected Rows

We’ll track selected rows using a Set of the student’s ID: and to do that we’ll add that Set to our viewModel like following.

var selectedStudents: Set<Student.ID> = []
Enter fullscreen mode Exit fullscreen mode

To make it a single selection, we will use options Student.ID? instead Set of ID

var selectedStudents: Student.ID? = nil
Enter fullscreen mode Exit fullscreen mode

Step 2: Bind the Table’s Selection

We bind the table’s selection to the selectedStudents set. And need to add EditButton to enable edit mode for multiple selections like the following.

Table(students, selection: $selectedStudents) {
    // TableColumns...
}
#if os(iOS)
    .toolbar(content: {
        EditButton()
     })
#endif
Enter fullscreen mode Exit fullscreen mode

With this setup, users can select multiple rows, and their IDs are stored in the selectedStudents set.

Sorting Students by Name, ID and Subject Grade

Sorting allows users to view data in a preferred order. Let’s add sorting by student name and ID and subjects grade-wise.

Step 1: Define Sorting Rule

Here we are going to sort Sutdents by name ID and subject grade so that we can define KeyPathComparator which sorts data with the provided path’s value like the following in our view.

var sortOrder = [
    KeyPathComparator(\Student.name),
    KeyPathComparator(\Student.gradeHistory.subjects.math)
]
Enter fullscreen mode Exit fullscreen mode

Step 2: Update TableColumns

We need to update table columns arguments to provide the right path to sort with like the following.

Table(viewModel.students,
      sortOrder: $viewModel.sortOrder) {
    TableColumn("Index") { student in
        let index = (viewModel.students.firstIndex(
            where: { $0.id == student
                .id }) ?? 0)
        Text("No. \(index + 1)")
    }

    TableColumn("Id", value: \.id)

    TableColumn("Name", value: \.name)
        .width(min: 150)

    TableColumn("Math", value:\.gradeHistory.subjects.math) {
        Text("\($0.gradeHistory.subjects.math)")
            .foregroundStyle(gradeColor(for: $0.gradeHistory.subjects.math))
    }
    TableColumn("Science", value: \.gradeHistory.subjects.science) {
        Text("\($0.gradeHistory.subjects.science)")
            .foregroundStyle(gradeColor(for: $0.gradeHistory.subjects.science))
    }
    TableColumn("English", value: \.gradeHistory.subjects.english) {
        Text("\($0.gradeHistory.subjects.english)")
            .foregroundStyle(gradeColor(for: $0.gradeHistory.subjects.english))
    }
    TableColumn("Physics", value: \.gradeHistory.subjects.physics) {
        Text("\($0.gradeHistory.subjects.physics)")
            .foregroundStyle(gradeColor(for: $0.gradeHistory.subjects.physics))
    }
    TableColumn("Computer", value: \.gradeHistory.subjects.computer) {
        Text("\($0.gradeHistory.subjects.computer)")
            .foregroundStyle(gradeColor(for: $0.gradeHistory.subjects.computer))
    }
    TableColumn("Social Science", value: \.gradeHistory.subjects.socialScience) {
        Text("\($0.gradeHistory.subjects.socialScience)")
            .foregroundStyle(gradeColor(for: $0.gradeHistory.subjects.socialScience))
    }
}
.onChange(of: viewModel.sortOrder) {
    viewModel.students.sort(using: viewModel.sortOrder)
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Add Sorting State Observer

To manage sorting we’ll observe sortOrder and on change of that sort our students

To do that, we’ll need to add the following code in our view.

.onChange(of: viewModel.sortOrder) {
    viewModel.students.sort(using: viewModel.sortOrder)
}
Enter fullscreen mode Exit fullscreen mode

It seems like magic, right? Let’s see how it works.

Filtering Students by Search Input

We can add filtering functionality to allow users to search for students by name, ID, or subject grades.

Step 1: Add a Search Text State

We add a Published variable in our viewModel to store the user’s search input:

We are adding just var not adding Published property wrapper as we are using the latest Observable property wrapper on our viewModel class. So all variables are default published.

var searchText: String = ""
Enter fullscreen mode Exit fullscreen mode

Step 2: Filter Students Based on Search Text

We modify our students to only show students that match the search text:

var _students: [Student] = []
var students: [Student] {
    var data: [Student] = _students
    if !searchText.isEmpty {

        data = _students.filter { student in
            student.name.lowercased().contains(searchText.lowercased()) ||
            student.id.lowercased().contains(searchText.lowercased()) ||
            "\(student.gradeHistory.subjects.math)".contains(searchText) ||
            "\(student.gradeHistory.subjects.science)".contains(searchText) ||
            "\(student.gradeHistory.subjects.english)".contains(searchText) ||
            "\(student.gradeHistory.subjects.physics)".contains(searchText) ||
            "\(student.gradeHistory.subjects.computer)".contains(searchText) ||
            "\(student.gradeHistory.subjects.socialScience)".contains(searchText)
        }
    }
    return data
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Add a Searchable Modifier

we add the searchable modifier to the Table view:

Table(viewModel.students,
      columns: {
    ///TableColumn...
})
.searchable(text: $viewModel.searchText, prompt: "Search by Name id & grades")
Enter fullscreen mode Exit fullscreen mode

This adds a search bar to the table, allowing users to filter data in real-time.

Interesting!, isn’t it?

This post only has basic Table creation and selection relatad things, to read the complete guide including Table with sorting expandable row and context manu support implementation please visit this blog.


The post is originally published on canopas.com.

Here is the GitHub Repo with full implementations and a demonstration of all functionality.

Show your support and help us to grow by giving the repository a star!⭐️

Your support means the world!

Top comments (1)

Collapse
 
marie1 profile image
Marie

To create a dynamic table view in SwiftUI, you can use the List view, which allows for the creation of scrollable lists that dynamically adjust based on the data source. This approach is effective for implementing content lists, such as those that might be featured on a streaming site like Aniwatch.

Here's a guide on setting up a dynamic table view with a simple data source:

Step 1: Define a Model
Start by creating a model for the data you want to display in your list. For example, if you’re displaying a list of shows or episodes, create a Show model.

swift
Copy code
import SwiftUI

struct Show: Identifiable {
var id = UUID()
var title: String
var description: String
}
Step 2: Create a Sample Data Source
Define an array of Show items, either hardcoded or dynamically loaded, that will act as the data source for the table view.

swift
Copy code
let sampleShows = [
Show(title: "Attack on Titan", description: "An epic battle between humanity and titans."),
Show(title: "Naruto", description: "A young ninja's journey to becoming Hokage."),
Show(title: "Demon Slayer", description: "A story of a boy fighting demons to save his sister.")
]
Step 3: Build the Dynamic List View
Use a List to create a dynamic table view in SwiftUI. The List view will iterate over each item in the data source and display it dynamically.

swift
Copy code
struct ContentView: View {
let shows = sampleShows // Replace with data source (e.g., an API call)

var body: some View {
    NavigationView {
        List(shows) { show in
            VStack(alignment: .leading) {
                Text(show.title)
                    .font(.headline)
                Text(show.description)
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
            .padding(.vertical, 4)
        }
        .navigationTitle("Aniwatch Shows") // Customize for Aniwatch
    }
}
Enter fullscreen mode Exit fullscreen mode

}
Step 4: Enhance the View with Dynamic Data (Optional)
For a real-world app like Aniwatch, you might fetch data dynamically from an API. To update the list dynamically:

Replace the hardcoded shows data with an @State or @ObservedObject that fetches data.
Use onAppear to load data when the view appears.
swift
Copy code
struct ContentView: View {
@State private var shows = Show

var body: some View {
    NavigationView {
        List(shows) { show in
            VStack(alignment: .leading) {
                Text(show.title)
                    .font(.headline)
                Text(show.description)
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
            .padding(.vertical, 4)
        }
        .navigationTitle("Aniwatch Shows")
        .onAppear {
            loadShows()
        }
    }
}

func loadShows() {
    // Load or fetch data here; for now, use static data
    shows = sampleShows
}
Enter fullscreen mode Exit fullscreen mode

}
Step 5: Test and Customize
Run your SwiftUI app in the simulator or on a device to see the list of items displayed. You can customize the style, add images, or even link each row to a detailed view to create a complete browsing experience for Aniwatch.

This setup provides a flexible, dynamic list of shows or movies that can be customized for Aniwatch’s specific content needs.