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.
Here is the 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.
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)
}
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.