axiom-transferable-ref

安装量: 61
排名: #12298

安装

npx skills add https://github.com/charleswiltgen/axiom --skill axiom-transferable-ref
Transferable & Content Sharing Reference
Comprehensive guide to the CoreTransferable framework and SwiftUI sharing surfaces: drag and drop, copy/paste, and ShareLink.
When to Use This Skill
Implementing drag and drop (
.draggable
,
.dropDestination
)
Adding copy/paste support (
.copyable
,
.pasteDestination
,
PasteButton
)
Sharing content via
ShareLink
Making custom types transferable
Declaring custom UTTypes for app-specific formats
Bridging
Transferable
types with UIKit's
NSItemProvider
Choosing between
CodableRepresentation
,
DataRepresentation
,
FileRepresentation
, and
ProxyRepresentation
Example Prompts
"How do I make my model draggable in SwiftUI?"
"ShareLink isn't showing my custom preview"
"How do I accept dropped files in my view?"
"What's the difference between DataRepresentation and FileRepresentation?"
"How do I add copy/paste support for my custom type?"
"My drag and drop works within the app but not across apps"
"How do I declare a custom UTType?"
Part 1: Quick Reference
Decision Tree: Which 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
Common Errors
Error / Symptom
Cause
Fix
"Type does not conform to Transferable"
Missing
transferRepresentation
Add
static var transferRepresentation: some TransferRepresentation
Drop works in-app but not across apps
Custom UTType not declared in Info.plist
Add
UTExportedTypeDeclarations
entry
Receiver always gets plain text instead of rich type
ProxyRepresentation listed before CodableRepresentation
Reorder: richest representation first
FileRepresentation crashes with "file not found"
Receiver didn't copy file before sandbox extension expired
Copy to app storage in the importing closure
PasteButton always disabled
Pasteboard doesn't contain matching Transferable type
Check UTType conformance; verify the pasted data matches
ShareLink shows generic preview
No
SharePreview
provided or image isn't
Transferable
Supply explicit
SharePreview
with title and image
.dropDestination
closure never fires
Wrong payload type or view has zero hit-test area
Verify
for:
type matches dragged content; add
.frame()
or
.contentShape()
Built-in Transferable Types
These work with zero additional code — no conformance needed:
String
,
Data
,
URL
,
AttributedString
,
Image
,
Color
Part 2: Making Types Transferable
The
Transferable
protocol has one requirement: a static
transferRepresentation
property.
CodableRepresentation
Best for: models already conforming to
Codable
. Uses JSON by default.
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
)
}
}
Custom encoder/decoder (e.g., PropertyList instead of JSON):
CodableRepresentation
(
contentType
:
.
todo
,
encoder
:
PropertyListEncoder
(
)
,
decoder
:
PropertyListDecoder
(
)
)
Requirement
Custom UTTypes need matching
UTExportedTypeDeclarations
in Info.plist (see Part 4).
DataRepresentation
Best for: custom binary formats where data is in memory and you control serialization.
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 or export-only variants:
// Import only
DataRepresentation
(
importedContentType
:
.
png
)
{
data
in
try
MyImage
(
pngData
:
data
)
}
// Export only
DataRepresentation
(
exportedContentType
:
.
png
)
{
image
in
try
image
.
pngData
(
)
}
Avoid
using
UTType.data
as the content type — use a specific type like
.png
,
.pdf
,
.commaSeparatedText
.
FileRepresentation
Best for: large payloads on disk (videos, documents, archives). Passes file URLs instead of loading bytes into memory.
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
)
}
}
}
Critical
The
received.file
URL has a temporary sandbox extension. Copy the file to your own storage in the importing closure — the URL becomes inaccessible after the closure returns.
SentTransferredFile
properties:
file: URL
— the file location
allowAccessingOriginalFile: Bool
— when
false
(default), receiver gets a copy
ReceivedTransferredFile
properties:
file: URL
— the received file on disk
isOriginalFile: Bool
— whether this is the sender's original file or a copy
Content type precision
:
.mpeg4Movie
only matches
.mp4
files. To accept all common video formats (
.mp4
,
.mov
,
.m4v
), use the parent type
.movie
— or declare multiple
FileRepresentation
s for specific subtypes:
// 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
:
{
...
}
Import-only
When your type only receives files (drop target, no export), use the import-only initializer — it makes intent explicit and avoids accidental export:
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
Best for: fallback representations that let your type work with receivers expecting simpler types.
struct
Profile
:
Transferable
{
var
name
:
String
var
avatar
:
Image
static
var
transferRepresentation
:
some
TransferRepresentation
{
CodableRepresentation
(
contentType
:
.
profile
)
ProxyRepresentation
(
exporting
:
\
.
name
)
// Fallback: paste as text
}
}
Export-only proxy (common pattern — reverse conversion often impossible):
ProxyRepresentation
(
exporting
:
\
.
name
)
// Profile → String (one-way)
Bidirectional proxy (when reverse makes sense):
ProxyRepresentation
{
item
in
item
.
name
// export
}
importing
:
{
name
in
Profile
(
name
:
name
)
// import
}
Combining Multiple Representations
List representations in the
transferRepresentation
body.
Order matters
— receivers use the first representation they support.
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
)
}
}
Common mistake
putting
ProxyRepresentation
first causes receivers that support both to always get the degraded version.
Conditional Export
Hide a representation at runtime when conditions aren't met:
DataRepresentation
(
contentType
:
.
commaSeparatedText
)
{
archive
in
try
archive
.
toCSV
(
)
}
importing
:
{
data
in
try
Self
(
csvData
:
data
)
}
.
exportingCondition
{
archive
in
archive
.
supportsCSV
}
Visibility
Control which processes can see a representation:
CodableRepresentation
(
contentType
:
.
profile
)
.
visibility
(
.
ownProcess
)
// Only within this app
Options:
.all
(default),
.team
(same developer team),
.group
(same App Group, macOS),
.ownProcess
(same app only)
Suggested File Name
Hint for receivers writing to disk:
FileRepresentation
(
contentType
:
.
mpeg4Movie
)
{
video
in
SentTransferredFile
(
video
.
file
)
}
importing
:
{
received
in
// ...
}
.
suggestedFileName
(
"My Video.mp4"
)
// Or dynamic:
.
suggestedFileName
{
video
in
video
.
title
+
".mp4"
}
Part 3: SwiftUI Surfaces
ShareLink
The standard sharing entry point. Accepts any
Transferable
type.
// 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"
)
)
)
Sharing multiple items with per-item previews:
ShareLink
(
items
:
photos
)
{
photo
in
SharePreview
(
photo
.
caption
,
image
:
photo
.
image
)
}
SharePreview
initializers:
SharePreview("Title")
— text only
SharePreview("Title", image: someImage)
— text + full-size image
SharePreview("Title", icon: someIcon)
— text + thumbnail icon
SharePreview("Title", image: someImage, icon: someIcon)
— all three
Gotcha
If you omit
SharePreview
for a custom type, the share sheet shows a generic preview. Always provide one for non-trivial types.
Drag and Drop
Making a view draggable:
Text
(
profile
.
name
)
.
draggable
(
profile
)
With custom drag preview:
Text
(
profile
.
name
)
.
draggable
(
profile
)
{
Label
(
profile
.
name
,
systemImage
:
"person"
)
.
padding
(
)
.
background
(
.
regularMaterial
)
}
Accepting drops:
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
}
Multiple item types
— use an enum wrapper conforming to
Transferable
rather than stacking
.dropDestination
modifiers (stacking may cause only the outermost handler to fire):
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 with reordering
— combine with
.onMove
or use
draggable
/
dropDestination
for cross-container moves.
Clipboard (Copy/Paste)
Copy support
(activates Edit > Copy / Cmd+C):
List
(
items
)
{
item
in
Text
(
item
.
name
)
}
.
copyable
(
items
)
Paste support
(activates Edit > Paste / 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
}
}
The validator closure runs before the action — return an empty array to prevent the paste.
Cut support:
.
cuttable
(
for
:
Item
.
self
)
{
let
selected
=
items
.
filter
{
$0
.
isSelected
}
items
.
removeAll
{
$0
.
isSelected
}
return
selected
}
PasteButton
— system button that handles paste with type filtering:
PasteButton
(
payloadType
:
String
.
self
)
{
strings
in
notes
.
append
(
contentsOf
:
strings
)
}
Platform difference: PasteButton auto-validates pasteboard changes on iOS but not on macOS.
Availability
:
.copyable
,
.pasteDestination
, and
.cuttable
are
macOS 13+ only
— they do not exist on iOS. On iOS, use
PasteButton
(iOS 16+) for paste, and standard context menus or
UIPasteboard
for programmatic copy/cut.
PasteButton
is cross-platform: macOS 10.15+, iOS 16+, visionOS 1.0+.
Part 4: UTType Declarations
System Types
Use Apple's built-in UTTypes when possible — they're already recognized across the system:
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
Declaring Custom Types
Step 1
Declare in Swift:
extension
UTType
{
static
var
recipe
:
UTType
=
UTType
(
exportedAs
:
"com.myapp.recipe"
)
}
Step 2
Add to Info.plist under
UTExportedTypeDeclarations
:
<
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
>
Both are required.
The Swift declaration alone makes it compile, but cross-app transfers silently fail without the Info.plist entry.
Imported vs Exported Types
Exported
(
exportedAs:
) — Your app owns this type. Use for app-specific formats.
Imported
(
importedAs:
) — Another app owns this type. Use when you want to accept their format.
UTType Conformance
Custom types should conform to system types for broader compatibility:
// Your .recipe conforms to public.data (binary data)
// This means any receiver that accepts generic data can also accept recipes
Common conformance parents:
public.data
,
public.content
,
public.text
,
public.image
Part 5: UIKit Bridging
NSItemProvider + Transferable
Bridge between UIKit's
NSItemProvider
(used by
UIActivityViewController
, extensions, drag sessions) and
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
}
}
When to Use UIActivityViewController
ShareLink
covers most sharing needs. Use
UIActivityViewController
when you need:
Custom activity items or excluded activity types
UIActivityItemsConfiguration
for lazy item provision
Custom
UIActivity
subclasses
Programmatic presentation control
struct
ShareSheet
:
UIViewControllerRepresentable
{
let
items
:
[
Any
]
func
makeUIViewController
(
context
:
Context
)
->
UIActivityViewController
{
UIActivityViewController
(
activityItems
:
items
,
applicationActivities
:
nil
)
}
func
updateUIViewController
(
_
vc
:
UIActivityViewController
,
context
:
Context
)
{
}
}
For most apps,
ShareLink
is sufficient and preferred — it integrates with
Transferable
natively.
Part 6: Gotchas & Troubleshooting
FileRepresentation Temporary File Lifecycle
The
received.file
URL in a
FileRepresentation
importing closure has a temporary sandbox extension. The system may revoke access after the closure returns. Always copy the file:
// 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
)
Async Work After File Drop
The
FileRepresentation
importing closure is synchronous — you cannot
await
inside it. Copy the file first, return the model, then do async post-processing (thumbnails, transcoding, metadata extraction) on the copied 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
}
Representation Ordering
Representations are tried
in declaration order
. The receiver uses the first one it supports.
// 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
}
Custom UTType Without Info.plist
If you declare
UTType(exportedAs: "com.myapp.type")
in Swift but forget the Info.plist entry:
In-app transfers work (same process recognizes the type)
Cross-app transfers silently fail (other apps can't resolve the type)
This is the most common "works in development, fails in production" issue.
Drop Target Hit Testing
.dropDestination
requires the view to have a non-zero frame for hit testing. If drops aren't registering:
// 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
)
{
...
}
Async Loading with loadTransferable
NSItemProvider.loadTransferable
is asynchronous. Update UI on the main actor:
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 Platform Differences
PasteButton
auto-validates against pasteboard changes on iOS — the button enables/disables as the pasteboard content changes. On macOS, this automatic validation does not occur. If your macOS app needs dynamic paste validation, monitor
UIPasteboard.changedNotification
(UIKit) or
NSPasteboard
change count manually.
Resources
WWDC
2022-10062, 2022-10052, 2022-10023, 2022-10093, 2022-10095
Docs
/coretransferable/transferable, /coretransferable/choosing-a-transfer-representation-for-a-model-type, /coretransferable/filerepresentation, /coretransferable/proxyrepresentation, /swiftui/sharelink, /swiftui/drag-and-drop, /swiftui/clipboard, /uniformtypeidentifiers
Skills
axiom-photo-library, axiom-codable, axiom-swiftui-gestures, axiom-app-intents-ref
返回排行榜