mcpField
A
axiom-transferable-ref

axiom-transferable-ref

github.com/charleswiltgen開発1.2k100 回インストール今日更新
リポジトリ

axiom-transferable-ref について

CoreTransferable と SwiftUI の共有機能を扱う Swift 開発者向けリファレンススキルです。ドラッグ&ドロップ、クリップボード、ShareLink に加え、独自型や UTType、転送表現の選び方を解説します。

主な機能

  1. 01ドラッグ&ドロップの実装
  2. 02コピー&ペーストの追加
  3. 03ShareLink によるコンテンツ共有
  4. 04独自型の転送表現の設定
  5. 05独自 UTType の宣言

活用シーン

  1. 01SwiftUI のモデルをドラッグ可能にする
  2. 02ビューでドロップファイルを受け付ける
  3. 03独自コンテンツにコピー&ペーストを追加
  4. 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

コメント