😐

【SwiftUI】 Table 2025

2025/02/18に公開

2025年(春の訪れのほんの少し前)におけるSwiftUIの Table の仕様をまとめる。

Table とは

表を作れるもの。

https://developer.apple.com/documentation/swiftui/table

用語

Row 行
横向きに切り裂いた時のそれぞれの塊

Column 列
縦向きに切り裂いた時のそれぞれの塊

仕様

  • iPhoneなど横幅が狭いときは全部表示しない これはサイズクラスの横がコンパクトのときにおこる
  • 行を選択可能。ひとつのみ/複数の両方に対応
  • 行をソート可能。ソートネタの列を指定する方式
  • 計算型のセルが可能

コードの共通部分

以下のものを共通で使用する

struct Person: Identifiable {
    let givenName: String
    let familyName: String
    let emailAddress: String
    let id = UUID()
    var fullName: String { givenName + " " + familyName }
}

//View内で
    @State 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")
    ]

基本

import SwiftUI

struct Person: Identifiable {
    let givenName: String
    let familyName: String
    let emailAddress: String
    let id = UUID()
    var fullName: String { givenName + " " + familyName }
}

struct PeopleTable: View {
    
    @State 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")
    ]
    
    var body: some View {
        Table(people) {
            TableColumn("Given Name", value: \.givenName)
            TableColumn("Family Name", value: \.familyName)
            TableColumn("E-Mail Address", value: \.emailAddress)
        }
    }
}
iPhone iPad

選択

ある行を選択可能にする。
Set を渡すと複数選択可能。
単一の選択なら0個選択に対応するためにoptionalで宣言する。

struct SelectableTable: View {
    @State private var selectedPeople = Set<Person.ID>()
    //単一の選択なら @State private var selectedPeople: Person.ID?

    @State private var people = //略

    @State 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")
    ]
    
    var body: some View {
        Table(people, selection: $selectedPeople) {
            TableColumn("Given Name", value: \.givenName)
            TableColumn("Family Name", value: \.familyName)
            TableColumn("E-Mail Address", value: \.emailAddress)
        }
        Text("\(selectedPeople.count) people selected")
    }
}
iPhone iPad

ソート

どれかの列をソートのネタにする。
テーブルのイニシャライザに sortOrder: を渡す。
sortOrder: が渡されていればソートする。かつ、他のソート方式にも操作できる。
表示した時点で sortOrder を適用させるには initial: true

「ソースコードの中からソート方式を変化させる」に対応するには onChange を使用する。

struct SortableTable: View {
    @State private var sortOrder = [KeyPathComparator(\Person.givenName)]

    @State private var people = //略

    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, initial: true) { _, sortOrder in
            people.sort(using: sortOrder)
        }
    }
}
iPhone iPad

iPadの方は列ヘッダーの横にソート状態を表示するものが付いています。

計算型セル

SwiftUIの言い方ではstatic rowsというらしい。
TableColumn では値を設定する代わりに、計算クロージャを設定する。
TableRow でネタを投下する。

struct Purchase: Identifiable {
    let price: Decimal
    let id = UUID()
}

struct TipTable: View {
    let currencyStyle = Decimal.FormatStyle.Currency(code: "USD")

    var body: some View {
        Table(of: Purchase.self) {
            TableColumn("Base price") { purchase in
                Text(purchase.price, format: currencyStyle)
            }
            TableColumn("With 15% tip") { purchase in
                Text(purchase.price * 1.15, format: currencyStyle)
            }
            TableColumn("With 20% tip") { purchase in
                Text(purchase.price * 1.2, format: currencyStyle)
            }
            TableColumn("With 25% tip") { purchase in
                Text(purchase.price * 1.25, format: currencyStyle)
            }
        } rows: {
            TableRow(Purchase(price: 20))
            TableRow(Purchase(price: 50))
            TableRow(Purchase(price: 75))
        }
        .tableStyle(.automatic)
    }
}
iPhone iPad

横方向のサイズクラスがコンパクトのときに対応する

iPhoneをはじめとしてサイズクラスの横がコンパクトの場合(要は横が狭い)はセルを横方向に全部表示しない。また、カラムのヘッダーも表示しない。なので工夫する。下の例では、列がひとつしか表示しないのでひとつの場所に情報詰め込むようにしている。

struct CompactableTable: View {
#if os(iOS)
    @Environment(\.horizontalSizeClass) private var horizontalSizeClass
    private var isCompact: Bool { horizontalSizeClass == .compact }
#else
    private let isCompact = false
#endif

    @State private var sortOrder = [KeyPathComparator(\Person.givenName)]

    @State private var people = //略

    var body: some View {
        Table(people, sortOrder: $sortOrder) {
            TableColumn("Given Name", value: \.givenName) { person in
                VStack(alignment: .leading) {
                    Text(isCompact ? person.fullName : person.givenName)
                    if isCompact {
                        Text(person.emailAddress)
                            .foregroundStyle(.secondary)
                    }
                }
            }
            TableColumn("Family Name", value: \.familyName)
            TableColumn("E-Mail Address", value: \.emailAddress)
        }
        .onChange(of: sortOrder) { _, sortOrder in
            people.sort(using: sortOrder)
        }
    }
}
iPhone iPad

TableColumn

TableColumn の基本要素

  • ヘッダーに書く文字列
  • カラムに表示する内容
  • そのカラムが(ネタに対して)なんであるか
TableColumn("Given name", value: \.givenName) { person in
    Text(person.givenName)
}

上の場合、
ヘッダーに書くのは Given name
表示内容は Text(person.givenName)
そのカラムが(ネタに対して) \.givenName であるとのことであるが、これはソートするときに使われる情報らしい。このカラムをソートするときは \.givenName によってソートされる。

また、多くの場合、表示する内容と(ネタに対して)なんであるかは同一となるので次のコンビニエンスイニシャライザライザが使える。上のものと同一の内容である。

TableColumn("Given name", value: \.givenName)

感想

今までサイズクラスへの対応を全くしたことがない(サイズクラス使用以上に細かく設定する場合など)人でも、Table を使う際は対応が必須になるのでは。

資料

https://developer.apple.com/documentation/swiftui/table/

Discussion