feed

2024年02月17日, 編集履歴

SwiftUI Tableの使い方

以下のような構造体の配列をSwiftUIのTableビューに表示する場合を考える。

struct Bookmark {
  var title: String
  var url: URL
}

単純に表形式で表示する

データを単純にTableに表示したいだけの場合、Tableinit<Data>(Data, columns: () -> Columns)と、TableColumninit(Text, content: (RowValue) -> Content)あたりを使う。このとき、表示するデータ構造はIdentifiableプロトコルに適合させる必要があるので、プロパティにidを追加した。

import SwiftUI

struct Bookmark: Identifiable {

  var id = UUID()
  var title: String
  var url: URL

}

struct ContentView: View {

  @State var sampleData: [Bookmark] = [
    Bookmark(title: "Genji App", url: URL(string: "https://genjiapp.com/")!),
    Bookmark(title: "Apple", url: URL(string: "https://www.apple.com/")!),
    Bookmark(title: "Google", url: URL(string: "https://www.google.com/")!)
  ]

  var body: some View {
    Table(sampleData) {
      TableColumn("Title") { bookmark in
        Text(bookmark.title)
      }
      TableColumn("URL") { bookmark in
        Text(bookmark.url.absoluteString)
      }
    }
    .tableStyle(.bordered)
    .padding()
  }

}

行を選択可能にする

テーブル行を選択可能にする場合、

あたりを使う。selection引数の型がIDのオプショナルか、IDSet型かの違い。

struct ContentView: View {

  @State var sampleData: [Bookmark] = [
    Bookmark(title: "Genji App", url: URL(string: "https://genjiapp.com/")!),
    Bookmark(title: "Apple", url: URL(string: "https://www.apple.com/")!),
    Bookmark(title: "Google", url: URL(string: "https://www.google.com/")!)
  ]
  @State private var selectedID: Bookmark.ID?

  var body: some View {
    Table(sampleData, selection: $selectedID) {
      TableColumn("Title") { bookmark in
        Text(bookmark.title)
      }
      TableColumn("URL") { bookmark in
        Text(bookmark.url.absoluteString)
      }
    }
    .tableStyle(.bordered)
    .padding()
  }
}

行クリック時に選択可能になった。

ドラッグ&ドロップで並べ替え

テーブル行をドラッグ&ドロップで並べ替え可能にするには、init(of: Value.Type, selection: Binding<Set<Value.ID>>, columns: () -> Columns, rows: () -> Rows)あたりを使う。先ほどまでのイニシャライザとは使い方がちょっと違う。第一引数に表示するデータの型、最後のrows引数にはTableRowで表示データを供給する。以下のような感じ。

struct ContentView: View {

  @State var sampleData: [Bookmark] = [
    Bookmark(title: "Genji App", url: URL(string: "https://genjiapp.com/")!),
    Bookmark(title: "Apple", url: URL(string: "https://www.apple.com/")!),
    Bookmark(title: "Google", url: URL(string: "https://www.google.com/")!)
  ]
  @State private var selectedID: Bookmark.ID?

  var body: some View {
    Table(of: Bookmark.self, selection: $selectedID) {
      TableColumn("Title") { bookmark in
        Text(bookmark.title)
      }
      TableColumn("URL") { bookmark in
        Text(bookmark.url.absoluteString)
      }
    } rows: {
      ForEach(sampleData) { bookmark in
        TableRow(bookmark)
      }
    }
    .tableStyle(.bordered)
    .padding()
  }

}

ここまでは、まだデータの表示と行選択ができるだけで、前項までとは別のイニシャライザを使ったに過ぎない。行をドラッグ&ドロップできるようにするには、draggabledropDestinationモディファイアを使う。この時、表示データはTransferableプロトコルに適合させる必要があり、そのためにはCodableプロトコルにも適合させておくとかんたんになる。また、ドラッグ&ドロップさせるデータのためのカスタムUTTypeも用意する。以下のような感じ。

import SwiftUI
import UniformTypeIdentifiers

extension UTType {
  static let bookmark = UTType(exportedAs: "com.genjiapp.TableSample.bookmark")
}

struct Bookmark: Identifiable, Codable, Transferable {

  static var transferRepresentation: some TransferRepresentation {
    CodableRepresentation(contentType: .bookmark)
  }

  var id = UUID()
  var title: String
  var url: URL

}

struct ContentView: View {

  @State var sampleData: [Bookmark] = [
    Bookmark(title: "Genji App", url: URL(string: "https://genjiapp.com/")!),
    Bookmark(title: "Apple", url: URL(string: "https://www.apple.com/")!),
    Bookmark(title: "Google", url: URL(string: "https://www.google.com/")!)
  ]
  @State private var selectedID: Bookmark.ID?

  var body: some View {
    Table(of: Bookmark.self, selection: $selectedID) {
      TableColumn("Title") { bookmark in
        Text(bookmark.title)
      }
      TableColumn("URL") { bookmark in
        Text(bookmark.url.absoluteString)
      }
    } rows: {
      ForEach(sampleData) { bookmark in
        TableRow(bookmark)
          .draggable(bookmark)
      }
      .dropDestination(for: Bookmark.self) { insertionIndex, insertionBookmarks in
        if let bookmark = insertionBookmarks.first,
           let originalIndex = sampleData.firstIndex(where: { $0.id == bookmark.id }) {
          sampleData.insert(bookmark, at: insertionIndex)
          let removeIndex = (insertionIndex > originalIndex) ? originalIndex : originalIndex + 1
          sampleData.remove(at: removeIndex)
        }
      }
    }
    .tableStyle(.bordered)
    .padding()
  }

}

draggableTableRowに、dropDestinationTableRowを囲むForEachに対して指定する。

また、用意したカスタムUTTypeは、その識別子をInfo.plist内で宣言しておかなければならない。