Sponsor Link
環境&対象
- macOS Ventura 13
- Xcode 14.1 beta 3
- iOS 16.0
Transferable 概要
Transferable は、Drag&Drop や Copy&Paster で要素を扱うための Protocol です。
Apple のドキュメントは、こちら。
以前は、NSItemProvider を使って実現していましたが、Transferable を使用すると、よりシンプルに記述できるようになります。
NSItemProvider を使った Drag&Drop の実装は以下の記事で解説しています。
[SwiftUI] SwiftUI で実装する Drag&Drop (その1: String を Drag&Drop)
[SwiftUI] SwiftUI で実装する Drag&Drop (その2:クラスオブジェクト を Drag&Drop)
[SwiftUI] SwiftUI Drag&Drop implementation(Vol. 3:use own UTI in Drag&Drop)
String の Drag&Drop
Transfereable を使用して String の Drag&Drop を実装してみます。
Drag&Drop を実装するアプリは、左右にリストがあるアプリとします。左側のリストの要素を右側のリストに D&D すると、要素として追加される という動作です。
struct ContentView: View {
@State private var leftListItems = ["Hello", "world"]
@State private var rightListItems = ["こんにちわ", "世界"]
var body: some View {
HStack {
List {
ForEach(leftListItems, id: \.self) { item in
Text(item)
}
}
List {
ForEach(rightListItems, id: \.self) { item in
Text(item)
}
}
}
.padding()
}
}
以下のように2つのリストがあり、左側のリストの要素を右側のリストに Drag&Drop できるようにします。
draggable
まずは、左側のリストの要素を Drag できるようにします。
View Modifier の .draggable を使用します。draggable の引数には、Transferable を返すような closure を指定します。
Apple のドキュメントは、こちら。
func draggable(_ payload: @autoclosure @escaping () -> T) -> some View where T : Transferable
draggable では、ドラッグ先に渡すデータを返す closure を指定します。渡すデータは、 Transferable に準拠している必要があります。
今回のケースで扱っている String は、すでに Transferable に準拠しているので、そのまま渡しています。
struct ContentView: View {
@State private var leftListItems = ["Hello", "world"]
@State private var rightListItems = ["こんにちわ", "世界"]
var body: some View {
HStack {
List {
ForEach(leftListItems, id: \.self) { item in
Text(item)
.draggable(item) // 🆕 追加
}
}
List {
ForEach(rightListItems, id: \.self) { item in
Text(item)
}
}
}
.padding()
}
}
上記のようにすることで、左側のリストの要素をドラッグできるようになります。
この状態で 例えば、メモ帳にドラッグすると、ドラッグした文字列がメモ帳に貼り付けられます。
dropDestination
次に、右側のリストが Drop を受け付けられるようにします。
Drop を受け付けられるように、List の View Modifier の dropDestination を使用します。
Apple のドキュメントは、こちら。
func dropDestination<T>(
for payloadType: T.Type = T.self,
action: @escaping ([T], CGPoint) -> Bool,
isTargeted: @escaping (Bool) -> Void = { _ in }
) -> some View where T : Transferable
1つ目の引数として受け付けるデータを指定して、2つ目の引数は、ドロップされたときの動作を記述します。
2つめの引数である closure は、データ受け付けができた場合は、true を、データを受け付けられなかった時には、 false を返すことが必要です。
struct ContentView: View {
@State private var leftListItems = ["Hello", "world"]
@State private var rightListItems = ["こんにちわ", "世界"]
var body: some View {
HStack {
List {
ForEach(leftListItems, id: \.self) { item in
Text(item)
.draggable(item)
}
}
List {
ForEach(rightListItems, id: \.self) { item in
Text(item)
}
}
.dropDestination(for: String.self) { items in // 🆕 追加
rightListItems.append(contentsOf: items)
return true
}
}
.padding()
}
}
上記のようにすることで、リストが String のデータをもつような Drop を受け付けるようになります。
ここでは、受け付けた要素を 右側のリストに追加しています。常に受け付けは成功していますので、true を返しています。
Transferable 詳細
String の例は、String が Transferable に準拠していることを利用して動作しています。
Transferable に準拠するとはどのようなことかを確認してみます。
Transferable protocol
Transferable は、iOS16/ macOS 13 で新しく導入された Protocol です。
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
public protocol Transferable {
associatedtype Representation : TransferRepresentation
@TransferRepresentationBuilder<Self> static var transferRepresentation: Self.Representation { get }
}
Apple のドキュメントは、こちら。
ある型が Transferable に準拠するには、static な (get できる) transferRepresentation を定義することが必要です。
TransferRepresentation も、iOS 16/macOS 13 で導入された protocol です。準拠することで、データをやり取りする際にデータ変換できる型とその変換方法を定義します。
Apple のドキュメントは、こちら。
この TransferRepresentation を使うことで、複数の表現方法をサポートすることができます。
独自型を Transferable に準拠する
実際に、型を Tranferable に準拠させて、 Drag&Drop で使ってみます。
独自型の定義
準拠させる型は、以下の型とします。Color と String を持つ struct です。
struct NamedColor {
let name: String
let color: Color
}
struct を直接渡せそうであれば、そのまま渡します。それ以外のケースとして、Color を受け取れそうであれば Color を、String を受け取れそうであれば、String を渡すようにしてみます。
Transferable に準拠
Transferable に準拠させるということは、以下を定義するということです。
static var transferRepresentation: some TransferRepresentation
このプロパティが、実際に渡されるデータを定義しています。
定義を辿ってみると TransferRepresentationBuilder という名前の Property Wrapper が付与されていることからも、この属性も Builder 系の属性です。
つまり、複数の定義を同梱できる属性です。ですが、まずは、1つの表現だけにしておきます。
TransferRepresentation 定義
Codable な型であれば、Codable であることを利用して、以下のように書けます。
extension NamedColor: Transferable {
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(for: NamedColor.self, contentType: .namedColor)
}
}
つまり、先に定義した NamedColor を Codable に準拠させるようにします。
自前の既存コードを使って、以下のように Codable に準拠させました。
extension NamedColor: Codable {
private enum CodingKeys: CodingKey {
case name, rgbString
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
let rgbStr = color.rgbValueStr
try container.encode(rgbStr, forKey: .rgbString)
}
public init(from decoder: Decoder) throws {
let colorContainer = try decoder.container(keyedBy: CodingKeys.self)
self.name = try colorContainer.decode(String.self, forKey: .name)
let rgbStr = try colorContainer.decode(String.self, forKey: .rgbString)
guard let color = Color(hexStr: rgbStr) else { fatalError("invalid color")}
self.color = color
}
}
String は、それ単体で Codable なのですが、Color は、Codable に準拠していません。
上記では、Color から RGB を使った文字列表現に変換して保存するようにしています。
ここでは、NamedColor を Codable に準拠させることがポイントです。どのように準拠させるかは本質ではありません。
UTType
Drag&Drop できるようにするということは、アプリ内で閉じる使い方だけではなく、別アプリとのやり取りにも使用されるかもしれません。
macOS では、UniformTypeIdentifier というものを使って、受け渡し含めデータを管理しています。
独自型についても、UniformTypeIdentifier を定義することが必要となります。
今回は、com.smalldesksoftware.example.namedcolor という識別子の UTType を定義しました。
Info.plist にも同様に登録することが必要です。
extension UTType {
static var namedColor = UTType(exportedAs: "com.smalldesksoftware.example.namedcolor")
}
UTType の扱いについては、以下の記事で説明しています。
[Swift][macOS][iOS]UTType の登録方法
独自型を Drag&Drop する
NamedColor を Transferable に準拠させることができたので、Drag&Drop できるようアプリを作ります。
左側に、3つの NamedColor を配置して、右側には受ける側の要素を配置しました。
以下がコードです。
struct ContentView: View {
@State private var colorName: String = "NoColor"
@State private var color: Color = .clear
var body: some View {
HStack {
VStack {
Color.blue.frame(width: 100, height: 100)
Color.yellow.frame(width: 100, height: 100)
Color.red.frame(width: 100, height: 100)
}
VStack {
Text(colorName)
.frame(width: 100, height: 100)
.background {
Rectangle()
.fill(color)
.frame(width: 100, height: 100)
.border(.black)
}
}
}
}
}
左側に配置された色の四角を、右側にドロップすることで、ドロップされた Color と String で、変数 color と colorName を更新するイメージです。
独自型を Drag できるようにする
最初の例では、String を Drag できるようにしたので、String を直接 draggable に渡していました。先ほど NamedColor もTransferable に準拠させましたので、NamedColor も draggable に直接渡せるようになっています。ということで、以下のようなコードになります。
struct ContentView: View {
@State private var colorName: String = "NoColor"
@State private var color: Color = .clear
var body: some View {
HStack {
VStack {
Color.blue.frame(width: 100, height: 100)
.draggable(NamedColor(name: "blue", color: .blue)) // 🆕
Color.yellow.frame(width: 100, height: 100)
.draggable(NamedColor(name: "yellow", color: .yellow)) // 🆕
Color.red.frame(width: 100, height: 100)
.draggable(NamedColor(name: "red", color: .red)) // 🆕
}
VStack {
Text(colorName)
.frame(width: 100, height: 100)
.background {
Rectangle()
.fill(color)
.frame(width: 100, height: 100)
.border(.black)
}
}
}
}
}
それぞれの色の四角を Drag するときに、それぞれに応じた NamedColor を渡しています。
独自型の Drop を受け取る
dropDestination を使用するところは、String の時と同じです。
struct ContentView: View {
@State private var colorName: String = "NoColor"
@State private var color: Color = .clear
@State private var colorNameOnly: String = "NoColor"
var body: some View {
HStack {
VStack {
Color.blue.frame(width: 100, height: 100)
.draggable(NamedColor(name: "blue", color: .blue))
Color.yellow.frame(width: 100, height: 100)
.draggable(NamedColor(name: "yellow", color: .yellow))
Color.red.frame(width: 100, height: 100)
.draggable(NamedColor(name: "red", color: .red))
}
VStack {
Text(colorName)
.frame(width: 100, height: 100)
.background {
Rectangle()
.fill(color)
.frame(width: 100, height: 100)
.border(.black)
}
.dropDestination(for: NamedColor.self) { items, location in // 🆕
guard let item = items.first else { return false }
color = item.color
colorName = item.name
return true
}
}
}
}
}
for: の引数に受け取る型を指定して、closure では、受け取った NamedColor を color と colorName にセットしています。
実装した動作
以下のような動作になります。
複数の型に対応した Drag&Drop
NamedColor を Drag&Drop できるようにしましたが、NamedColor は、自分で作った型なので、受け取れるアプリは存在しません。受け取り側が、String だけであれば 受け取れるかもしれませんし、Color だけであれば 受け取れるかもしれません。Drag&Drop では、データを出す側は、さまざまなデータを用意しておくだけで、最終的にどのデータを受け取るかは、Drop される側の判断です。
Transferable 準拠にしていると、そのようなケースにも対応して、複数の形式で、Drag&Drop 対応させることができます。
# NSItemProvider でもできます。
String として Drag&Drop
まずは、文字列として、受け取るケースを実装します。
Drag 側は、同じです。NamedColor を渡します。
調整する箇所は、以下です。
・transferRepresentation の定義
・受け取る側(dropDestination)
transferRepresentation の定義を以下のように追加することで、String (name プロパティ)を渡すこと”も” できるようになります。
extension NamedColor: Transferable {
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(for: NamedColor.self, contentType: .namedColor)
ProxyRepresentation(exporting: \.name) // 🆕
}
}
transferRepresentation に記載する順序は重要です。
上から順番にチェックされ、適合するデータが Drag&Drop に使用されます。
サンプルアプリには、受け取る側のView を追加しました。
String を受け取るので、Text を使った表示を変更しています。
struct ContentView: View {
@State private var colorName: String = "NoColor"
@State private var color: Color = .clear
@State private var colorNameOnly: String = "NoColor"
var body: some View {
HStack {
VStack {
Color.blue.frame(width: 100, height: 100)
.draggable(NamedColor(name: "blue", color: .blue))
Color.yellow.frame(width: 100, height: 100)
.draggable(NamedColor(name: "yellow", color: .yellow))
Color.red.frame(width: 100, height: 100)
.draggable(NamedColor(name: "red", color: .red))
}
VStack {
Text(colorName)
.frame(width: 100, height: 100)
.background {
Rectangle()
.fill(color)
.frame(width: 100, height: 100)
.border(.black)
}
.dropDestination(for: NamedColor.self) { items, location in
guard let item = items.first else { return false }
color = item.color
colorName = item.name
return true
}
Text("dropped \(colorNameOnly) as String") // 🆕
.frame(width: 200, height: 50)
.border(.black)
.dropDestination(for: String.self) { items, location in
guard let item = items.first else { return false }
colorNameOnly = item
return true
}
}
}
}
}
以下のような動作になります。
Color として Drag&Drop
最後に、Color だけを 受け取るケースを実装してみます。
Drag 側は、同じです。NamedColor を渡します。
調整する箇所は、以下です。
・transferRepresentation の定義
・受け取る側(dropDestination)
transferRepresentation の定義を以下のように追加することで、Color (color プロパティ)を渡すこと”も” できるようになります。
extension NamedColor: Transferable {
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(for: NamedColor.self, contentType: .namedColor)
ProxyRepresentation(exporting: \.name)
ProxyRepresentation(exporting: \.color)
}
}
サンプルアプリには、受け取る側のView を追加しました。
Color を受け取るので、新しくColor 表示を追加して、受け取った Color で更新するようにしました。
struct ContentView: View {
@State private var colorName: String = "NoColor"
@State private var color: Color = .clear
@State private var colorNameOnly: String = "NoColor"
@State private var colorOnly: Color = .clear
var body: some View {
HStack {
VStack {
Color.blue.frame(width: 100, height: 100)
.draggable(NamedColor(name: "blue", color: .blue))
Color.yellow.frame(width: 100, height: 100)
.draggable(NamedColor(name: "yellow", color: .yellow))
Color.red.frame(width: 100, height: 100)
.draggable(NamedColor(name: "red", color: .red))
}
VStack {
Text(colorName)
.frame(width: 100, height: 100)
.background {
Rectangle()
.fill(color)
.frame(width: 100, height: 100)
.border(.black)
}
.dropDestination(for: NamedColor.self) { items, location in
guard let item = items.first else { return false }
color = item.color
colorName = item.name
return true
}
Text("dropped \(colorNameOnly) as String")
.frame(width: 200, height: 50)
.border(.black)
.dropDestination(for: String.self) { items, location in
guard let item = items.first else { return false }
colorNameOnly = item
return true
}
colorOnly.frame(width: 100, height: 100).border(.black)
.dropDestination(for: Color.self) { items, location in
guard let item = items.first else { return false }
colorOnly = item
return true
}
}
}
}
}
以下のような動作になります。
まとめ
Transferable を使った Drag & Drop を実装しました
- 渡したい型を Transferable に準拠させる
- static var transferRepresentation として、渡したい型を定義する
- Codable に準拠させておくと簡単
- UTType の登録は必要
- 必要に応じて、複数のデータ型に対応しておくと便利
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
SwiftUI おすすめ本
SwiftUI を理解するには、以下の本がおすすめです。
SwiftUI ViewMatery
SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。
英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。
View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。
超便利です
販売元のページは、こちらです。
SwiftUI 徹底入門
# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。
Swift学習におすすめの本
詳解Swift
Swift の学習には、詳解 Swift という書籍が、おすすめです。
著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。
最新版を購入するのがおすすめです。
現時点では、上記の Swift 5 に対応した第5版が最新版です。
Sponsor Link