Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
674
.claude/skills/axiom-transferable-ref/SKILL.md
Normal file
674
.claude/skills/axiom-transferable-ref/SKILL.md
Normal file
@@ -0,0 +1,674 @@
|
||||
---
|
||||
name: axiom-transferable-ref
|
||||
description: Use when implementing drag and drop, copy/paste, ShareLink, or ANY content sharing between apps or views - covers Transferable protocol, TransferRepresentation types, UTType declarations, SwiftUI surfaces, and NSItemProvider bridging
|
||||
license: MIT
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
```swift
|
||||
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):
|
||||
|
||||
```swift
|
||||
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.
|
||||
|
||||
```swift
|
||||
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:
|
||||
|
||||
```swift
|
||||
// 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.
|
||||
|
||||
```swift
|
||||
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:
|
||||
|
||||
```swift
|
||||
// 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:
|
||||
|
||||
```swift
|
||||
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.
|
||||
|
||||
```swift
|
||||
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):
|
||||
|
||||
```swift
|
||||
ProxyRepresentation(exporting: \.name) // Profile → String (one-way)
|
||||
```
|
||||
|
||||
Bidirectional proxy (when reverse makes sense):
|
||||
|
||||
```swift
|
||||
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.
|
||||
|
||||
```swift
|
||||
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:
|
||||
|
||||
```swift
|
||||
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:
|
||||
|
||||
```swift
|
||||
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:
|
||||
|
||||
```swift
|
||||
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.
|
||||
|
||||
```swift
|
||||
// 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:
|
||||
|
||||
```swift
|
||||
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:**
|
||||
|
||||
```swift
|
||||
Text(profile.name)
|
||||
.draggable(profile)
|
||||
```
|
||||
|
||||
With custom drag preview:
|
||||
|
||||
```swift
|
||||
Text(profile.name)
|
||||
.draggable(profile) {
|
||||
Label(profile.name, systemImage: "person")
|
||||
.padding()
|
||||
.background(.regularMaterial)
|
||||
}
|
||||
```
|
||||
|
||||
**Accepting drops:**
|
||||
|
||||
```swift
|
||||
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):
|
||||
|
||||
```swift
|
||||
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):
|
||||
|
||||
```swift
|
||||
List(items) { item in
|
||||
Text(item.name)
|
||||
}
|
||||
.copyable(items)
|
||||
```
|
||||
|
||||
**Paste support** (activates Edit > Paste / Cmd+V):
|
||||
|
||||
```swift
|
||||
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:**
|
||||
|
||||
```swift
|
||||
.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:
|
||||
|
||||
```swift
|
||||
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:
|
||||
|
||||
```swift
|
||||
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:
|
||||
|
||||
```swift
|
||||
extension UTType {
|
||||
static var recipe: UTType = UTType(exportedAs: "com.myapp.recipe")
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2**: Add to Info.plist under `UTExportedTypeDeclarations`:
|
||||
|
||||
```xml
|
||||
<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:
|
||||
|
||||
```swift
|
||||
// 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`:
|
||||
|
||||
```swift
|
||||
// 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
|
||||
|
||||
```swift
|
||||
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:
|
||||
|
||||
```swift
|
||||
// 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:
|
||||
|
||||
```swift
|
||||
// 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.
|
||||
|
||||
```swift
|
||||
// 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:
|
||||
|
||||
```swift
|
||||
// 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:
|
||||
|
||||
```swift
|
||||
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
|
||||
Reference in New Issue
Block a user