# Platform and Sharing

Moving data in and out of the app (`Transferable`, `ShareLink`, drag and drop,
the pasteboard), media previews, top bar overlays, title menus, input bars,
menu bar commands and the macOS Settings window.

## Contents

1. [Transferable](#1-transferable)
2. [ShareLink](#2-sharelink)
3. [Drag and Drop](#3-drag-and-drop)
4. [Pasteboard](#4-pasteboard)
5. [Transferable Recipes](#5-transferable-recipes)
6. [Transferable Pitfalls](#6-transferable-pitfalls)
7. [Media Previews and Viewer](#7-media-previews-and-viewer)
8. [Top Bar Overlays](#8-top-bar-overlays)
9. [Title Menus](#9-title-menus)
10. [Input Toolbar](#10-input-toolbar)
11. [Menu Bar Commands](#11-menu-bar-commands)
12. [macOS Settings](#12-macos-settings)

## 1. Transferable

`Transferable` (iOS 16+) is one protocol behind sharing, drag and drop, copy
and paste, and `ShareLink`. Apple docs:
[Transferable](https://developer.apple.com/documentation/coretransferable/transferable),
the [representation guide](https://developer.apple.com/documentation/coretransferable/choosing-a-transfer-representation-for-a-model-type).

A type conforms by providing
`static var transferRepresentation: some TransferRepresentation`.

```swift
import CoreTransferable
import UniformTypeIdentifiers

struct Recipe: Codable, Identifiable {
    var id = UUID()
    var name: String
    var steps: String
}

extension UTType {
    static let recipe = UTType(exportedAs: "com.example.cookbook.recipe")
}

extension Recipe: Transferable {
    static var transferRepresentation: some TransferRepresentation {
        CodableRepresentation(contentType: .recipe)
        ProxyRepresentation(exporting: \.steps)
    }
}
```

Order matters. The receiver takes the first representation it understands, so
list the richest format first and broader fallbacks (plain text here) after it.

### Types that already conform

| Type | Content types |
|---|---|
| `String` | `.plainText`, `.utf8PlainText` |
| `Data` | `.data` |
| `URL` | `.url` |
| `AttributedString` | rich text |
| SwiftUI `Image` | `.image` |
| SwiftUI `Color` | `.color` |

### Representation kinds

- `CodableRepresentation(contentType:)` for `Codable` types. It encodes as
  JSON unless you pass another encoder.
- `ProxyRepresentation` hands off to another `Transferable` type. Good for a
  quick text or URL fallback. `ProxyRepresentation(exporting: \.name)` only
  exports; `ProxyRepresentation(exporting: \.link, importing: { Bookmark(link: $0) })`
  works in both directions through a URL.
- `DataRepresentation(contentType:exporting:importing:)` gives full control of
  the bytes:

```swift
extension Sticker: Transferable {
    static var transferRepresentation: some TransferRepresentation {
        DataRepresentation(contentType: .png) { sticker in
            try sticker.encodedPNG()
        } importing: { data in
            try Sticker(pngData: data)
        }
    }
}
```

- `DataRepresentation(exportedContentType:exporting:)` is the export-only
  version.
- `FileRepresentation(contentType:exporting:importing:)` moves large content as
  files. The received file is temporary, so copy it before using it:

```swift
extension Recording: Transferable {
    static var transferRepresentation: some TransferRepresentation {
        FileRepresentation(contentType: .mpeg4Audio) { recording in
            SentTransferredFile(recording.fileURL)
        } importing: { received in
            let target = FileManager.default.temporaryDirectory
                .appending(path: received.file.lastPathComponent)
            let files = FileManager.default
            try? files.removeItem(at: target)
            try files.copyItem(at: received.file, to: target)
            return Recording(fileURL: target)
        }
    }
}
```

## 2. ShareLink

`ShareLink` presents the system share sheet. Its item must be `Transferable`.
`SharePreview` supplies a title and, optionally, an image and an icon.

```swift
ShareLink(item: recipe, preview: SharePreview(recipe.name)) {
    Label("Share recipe", systemImage: "square.and.arrow.up")
}

ShareLink(items: selectedRecipes) { recipe in
    SharePreview(recipe.name)
}

ShareLink(item: "Dinner at 8", subject: Text("Tonight"))
```

## 3. Drag and Drop

- `.draggable(value)` makes a view a drag source; the value must be
  `Transferable`.
- `.draggable(value) { preview }` supplies a custom drag preview.

```swift
struct RecipeBoard: View {
    @State private var pinned: [Recipe] = []
    @State private var isHovering = false

    var body: some View {
        VStack {
            ForEach(pinned) { Text($0.name) }
        }
        .frame(maxWidth: .infinity, minHeight: 200)
        .background(isHovering ? Color.accentColor.opacity(0.15) : Color.clear)
        .dropDestination(for: Recipe.self) { recipes, _ in
            pinned.append(contentsOf: recipes)
            return true
        } isTargeted: { hovering in
            isHovering = hovering
        }
    }
}
```

- To reorder rows, either pair `.draggable` and `.dropDestination` yourself,
  or let a `List` do it through `onMove` on its `ForEach`.
- Several accepted types: add one `.dropDestination` per type, or use a
  `DropDelegate` for more complex rules.

```swift
.dropDestination(for: String.self) { texts, _ in
    pinned.append(contentsOf: texts.map { Recipe(name: $0, steps: "") })
    return true
}
```

## 4. Pasteboard

Outside drag and drop, iOS code can use `UIPasteboard` directly:

```swift
UIPasteboard.general.string = recipe.steps
let pasted: String? = UIPasteboard.general.string
```

For a custom `Transferable` type, export the data first:

```swift
let data = try await recipe.exported(as: .recipe)
let board = UIPasteboard.general
board.setData(data, forPasteboardType: UTType.recipe.identifier)
```

On iOS, `PasteButton(payloadType:onPaste:)` (iOS 16+) pastes without the
system permission prompt. On macOS, the SwiftUI modifiers `.copyable(_:)`,
`.cuttable(for:action:)` and `.pasteDestination(for:action:validator:)`
(macOS 13+) connect to the Edit menu and its keyboard shortcuts; prefer them
there over `NSPasteboard`. These three modifiers are not available on iOS.

## 5. Transferable Recipes

One enum, one plain-text representation:

```swift
enum Clip: Transferable {
    case note(String)
    case link(URL)

    static var transferRepresentation: some TransferRepresentation {
        ProxyRepresentation { clip in
            switch clip {
            case .note(let text): text
            case .link(let url): url.absoluteString
            }
        }
    }
}
```

Export only, as a PDF:

```swift
extension Statement: Transferable {
    static var transferRepresentation: some TransferRepresentation {
        DataRepresentation(exportedContentType: .pdf) { statement in
            try await statement.renderPDF()
        }
    }
}
```

## 6. Transferable Pitfalls

- Every custom `UTType` needs an Info.plist entry: Exported Type Identifiers
  for types the app defines, Imported Type Identifiers for types it only
  reads.
- Files delivered through `FileRepresentation` are temporary. Copy them to keep
  them.
- Declare the conformance in the module that defines the type. A conformance
  added from another module is retroactive and can clash with one the owner
  adds later.
- Test drag and drop on a device. The Simulator does not match hardware for
  haptics and drop targeting.

## 7. Media Previews and Viewer

Goal: one consistent way to load images, show inline previews and open a
full-screen viewer.

- `AsyncImage` covers simple remote images. For caching and prefetching, a
  library such as Nuke provides `LazyImage`.
- Inline media uses a light preview component.
- The full-screen viewer is driven by shared viewer state, for example a
  `MediaViewer` object in the environment.
- On macOS and visionOS open the viewer with `openWindow`; on iOS use a sheet.

```swift
struct AttachmentStrip: View {
    @Environment(MediaViewer.self) private var viewer
    let attachments: [Attachment]

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack {
                ForEach(attachments) { item in
                    Button {
                        viewer.open(item, among: attachments)
                    } label: {
                        AsyncImage(url: item.thumbnailURL) { image in
                            image.resizable().aspectRatio(contentMode: .fill)
                        } placeholder: {
                            ProgressView()
                        }
                        .frame(width: 120, height: 120)
                        .clipped()
                    }
                    .buttonStyle(.plain)
                }
            }
        }
    }
}

@main
struct AlbumApp: App {
    @State private var viewer = MediaViewer.shared

    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environment(viewer)
                .sheet(item: Bindable(viewer).current) { item in
                    FullScreenMedia(item: item)
                }
        }
    }
}
```

Guidelines:

- Keep previews light; load full-resolution media only inside the viewer.
- Shared viewer state lets any view open media without passing it down
  through every layer.
- One entry point (a sheet or a window) avoids duplicate viewers.

Pitfalls:

- Rows that decode original-resolution images. Downscale for the preview.
- Two viewer sheets open together because two views each own one. One state
  object, one presenter.

## 8. Top Bar Overlays

Goal: a custom selector or pill row above scrolling content. On iOS 26 use
`safeAreaBar`; earlier releases need a fallback.

```swift
struct FeedScreen: View {
    @State private var section: FeedSection = .following

    var body: some View {
        feedList
            .modifier(TopSelector(section: $section))
    }
}

struct TopSelector: ViewModifier {
    @Binding var section: FeedSection

    func body(content: Self.Content) -> some View {
        if #available(iOS 26.0, *) {
            content.safeAreaBar(edge: .top) {
                SectionPicker(section: $section)
                    .padding(.horizontal, Spacing.medium)
            }
        } else {
            content
                .toolbarBackground(.hidden, for: .navigationBar)
                .safeAreaInset(edge: .top, spacing: 0) {
                    VStack(spacing: 0) {
                        SectionPicker(section: $section)
                            .padding(.vertical, 8)
                            .background(Color.primary.opacity(0.05))
                            .background(Material.ultraThin)
                        Divider()
                    }
                }
        }
    }
}
```

Hiding the toolbar background in the fallback prevents two stacked
backgrounds.

Guidelines:

- Prefer `safeAreaBar` where available; it blends with the navigation bar.
- The fallback separates itself from content with a faint background and a
  divider.
- Keep the selector short so content does not move far down.

Pitfalls:

- Several top insets stacked on each other add extra padding.
- Heavy, opaque backgrounds that clash with the navigation bar.

## 9. Title Menus

Goal: a menu on the navigation title for filtering or quick context actions,
with no extra chrome.

- `ToolbarTitleMenu` (iOS 16+) attaches a menu to the title.
- Keep its content short and group it with `Divider()`.

```swift
struct TasksScreen: View {
    @State private var filter: TaskFilter = .open

    var body: some View {
        NavigationStack {
            TaskList(filter: filter)
                .navigationTitle(filter.title)
                .toolbar { titleMenu }
        }
    }

    @ToolbarContentBuilder
    private var titleMenu: some ToolbarContent {
        ToolbarTitleMenu {
            Button("Open") { filter = .open }
            Button("Done") { filter = .done }
            Divider()
            Button("Everything") { filter = .all }
        }
    }
}
```

A title with a subtitle under it:

```swift
.toolbar {
    ToolbarItem(placement: .principal) {
        VStack(spacing: 2) {
            Text(project.name).font(.headline)
            Text("\(project.openCount) open").font(.caption).foregroundStyle(.secondary)
        }
    }
    titleMenu
}
.navigationBarTitleDisplayMode(.inline)
```

Guidelines:

- Show the title menu only when there is something to filter or switch.
- Keep the title short enough that it is not truncated.
- Put extra context in the secondary line under the title.

Pitfalls:

- Too many options in the menu.
- Destructive actions in a title menu.

## 10. Input Toolbar

Goal: an input bar pinned to the bottom (chat, composer, quick actions) that
moves with the keyboard.

- Anchor it with `.safeAreaInset(edge: .bottom)` so it sits above the keyboard.
- The messages or items scroll in a `ScrollView` or `List`.
- `@FocusState` decides when the field is active; focusing it on appear is
  often right for a composer.
- Keep the bar out of the scrolling content.

```swift
@MainActor
struct ChatThread: View {
    @State private var draft = ""
    @State private var position = ScrollPosition(edge: .bottom)
    @FocusState private var isComposing: Bool
    let messages: [Message]
    let onSend: (String) -> Void

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(messages) { MessageBubble(message: $0) }
            }
            .scrollTargetLayout()
        }
        .scrollPosition($position)
        .scrollDismissesKeyboard(.interactively)
        .safeAreaInset(edge: .bottom) {
            HStack {
                TextField("Message", text: $draft)
                    .focused($isComposing)
                    .onSubmit(send)
                Button("Send", systemImage: "arrow.up.circle.fill", action: send)
            }
            .padding()
            .background(.bar)
        }
        .onAppear { isComposing = true }
    }

    private func send() {
        onSend(draft)
        draft = ""
    }
}
```

`ScrollPosition` and `scrollPosition(_:)` with it need iOS 18.

Guidelines:

- Keep the bar visually distinct from the scrolling content.
- Chat-style screens use `.scrollDismissesKeyboard(.interactively)`.
- Sending must work from the return key and from a clear button.

Pitfalls:

- An input field placed inside the scroll stack moves with the content.
- Nested scroll views fighting over the drag gesture.

## 11. Menu Bar Commands

Applies when adding to or customising the menu bar on macOS and iPadOS with
SwiftUI commands.

- Attach commands to a `Scene` with `.commands { }`.
- Add `SidebarCommands()` when the UI has a navigation sidebar.
- `CommandMenu` creates an app-specific menu of related actions.
- `CommandGroup` inserts items before or after a system group, or replaces it.
- `FocusedValue` makes items depend on the active scene.

```swift
WindowGroup {
    ContentView()
}
.commands {
    SidebarCommands()
    CommandMenu("Library") {
        Button("Rescan", action: rescan).keyboardShortcut("R")
        Button("Stop Scan", action: stopScan).keyboardShortcut(".")
    }
    CommandGroup(before: .systemServices) {
        Button("Diagnostics", action: showDiagnostics)
    }
    CommandGroup(after: .newItem) {
        Button("New Playlist", action: newPlaylist)
    }
    CommandGroup(replacing: .help) {
        Button("Library Guide", action: openGuide)
    }
}
```

Context-sensitive commands read a focused value that the active scene
publishes:

```swift
@MainActor @Observable
final class LibraryModel {
    func addPlaylist() { }
}

struct LibraryWindow: View {
    @State private var model = LibraryModel()

    var body: some View {
        LibraryView(model: model)
            .focusedSceneValue(model)
    }
}

struct LibraryCommands: Commands {
    @FocusedValue(LibraryModel.self) private var model: LibraryModel?

    var body: some Commands {
        CommandGroup(after: .newItem) {
            Button("Add Playlist") { model?.addPlaylist() }
                .disabled(model == nil)
        }
    }
}
```

On macOS, the app menu gains its Settings item as soon as the `App` declares a
`Settings` scene. To open settings from inside the app, use `SettingsLink` or the
`openSettings` environment action (`OpenSettingsAction`).

Pitfalls:

- The same shortcut assigned in several command groups.
- A critical feature reachable only through a menu item.

## 12. macOS Settings

Applies when building the macOS Settings window with the SwiftUI `Settings`
scene.

- Declare `Settings { PreferencesView() }` in the `App`, wrapped in
  `#if os(macOS)` so it compiles only for macOS.
- Give settings their own root view and back values with `@AppStorage`.
- More than one category: group them with a `TabView`.
- Use a `Form` inside each tab for aligned, accessible controls.
- Open settings from the app with `SettingsLink` or `OpenSettingsAction`.

```swift
@main
struct ReaderApp: App {
    var body: some Scene {
        WindowGroup { LibraryView() }
        #if os(macOS)
        Settings { PreferencesView() }
        #endif
    }
}

@MainActor
struct PreferencesView: View {
    @AppStorage("showThumbnails") private var showThumbnails = true
    @AppStorage("readerTextSize") private var readerTextSize = 14.0

    var body: some View {
        TabView {
            Tab("General", systemImage: "gearshape") {
                Form {
                    Toggle("Show thumbnails", isOn: $showThumbnails)
                }
            }
            Tab("Reading", systemImage: "textformat.size") {
                Form {
                    Slider(value: $readerTextSize, in: 9...96) {
                        Text("Text size: \(readerTextSize, specifier: "%.0f")")
                    }
                }
            }
        }
        .scenePadding()
        .frame(maxWidth: 420, minHeight: 240)
    }
}
```

`Tab` inside the settings `TabView` needs macOS 15; for macOS 14 use
`.tabItem { Label(...) }` on each child instead.

Guidelines:

- Do not wrap the settings view in a `NavigationStack` unless deep push
  navigation is genuinely needed.
- Settings is its own window and should feel flat: tabs or sections.
- For hierarchical settings, use one `NavigationSplitView` with a sidebar of
  categories.

Pitfalls:

- Reusing iOS settings layouts on the Mac (full-screen stacks, toolbar-heavy
  flows).
- Deep custom hierarchies stuffed into a `Form`. Each row should hold one
  simple, accessible control.
