< How to read the Digital Crown on watchOS using digitalCrownRotation() | Introduction to using Core Data with SwiftUI > |
Updated for Xcode 14.0 beta 1
macOS has a dedicated Table
view type for creating lists with multiple columns, including selection and sorting. They work quite differently from lists, because we pass the Table
an array of data to show then specify values to display using key paths, each time also passing a title to show in the header area.
As an example, we might have a simple User
struct like this one:
struct User: Identifiable {
let id: Int
var name: String
var score: Int
}
I’ve used both a String
and Int
for types there, because both are used slightly differently inside Table
.
With that User
struct in place, we could then go ahead and create a few instances of that struct, then display it using a simple Table
like this:
struct ContentView: View {
@State private var users = [
User(id: 1, name: "Taylor Swift", score: 95),
User(id: 2, name: "Justin Bieber", score: 80),
User(id: 3, name: "Adele Adkins", score: 85)
]
var body: some View {
Table(users) {
TableColumn("Name", value: \.name)
TableColumn("Score") { user in
Text(String(user.score))
}
}
}
}
Notice how:
@State
so that we can add sorting shortly. If you don’t need sorting, a simple constant is fine.\.name
.The reason I’ve taken two different approaches is because TableColumn
knows how to use a key path to display values that are simple strings, but for anything else – a score
integer in our case – we need to add custom view creation code by hand. So, when you just want to display a simple string you should use a key path, but for displaying all other types you should use a custom view.
Selection in tables works slightly differently from lists: rather than storing the specific object that was selected, Table
instead wants to bind to the identifier of the object. As our User
struct conforms to Identifiable
, this will be User.ID
– the associated type that points to our id
property. So, we’d add a new property to store an optional User.ID
value, then bind it to the Table
like this:
struct ContentView: View {
@State private var users = [
User(id: 1, name: "Taylor Swift", score: 95),
User(id: 2, name: "Justin Bieber", score: 80),
User(id: 3, name: "Adele Adkins", score: 85)
]
@State private var selection: User.ID?
var body: some View {
Table(users, selection: $selection) {
TableColumn("Name", value: \.name)
TableColumn("Score") { user in
Text(String(user.score))
}
}
}
}
Tip: If you want multiple rows to be selectable, use selection = Set<User.ID>()
rather than selection: User.ID?
.
Adding sorting to the mix takes four steps:
KeyPathComparator
objects with your default sorting inside.sortOrder
parameter to your Table
initializer.@State
, so we can sort it in place.Here’s how our example code looks with those four changes in place:
struct ContentView: View {
@State private var users = [
User(id: 1, name: "Taylor Swift", score: 95),
User(id: 2, name: "Justin Bieber", score: 80),
User(id: 3, name: "Adele Adkins", score: 85)
]
@State private var sortOrder = [KeyPathComparator(\User.name)]
@State private var selection: User.ID?
var body: some View {
Table(users, selection: $selection, sortOrder: $sortOrder) {
TableColumn("Name", value: \.name)
TableColumn("Score", value: \.score) { user in
Text(String(user.score))
}
}
.onChange(of: sortOrder) { newOrder in
users.sort(using: newOrder)
}
}
}
There are two extra things you’ll want to know when using Table
. First, you can set a specific width for one or more columns using a width()
modifier. This can be a fixed value, like this:
TableColumn("Score", value: \.score) { user in
Text(String(user.score))
}
.width(100)
Or you can provide a range of widths, like frame()
:
TableColumn("Score", value: \.score) { user in
Text(String(user.score))
}
.width(min: 50, max: 100)
And second, rather than sending a fixed into Table
, you can also pass a rows
closure that specifies the exact data you want to show. This is helpful when you want to use static list rows, or mix static and dynamic at the same time. Each row needs to be sent in as a TableRow
instance, which will take as its only parameter a value to show – one of our User
instances in our case.
As an example, we could use a ForEach
to create all the regular dynamic rows, but also add a “First” and “Last” user at the start and end of our table:
struct ContentView: View {
@State private var users = [
User(id: 1, name: "Taylor Swift", score: 90),
User(id: 2, name: "Justin Bieber", score: 80),
User(id: 3, name: "Adele Adkins", score: 85)
]
@State private var sortOrder = [KeyPathComparator(\User.name)]
@State private var selection: User.ID?
var body: some View {
Table(selection: $selection, sortOrder: $sortOrder) {
TableColumn("Name", value: \.name)
TableColumn("Score", value: \.score) { user in
Text(String(user.score))
}
.width(min: 50, max: 100)
} rows: {
TableRow(User(id: 0, name: "First", score: 0))
ForEach(users, content: TableRow.init)
TableRow(User(id: 4, name: "Last", score: 100))
}
.onChange(of: sortOrder) { newOrder in
users.sort(using: newOrder)
}
}
}
Tip: As the two extra rows are fixed data, they won’t be affected by any sorting in the users
array.
SAVE 50% To celebrate WWDC22, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.