Displaying tabular data in SwiftUI using Table View

Displaying tabular data in SwiftUI using Table View

Learn how to use tables to represent data in your SwiftUI apps.

The Table view component is particularly useful for representing tabular data within our macOS and iPadOS apps. Each row in a Table represents an item from your data source, while columns display specific properties of the item.

In this article, we will explore how to implement a Table view, focusing on managing the way data is displayed within the table.

Before we start we need a collection of items that will be displayed on the table. For this purpose, we are going to define a custom type to be used in the examples called Project:

struct Project: Identifiable {
    let id = UUID()
    let appName: String
    let developmentHours: Int
    let frameworkNumbers: Int
    let downloads: Int
    let revenue: Double
}

Organizing data in columns

We can now create a collection of items within our view that will organized in a table layout using the Table view component.

import SwiftUI

struct ContentView: View {
    @State var projects = [
        Project(appName: "Procrastinatr", developmentHours: 250, frameworkNumbers: "5", downloads: "1M", revenue: 150000.0),
        Project(appName: "FitTrack", developmentHours: 183, frameworkNumbers: "3", downloads: "500K", revenue: 95000.0),
        Project(appName: "EduLearn", developmentHours: 70, frameworkNumbers: "7", downloads: "2M", revenue: 250000.0),
        Project(appName: "GameZone", developmentHours: 217, frameworkNumbers: "10", downloads: "10M", revenue: 1000000.0),
        Project(appName: "DietCheatr", developmentHours: 92, frameworkNumbers: "4", downloads: "750K", revenue: 175000.0)
    ]
    
    var body: some View {
        Table(projects) {
            // Table content here
        }
    }
    
}

In the example above we are using the Table component with the initializer that requires the collection of data to be displayed and a @TableColumnBuilder result builder that will be responsible for creating table column content from closures.

Each column in the table is represented by a TableColumn object, which defines how a view is displayed for each row.

import SwiftUI

struct ContentView: View {
    @State var projects = [ ... ]
    
    var body: some View {
        Table(apps) {
            TableColumn("App Name", value: \.appName) { app in
                Text(app.appName)
            }
        }
    }
    
}

If we only need to display string properties, we can use the convenience initializer that eliminates the need for an explicit content closure. This initializer directly renders the string as a Text View, simplifying the implementation.

Table(apps) {
    TableColumn("App Name", value: \.appName)
}    

To represent value types that are not strings, the view can be explicitly created:

import SwiftUI

struct ContentView: View {
    @State var projects = [
        Project(appName: "Procrastinatr", developmentHours: 250, frameworkNumbers: "5", downloads: "1M", revenue: 150000.0),
        Project(appName: "FitTrack", developmentHours: 183, frameworkNumbers: "3", downloads: "500K", revenue: 95000.0),
        Project(appName: "EduLearn", developmentHours: 70, frameworkNumbers: "7", downloads: "2M", revenue: 250000.0),
        Project(appName: "GameZone", developmentHours: 217, frameworkNumbers: "10", downloads: "10M", revenue: 1000000.0),
        Project(appName: "DietCheatr", developmentHours: 92, frameworkNumbers: "4", downloads: "750K", revenue: 175000.0)
    ]
    
    var body: some View {
    
        Table(projects) {
            TableColumn("App Name", value: \.appName)
            
            TableColumn("Development Hours", value: \.developmentHours) { app in
                Text("\(app.developmentHours)")  
            }
            
            TableColumn("Framework Numbers", value: \.frameworkNumbers) { app in
                Text("\(app.frameworkNumbers)")
            }
            
            TableColumn("Downloads", value: \.downloads) { app in
                Text("\(app.downloads)")
            }
            
            TableColumn("Revenue", value: \.revenue) { app in
                Text("$\(app.revenue, specifier: "%.2f")")
            }
        }
        
    }
    
}

Organizing data in rows

Another possibility is just defining the different columns of the table and providing value with static rows. Use the init(of:columns:rows:) initializer where we need to specify the value type of the content that is going to be displayed and the rows closure that takes a TableRow object, that takes a single parameter as a static rows.

struct ContentView: View {
    var body: some View {
        Table(of: Project.self) {
            TableColumn("App Name", value: \.appName)
            
            TableColumn("Development Hours", value: \.developmentHours) { app in
                Text("\(app.developmentHours)")
            }
            
            TableColumn("Framework Numbers", value: \.frameworkNumbers) { app in
                Text("\(app.frameworkNumbers)")
            }
            
            TableColumn("Downloads", value: \.downloads) { app in
                Text("\(app.downloads)")
            }
            
            TableColumn("Revenue", value: \.revenue) { app in
                Text("$\(app.revenue, specifier: "%.2f")")
            }
        } rows: {
            TableRow(Project(appName: "Defualt Name", developmentHours: 0, frameworkNumbers: "0", downloads: "", revenue: 0))
        }
    }
}

If we need to display dynamic data we can include a ForEeach block in the rows closure that will dynamically create rows based on the current item.

import SwiftUI

struct ContentView: View {

    @State var projects: [Project] = [ ... ]
    
    var body: some View {
        VStack {
            Table(of: Project.self) {
                TableColumn("App Name", value: \.appName)
                
                TableColumn("Development Hours", value: \.developmentHours) { app in
                    Text("\(app.developmentHours)")
                }
                
                TableColumn("Framework Numbers", value: \.frameworkNumbers) { app in
                    Text("\(app.frameworkNumbers)")
                }
                
                TableColumn("Downloads", value: \.downloads) { app in
                    Text("\(app.downloads)")
                }
                
                TableColumn("Revenue", value: \.revenue) { app in
                    Text("$\(app.revenue, specifier: "%.2f")")
                }
            } rows: {
                ForEach(projects) { project in
                    TableRow(project)
                }
                
                TableRow(Project(appName: "Defualt Name", developmentHours: 0, frameworkNumbers: "0", downloads: "", revenue: 0))
            }
        }
        
        Button("Add Project") {
            projects = [
                Project(appName: "Procrastinatr", developmentHours: 250, frameworkNumbers: "5", downloads: "1M", revenue: 150000.0),
                Project(appName: "FitTrack", developmentHours: 183, frameworkNumbers: "3", downloads: "500K", revenue: 95000.0),
                Project(appName: "EduLearn", developmentHours: 70, frameworkNumbers: "7", downloads: "2M", revenue: 250000.0),
                Project(appName: "GameZone", developmentHours: 217, frameworkNumbers: "10", downloads: "10M", revenue: 1000000.0),
                Project(appName: "DietCheatr", developmentHours: 92, frameworkNumbers: "4", downloads: "750K", revenue: 175000.0)
            ]
        }
    }
}

Sorting items

One of the advantages of displaying data using tables is the possibility of easily ordering items based on columns. To sort data within a Table view we need to define a sortOrder property that will be included into the Table(_:selection:sortOrder:columns:) initializer:

struct ContentView: View {

    @State var projects = [ ... ]
    @State var selectionData : Project.ID? = nil
    @State private var sortOrder = [KeyPathComparator(\Project.downloads)]
    
    var body: some View {
        
        Table(projects, selection: $selectionData, sortOrder: $sortOrder) {
            // Table content here
        }
        .onChange(of: sortOrder) { _, sortOrder in
            projects.sort(using: sortOrder)
        }
        
    }
}

In the example above we check every time that the sortOrder update its value with the "on change" modifier sorting the elements according to the sortOrder property.

0:00
/0:14