axiom-transferable-ref について
CoreTransferable と SwiftUI の共有機能を扱う Swift 開発者向けリファレンススキルです。ドラッグ&ドロップ、クリップボード、ShareLink に加え、独自型や UTType、転送表現の選び方を解説します。
主な機能
- 01ドラッグ&ドロップの実装
- 02コピー&ペーストの追加
- 03ShareLink によるコンテンツ共有
- 04独自型の転送表現の設定
- 05独自 UTType の宣言
活用シーン
- 01SwiftUI のモデルをドラッグ可能にする
- 02ビューでドロップファイルを受け付ける
- 03独自コンテンツにコピー&ペーストを追加
- 04共有アイテムのプレビューを設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-transferable-refインストール後、Claude Code で/axiom-transferable-refを入力して読み込まれたか確認します。
詳細
転送可能なコンテンツとコンテンツ共有のリファレンス
CoreTransferable フレームワークと SwiftUI の共有機能(ドラッグ&ドロップ、コピー/ペースト、ShareLink)に関する包括的なガイドです。
このスキルを使用する場面
- ドラッグ&ドロップを実装する(
.draggable、.dropDestination) - コピー/ペーストのサポートを追加する(
.copyable、.pasteDestination、PasteButton) ShareLinkを使ってコンテンツを共有する- カスタム型を転送可能にする
- アプリ固有の形式に対応するカスタム UTType を宣言する
Transferable型を UIKit のNSItemProviderと連携させるCodableRepresentation、DataRepresentation、FileRepresentation、ProxyRepresentationのいずれを使うか選択する
プロンプトの例
「SwiftUI でモデルをドラッグ可能にするにはどうすればよいですか?」 「ShareLink にカスタムプレビューが表示されません」 「ビューでドロップされたファイルを受け取るにはどうすればよいですか?」 「DataRepresentation と FileRepresentation の違いは何ですか?」 「カスタム型にコピー/ペーストのサポートを追加するにはどうすればよいですか?」 「アプリ内ではドラッグ&ドロップが動作しますが、アプリ間では動作しません」 「カスタム UTType を宣言するにはどうすればよいですか?」
パート 1:クイックリファレンス
判断フローチャート:どの TransferRepresentation を使うか
Your model type...
├─ Conforms to Codable + no specific binary format needed?
│ → CodableRepresentation
├─ Has custom binary format (Data in memory)?
│ → DataRepresentation (exporting/importing closures)
├─ Lives on disk (large files, videos, documents)?
│ → FileRepresentation (passes file URLs, not bytes)
├─ Need a fallback for receivers that don't understand your type?
│ → Add ProxyRepresentation (e.g., export as String or URL)
└─ Need to conditionally hide a representation?
→ Apply .exportingCondition to any representation
よくあるエラー
| エラー/症状 | 原因 | 対処方法 |
|---|---|---|
| "Type does not conform to Transferable" | transferRepresentation がありません | static var transferRepresentation: some TransferRepresentation を追加します |
| アプリ内ではドロップできますが、アプリ間ではできません | カスタム UTType が Info.plist で宣言されていません | UTExportedTypeDeclarations エントリを追加します |
| 受信側でリッチな型ではなく、常にプレーンテキストが取得されます | ProxyRepresentation が CodableRepresentation より前に記述されています | 順序を変更し、最もリッチな表現を先にします |
| FileRepresentation で "file not found" が発生してクラッシュします | サンドボックス拡張の有効期限が切れる前に、受信側でファイルがコピーされませんでした | インポート用クロージャーでアプリのストレージにコピーします |
| PasteButton が常に無効です | ペーストボードに一致する Transferable 型が含まれていません | UTType への準拠を確認し、ペーストされたデータが一致することを確認します |
| ShareLink に汎用プレビューが表示されます | SharePreview が指定されていないか、画像が Transferable ではありません | タイトルと画像を含む SharePreview を明示的に指定します |
.dropDestination のクロージャーが一度も呼び出されません | ペイロードの型が誤っているか、ビューのヒットテスト領域がゼロです | for: の型がドラッグされたコンテンツと一致することを確認し、.frame() または .contentShape() を追加します |
組み込みの Transferable 型
これらは追加のコードなしで使用できます。準拠を追加する必要はありません。
String、Data、URL、AttributedString、Image、Color
パート 2:型を転送可能にする
Transferable プロトコルの要件は、静的な transferRepresentation プロパティ 1 つです。
CodableRepresentation
最適な用途:すでに Codable に準拠しているモデル。デフォルトでは JSON を使用します。
import UniformTypeIdentifiers
extension UTType {
static var todo: UTType = UTType(exportedAs: "com.example.todo")
}
struct Todo: Codable, Transferable {
var text: String
var isDone: Bool
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .todo)
}
}
カスタムのエンコーダー/デコーダー(例:JSON の代わりに PropertyList を使用):
CodableRepresentation(
contentType: .todo,
encoder: PropertyListEncoder(),
decoder: PropertyListDecoder()
)
要件:カスタム UTType には、Info.plist に対応する UTExportedTypeDeclarations が必要です(パート 4 を参照)。
DataRepresentation
最適な用途:データがメモリ上にあり、シリアライズを制御できるカスタムのバイナリ形式。
struct ProfilesArchive: Transferable {
var profiles: [Profile]
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(contentType: .commaSeparatedText) { archive in
try archive.toCSV()
} importing: { data in
try ProfilesArchive(csvData: data)
}
}
}
インポート専用またはエクスポート専用のバリエーション:
// Import only
DataRepresentation(importedContentType: .png) { data in
try MyImage(pngData: data)
}
// Export only
DataRepresentation(exportedContentType: .png) { image in
try image.pngData()
}
コンテンツタイプとして UTType.data を使用するのは避けてください。.png、.pdf、.commaSeparatedText のような具体的な型を使用してください。
FileRepresentation
最適な用途:ディスク上の大きなペイロード(動画、ドキュメント、アーカイブ)。バイト列をメモリに読み込まず、ファイル URL を渡します。
struct Video: Transferable {
let file: URL
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(contentType: .mpeg4Movie) { video in
SentTransferredFile(video.file)
} importing: { received in
// MUST copy — sandbox extension is temporary
let dest = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("mp4")
try FileManager.default.copyItem(at: received.file, to: dest)
return Video(file: dest)
}
}
}
重要:received.file の URL には一時的なサンドボックス拡張が付与されています。インポートクロージャー内でファイルを自身のストレージにコピーしてください。クロージャーが戻ると、URL にアクセスできなくなります。
SentTransferredFile のプロパティ:
file: URL— ファイルの場所allowAccessingOriginalFile: Bool—false(デフォルト)の場合、受信側にはコピーが渡されます
ReceivedTransferredFile のプロパティ:
file: URL— ディスク上の受信ファイルisOriginalFile: Bool— 送信側の元のファイルかコピーかを示します
コンテンツタイプの精度:.mpeg4Movie は .mp4 ファイルにのみ一致します(.mp4、.mov、.m4v など)一般的なすべての動画形式を受け入れるには、親タイプの .movie を使用するか、特定のサブタイプに対して複数の FileRepresentation を宣言してください。
// Broad: accept any video format the system recognizes
FileRepresentation(contentType: .movie) { ... } importing: { ... }
// Or specific: separate handlers per format
FileRepresentation(contentType: .mpeg4Movie) { ... } importing: { ... }
FileRepresentation(contentType: .quickTimeMovie) { ... } importing: { ... }
インポート専用:型がファイルを受信するだけの場合(ドロップ先で、エクスポートはしない場合)は、インポート専用のイニシャライザーを使用してください。意図が明確になり、誤ってエクスポートするのを防げます。
FileRepresentation(importedContentType: .movie) { received in
let dest = appStorageURL.appendingPathComponent(received.file.lastPathComponent)
try FileManager.default.copyItem(at: received.file, to: dest)
return VideoClip(localURL: dest)
}
ProxyRepresentation
最適な用途:より単純な型を想定する受信側でも使えるようにする、フォールバック表現。
struct Profile: Transferable {
var name: String
var avatar: Image
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .profile)
ProxyRepresentation(exporting: \.name) // Fallback: paste as text
}
}
エクスポート専用のプロキシ(一般的なパターン。逆変換が不可能な場合が多いため):
ProxyRepresentation(exporting: \.name) // Profile → String (one-way)
双方向プロキシ(逆変換が妥当な場合):
ProxyRepresentation { item in
item.name // export
} importing: { name in
Profile(name: name) // import
}
複数の表現の組み合わせ
transferRepresentation 本体に表現を列挙します。順序が重要です。受信側は、自身がサポートする最初の表現を使用します。
struct Profile: Transferable {
static var transferRepresentation: some TransferRepresentation {
// 1. Richest: full profile data (apps that understand .profile)
CodableRepresentation(contentType: .profile)
// 2. Fallback: plain text (text fields, notes, any app)
ProxyRepresentation(exporting: \.name)
}
}
よくある間違い:ProxyRepresentation を先頭に置くと、両方をサポートする受信側には、常に劣化したバージョンが渡されます。
条件付きエクスポート
条件を満たさない場合に、実行時に表現を非表示にします。
DataRepresentation(contentType: .commaSeparatedText) { archive in
try archive.toCSV()
} importing: { data in
try Self(csvData: data)
}
.exportingCondition { archive in
archive.supportsCSV
}
可視性
どのプロセスから表現を参照できるかを制御します。
CodableRepresentation(contentType: .profile)
.visibility(.ownProcess) // Only within this app
オプション:.all(デフォルト)、.team(同じ開発者チーム)、.group(同じ App Group、macOS)、.ownProcess(同じアプリのみ)
推奨ファイル名
ディスクに書き込む受信側へのヒント:
FileRepresentation(contentType: .mpeg4Movie) { video in
SentTransferredFile(video.file)
} importing: { received in
// ...
}
.suggestedFileName("My Video.mp4")
// Or dynamic:
.suggestedFileName { video in video.title + ".mp4" }
パート 3:SwiftUI のサーフェス
ShareLink
標準の共有エントリーポイントです。任意の Transferable 型を受け付けます。
// Simple: share a string
ShareLink(item: "Check out this app!")
// With preview
ShareLink(
item: photo,
preview: SharePreview(photo.caption, image: photo.image)
)
// Share a URL with custom preview (prevents system metadata fetch)
ShareLink(
item: URL(string: "https://example.com")!,
preview: SharePreview("My Site", image: Image("hero"))
)
アイテムごとにプレビューを指定して複数のアイテムを共有する場合:
ShareLink(items: photos) { photo in
SharePreview(photo.caption, image: photo.image)
}
SharePreview のイニシャライザー:
SharePreview("Title")— テキストのみSharePreview("Title", image: someImage)— テキスト+フルサイズ画像SharePreview("Title", icon: someIcon)— テキスト+サムネイルアイコンSharePreview("Title", image: someImage, icon: someIcon)— 3 つすべて
注意点:カスタム型の SharePreview を省略すると、共有シートには汎用プレビューが表示されます。複雑な型には必ず指定してください。
ドラッグ&ドロップ
ビューをドラッグ可能にする:
Text(profile.name)
.draggable(profile)
カスタムのドラッグプレビューを使用する場合:
Text(profile.name)
.draggable(profile) {
Label(profile.name, systemImage: "person")
.padding()
.background(.regularMaterial)
}
ドロップを受け付ける:
Color.clear
.frame(width: 200, height: 200)
.dropDestination(for: Profile.self) { profiles, location in
guard let profile = profiles.first else { return false }
self.droppedProfile = profile
return true
} isTargeted: { isTargeted in
self.isDropTargeted = isTargeted
}
複数のアイテム型 — .dropDestination モディファイアを重ねるのではなく、Transferable に準拠する enum ラッパーを使用してください(重ねると、最も外側のハンドラーしか呼び出されない場合があります)。
enum DroppableItem: Transferable {
case image(Image)
case text(String)
static var transferRepresentation: some TransferRepresentation {
ProxyRepresentation { (image: Image) in DroppableItem.image(image) }
ProxyRepresentation { (text: String) in DroppableItem.text(text) }
}
}
myView
.dropDestination(for: DroppableItem.self) { items, _ in
for item in items {
switch item {
case .image(let img): handleImage(img)
case .text(let str): handleString(str)
}
}
return true
}
並べ替えを行う ForEach — .onMove と組み合わせるか、コンテナ間の移動には draggable/dropDestination を使用します。
クリップボード(コピー/ペースト)
コピーのサポート(編集 > コピー/Cmd+C を有効にします):
List(items) { item in
Text(item.name)
}
.copyable(items)
ペーストのサポート(編集 > ペースト/Cmd+V を有効にします):
List(items) { item in
Text(item.name)
}
.pasteDestination(for: Item.self) { pasted in
items.append(contentsOf: pasted)
} validator: { candidates in
candidates.filter { $0.isValid }
}
バリデータークロージャーはアクションの前に実行されます。ペーストを防ぐには、空の配列を返します。
カットのサポート:
.cuttable(for: Item.self) {
let selected = items.filter { $0.isSelected }
items.removeAll { $0.isSelected }
return selected
}
PasteButton — 型のフィルタリングを行ってペーストを処理するシステムボタン:
PasteButton(payloadType: String.self) { strings in
notes.append(contentsOf: strings)
}
プラットフォームによる違い:PasteButton は iOS ではペーストボードの変更を自動的に検証しますが、macOS では検証しません。
利用可能なプラットフォーム:.copyable、.pasteDestination、.cuttable は macOS 13 以降のみで利用できます。iOS には存在しません。iOS では、ペーストには PasteButton(iOS 16 以降)を使用し、プログラムによるコピー/カットには標準のコンテキストメニューまたは UIPasteboard を使用してください。PasteButton はクロスプラットフォームで、macOS 10.15 以降、iOS 16 以降、visionOS 1.0 以降で利用できます。
パート 4:UTType の宣言
システム型
可能な場合は、Apple に組み込まれている UTType を使用してください。システム全体ですでに認識されています。
import UniformTypeIdentifiers
// Common types
UTType.plainText // public.plain-text
UTType.utf8PlainText // public.utf8-plain-text
UTType.json // public.json
UTType.png // public.png
UTType.jpeg // public.jpeg
UTType.pdf // com.adobe.pdf
UTType.mpeg4Movie // public.mpeg-4
UTType.commaSeparatedText // public.comma-separated-values-text
カスタム型の宣言
手順 1:Swift で宣言します。
extension UTType {
static var recipe: UTType = UTType(exportedAs: "com.myapp.recipe")
}
手順 2:UTExportedTypeDeclarations の下に Info.plist を追加します。
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>com.myapp.recipe</string>
<key>UTTypeDescription</key>
<string>Recipe</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>recipe</string>
</array>
</dict>
</dict>
</array>
両方が必要です。 Swift の宣言だけでもコンパイルはできますが、Info.plist のエントリーがないと、アプリ間の転送はエラーも表示されずに失敗します。
インポート型とエクスポート型
- エクスポート型(
exportedAs:)— この型はアプリが所有します。アプリ固有の形式に使用します。 - インポート型(
importedAs:)— この型は別のアプリが所有します。そのアプリの形式を受け入れる場合に使用します。
UTType の適合性
幅広い互換性を確保するため、カスタム型はシステム型に適合させる必要があります。
// Your .recipe conforms to public.data (binary data)
// This means any receiver that accepts generic data can also accept recipes
一般的な適合先の親型:public.data、public.content、public.text、public.image
パート 5:UIKit とのブリッジ
NSItemProvider と Transferable
UIKit の NSItemProvider(UIActivityViewController、エクステンション、ドラッグセッションで使用)と Transferable を連携させます。
// Load a Transferable from an NSItemProvider
let provider: NSItemProvider = // from drag session, extension, etc.
provider.loadTransferable(type: Profile.self) { result in
switch result {
case .success(let profile):
// Use the profile
case .failure(let error):
// Handle error
}
}
UIActivityViewController を使用する場合
ほとんどの共有ニーズには ShareLink で対応できます。次の場合は UIActivityViewController を使用してください。
- カスタムのアクティビティ項目や、除外するアクティビティ型が必要な場合
- 項目を遅延して提供するために
UIActivityItemsConfigurationを使用する場合 - カスタムの
UIActivityサブクラスが必要な場合 - 表示をプログラムで制御する場合
struct ShareSheet: UIViewControllerRepresentable {
let items: [Any]
func makeUIViewController(context: Context) -> UIActivityViewController {
UIActivityViewController(activityItems: items, applicationActivities: nil)
}
func updateUIViewController(_ vc: UIActivityViewController, context: Context) {}
}
ほとんどのアプリでは ShareLink で十分であり、推奨されます。Transferable とネイティブに連携します。
パート 6:注意点とトラブルシューティング
FileRepresentation の一時ファイルのライフサイクル
FileRepresentation のインポートクロージャー内にある received.file URL には、一時的なサンドボックス拡張が適用されます。クロージャーの戻り後、システムによってアクセスが取り消される可能性があります。必ずファイルをコピーしてください。
// WRONG — file may become inaccessible
return Video(file: received.file)
// RIGHT — copy to your own storage
let dest = myAppDirectory.appendingPathComponent(received.file.lastPathComponent)
try FileManager.default.copyItem(at: received.file, to: dest)
return Video(file: dest)
ファイルのドロップ後の非同期処理
FileRepresentation のインポートクロージャーは同期処理のため、その中で await を使用できません。まずファイルをコピーしてモデルを返し、その後、コピーした URL を使ってサムネイル生成、トランスコード、メタデータ抽出などの非同期後処理を行います。
// WRONG — can't await in the importing closure
FileRepresentation(importedContentType: .movie) { received in
let dest = ...
try FileManager.default.copyItem(at: received.file, to: dest)
let thumbnail = await generateThumbnail(for: dest) // ❌ compile error
return VideoClip(localURL: dest, thumbnail: thumbnail)
}
// RIGHT — return immediately, process async afterward
// In your view model or drop handler:
.dropDestination(for: VideoClip.self) { clips, _ in
for clip in clips {
timeline.append(clip)
Task {
// clip.localURL is the COPY — safe to access anytime
let thumbnail = await generateThumbnail(for: clip.localURL)
clip.thumbnail = thumbnail
}
}
return true
}
表現の順序
表現は宣言された順序で試されます。受信側は、サポートする最初の表現を使用します。
// WRONG — receivers always get plain text
static var transferRepresentation: some TransferRepresentation {
ProxyRepresentation(exporting: \.name) // ← every receiver supports String
CodableRepresentation(contentType: .profile) // ← never reached
}
// RIGHT — richest first, fallbacks last
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .profile) // ← apps that understand Profile
ProxyRepresentation(exporting: \.name) // ← fallback for everyone else
}
Info.plist にないカスタム UTType
Swift で UTType(exportedAs: "com.myapp.type") を宣言しても、Info.plist のエントリーを忘れた場合:
- アプリ内の転送は機能します(同じプロセス内では型が認識されるため)
- アプリ間の転送はエラーも表示されずに失敗します(ほかのアプリが型を解決できないため)
これは「開発中は動くのに、本番環境では失敗する」という最もよくある問題です。
ドロップ先のヒットテスト
ヒットテストを行うには、.dropDestination を設定したビューにゼロ以外のフレームが必要です。ドロップが認識されない場合:
// WRONG — Color.clear has zero intrinsic size
Color.clear
.dropDestination(for: Image.self) { ... }
// RIGHT — give it a frame
Color.clear
.frame(width: 200, height: 200)
.contentShape(Rectangle()) // ensure full area is hit-testable
.dropDestination(for: Image.self) { ... }
loadTransferable による非同期読み込み
NSItemProvider.loadTransferable は非同期です。UI はメインアクターで更新してください。
provider.loadTransferable(type: Profile.self) { result in
Task { @MainActor in
switch result {
case .success(let profile):
self.profile = profile
case .failure(let error):
self.errorMessage = error.localizedDescription
}
}
}
PasteButton のプラットフォームごとの違い
iOS では PasteButton がペーストボードの変更を自動的に検証します。ペーストボードの内容が変わると、ボタンの有効・無効が切り替わります。macOS では、この自動検証は行われません。macOS アプリでペーストの検証を動的に行う必要がある場合は、UIPasteboard.changedNotification(UIKit)を監視するか、NSPasteboard の変更カウントを手動で確認してください。
リソース
WWDC:2022-10062、2022-10052、2022-10023、2022-10093、2022-10095
ドキュメント:/coretransferable/transferable、/coretransferable/choosing-a-transfer-representation-for-a-model-type、/coretransferable/filerepresentation、/coretransferable/proxyrepresentation、/swiftui/sharelink、/swiftui/drag-and-drop、/swiftui/clipboard、/uniformtypeidentifiers
スキル:axiom-photo-library、axiom-codable、axiom-swiftui-gestures、axiom-app-intents-ref