SwiftUI's Table View on iOS 16

SwiftUI's Table View on iOS 16

How you can customize the table’s appearance by implementing compact-specific logic in the first column

In this blog post, I'll introduce the collection view Table in SwiftUI and explain how to leverage this view on iOS 16 to build a multiplatform app.

SwiftUI provides several collection views that you can use to assemble other views into dynamic groupings with complex, built-in behaviors. For example, you can create a List view to enable scrolling through an extensive data set arranged in a single column. The list automatically provides certain basic behaviors, but you can add others with minimal additional configuration, like swipe and pull-to-refresh.

SwiftUI also provides Table, a container that presents rows of data arranged in multiple columns. Table is available on macOS since 12.0+ and will become available on iOS 16.0+ and iPadOS 16.0+.

Your data model has to conform to Identifiable.

To make the columns of a table sortable, provide a binding to an array of SortComparator instances. The table reflects the sorted state through its column headers, allowing sorting for any columns with key paths.

struct Person: Identifiable {
    let givenName: String
    let familyName: String
    let emailAddress: String
    let id = UUID()
}
private var people = [
    Person(givenName: "Juan", familyName: "Chavez", emailAddress: "juanchavez@icloud.com"),
    Person(givenName: "Mei", familyName: "Chen", emailAddress: "meichen@icloud.com"),
    Person(givenName: "Tom", familyName: "Clark", emailAddress: "tomclark@icloud.com"),
    Person(givenName: "Gita", familyName: "Kumar", emailAddress: "gitakumar@icloud.com")
]
@State private var sortOrder = [KeyPathComparator(\Person.givenName)]

var body: some View {
    Table(people, sortOrder: $sortOrder) {
        TableColumn("Given Name", value: \.givenName)
        TableColumn("Family Name", value: \.familyName)
        TableColumn("E-Mail address", value: \.emailAddress)
    }
    .onChange(of: sortOrder) {
        people.sort(using: $0)
    }
}

Here is the sortable table on macOS.

Sortable table on MacOS

Here is the sortable table on iPad.

Sortable table on iPad

If there are more rows than can fit in the available space, Table provides vertical scrolling automatically. On macOS, the table also provides horizontal scrolling if there are more columns than can fit in the width of the view. Scroll bars appear as needed on iOS; on macOS, the Table shows or hides scroll bars based on the “Show scroll bars” system preference.

I predict that macOS developers will be intrigued to use Table on iOS with the release of iOS 16 in September 2022. There is one caveat to point out! The layout needs to be handled differently when the device has compact width, e.g. smaller iPhones.

Here is the same sortable table on iPhone 13.

Sortable table on iPhone 13

Only the first column is shown. The Apple documentation states:

macOS and iPadOS support SwiftUI tables. On iOS, and in other situations with a compact horizontal size class, tables don’t show headers and collapse all columns after the first. If you present a table on iOS, you can customize the table’s appearance by implementing compact-specific logic in the first column.

This doesn't sound difficult. After all, SwiftUI offers horizontalSizeClass as an environment value.

    @Environment(\.horizontalSizeClass) var horizontalSizeClass

You can use this environment value in your body implementation to choose a different view that aggregates data from different columns.

    Table(people, sortOrder: $sortOrder) {
          TableColumn("Given Name", sortUsing: KeyPathComparator(\Person.givenName)) { person in
                if horizontalSizeClass == .compact {
                    // TODO: different representation
                } else {
                    Text(person.givenName)
                }
            }
            TableColumn("Family Name", value: \.familyName)
            TableColumn("E-Mail address", value: \.emailAddress)
    }

However, the environment value does not exist on macOS!

This is not a problem if your target builds only for iOS. But if you want to use a multiplatform target, available in Xcode 14, then you need a shim.

Stackoverflow explains that you can implement size classes on macOS as custom EnvironmentValues. They return .regular at all times, but it's enough to function the same as on iOS.

#if os(macOS)
    enum UserInterfaceSizeClass {
        case compact
        case regular
    }

    struct HorizontalSizeClassEnvironmentKey: EnvironmentKey {
        static let defaultValue: UserInterfaceSizeClass = .regular
    }

    struct VerticalSizeClassEnvironmentKey: EnvironmentKey {
        static let defaultValue: UserInterfaceSizeClass = .regular
    }

    extension EnvironmentValues {
        var horizontalSizeClass: UserInterfaceSizeClass {
            get { self[HorizontalSizeClassEnvironmentKey.self] }
            set { self[HorizontalSizeClassEnvironmentKey.self] = newValue }
        }

        var verticalSizeClass: UserInterfaceSizeClass {
            get { self[VerticalSizeClassEnvironmentKey.self] }
            set { self[VerticalSizeClassEnvironmentKey.self] = newValue }
        }
    }
#endif

Now the example can compile in a multiplatform target when running on macOS.

The last step is providing a View to outline data running in compact width.

struct TableRowView: View {
    var person: Person
    var body: some View {
        HStack {
            Text(person.givenName)
            Text(person.familyName)
            Spacer()
            Text(person.emailAddress)
        }
    }
}

// ...

    Table(people, sortOrder: $sortOrder) {
          TableColumn("Given Name", sortUsing: KeyPathComparator(\Person.givenName)) { person in
                if horizontalSizeClass == .compact {
                    TableRowView(person: person
                } else {
                    Text(person.givenName)
                }
            }
            TableColumn("Family Name", value: \.familyName)
            TableColumn("E-Mail address", value: \.emailAddress)
    }

Sortable Table on iOS with custom layout

My TableRowView is a simplified example. Probably better would be to use LabeledContent, a new view that will be available in iOS 16.0 and macOS 13.0. I recommend reading Mastering LabeledContent in SwiftUI from Majid Jabrayilov for more information.

I can also recommend StewartLynch's video about SwiftUI Table.

Did you find this article valuable?

Support Swifty Tech by Marco Eidinger by becoming a sponsor. Any amount is appreciated!