🐙

SwiftUI copyable, cuttable, pasteDestinationの使い方

2023/04/11に公開

copyable, cuttable, pasteDestination

copyable, cuttable, pasteDestinationの3つは、macOS 13.0(Ventura)から利用できるClipboardに関するViewModifierです。

キーボード操作に関わるものなので、iOS/ tvOS/ watchOSなどにはありません。(iPadがあるので、iOSでもそのうち有効になるかも?)

https://developer.apple.com/documentation/swiftui/clipboard?changes=_1

前提知識

https://zenn.dev/zunda_pixel/articles/3e9871a18d3b01

以下も知っておくとよりわかりやすい

https://zenn.dev/zunda_pixel/articles/7dd3a47b0a3998
https://zenn.dev/zunda_pixel/articles/0be43b8b4b36b5

今までにあった機能(古くなったもの)

以下はNSItemProviderを使用することでコピー、カット、ペーストを実現していました。
SwiftUIではNSItemProviderTransferableへ移行しようとする動きがあります。

copyable(_:)

Command-Cが入力された際に、Transferableに準拠しているアイテムの配列を渡します。
基本的には選択された複数のアイテムをコピーします。

.copyable(birds.filter { selection.contains($0.id) })

cuttable(for:action:)

Command-Xが入力された際に、Transferableに準拠しているアイテムの配列を渡します。
基本的には選択された複数のアイテムをカットします。

カットしているので、全体のデータからカットした文を削除しつつ、コピーする必要があります。

.cuttable(for: Bird.self) {
  let birds = birds.filter { selection.contains($0.id) }
  self.birds.removeAll { selection.contains($0.id) }
  return birds
}

pasteDestination(for:action:validator:)

Command-Vが入力された際に、Transferableに準拠しているアイテムの配列を受け取ります。

ここでは文字列を渡された想定です。
渡された文字列を元に、Birdを作成して、データに追加しています。

validatorでは渡されたアイテムで有効なアイテムのみを返す(filter)ようにします。指定しなければ全て利用されます。

// 文字列のコピーをペーストする
.pasteDestination(for: String.self) { names in
  let newBirds: [Bird] = names.map { .init(id: .init(), name: $0) }
  birds.append(contentsOf: newBirds)
} validator: { names in
  // validate(有効)な文字列のみペーストするように
  names.filter { knownBirds.contains($0) }
}

全体のコード

import SwiftUI

struct Bird: Identifiable, Codable, Transferable {
  let id: UUID
  let name: String
  
  static var transferRepresentation: some TransferRepresentation {
    CodableRepresentation(contentType: .data)
  }
}

struct ContentView: View {
  @State private var birds: [Bird] = [
    .init(id: .init(), name: "owl"),
    .init(id: .init(), name: "sparrow"),
    .init(id: .init(), name: "robin")
  ]
  
  @State private var selection: Set<UUID> = []
  
  let knownBirds = [
    "owl",
    "parrot",
    "swift",
    "sparrow",
    "robin",
    "bluebird"
  ]
  
  var body: some View {
    List(birds, selection: $selection) {
      Text($0.name)
        .tag($0.id)
    }
    .copyable(birds.filter { selection.contains($0.id) })
    .cuttable(for: Bird.self) {
      let birds = birds.filter { selection.contains($0.id) }
      self.birds.removeAll { selection.contains($0.id) }
      return birds
    }
    .pasteDestination(for: Bird.self) { birds in
      let newBirds: [Bird] = birds.map { .init(id: .init(), name: $0.name) }
      self.birds.append(contentsOf: newBirds)
    }
    // 文字列のコピーをペーストする
    .pasteDestination(for: String.self) { names in
      let newBirds: [Bird] = names.map { .init(id: .init(), name: $0) }
      birds.append(contentsOf: newBirds)
    } validator: { names in
      // validate(有効)な文字列のみペーストするように
      names.filter { knownBirds.contains($0) }
    }
  }
}

Discussion