CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

80

Quality

100%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

platform-and-sharing.mdskills/swiftui-patterns/references/

Platform And Sharing

Contents

  • Transferable, Drag & Drop, and ShareLink
  • Media Patterns
  • Top Bar Overlays
  • Title Menus
  • Input Toolbar
  • Menu Bar Commands
  • macOS Settings

Transferable, Drag & Drop, and ShareLink

Intent

Adopt the Transferable protocol to enable sharing, drag and drop, copy/paste, and ShareLink with a unified API. Available iOS 16+.

Docs: Transferable · Choosing a transfer representation

Transferable protocol overview

Transferable describes how a type converts to and from transfer representations (clipboard, drag, share sheet). Conform by implementing a static transferRepresentation property.

struct Note: Codable, Identifiable {
    let id: UUID
    var title: String
    var body: String
}

extension Note: Transferable {
    static var transferRepresentation: some TransferRepresentation {
        CodableRepresentation(contentType: .note)
        ProxyRepresentation(exporting: \.body) // fallback: plain text
    }
}

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

Representation order matters — place the most specific first, with broader fallbacks after.

Common transferable values

Use existing transferable values as proxies or fallbacks when they describe your data accurately:

ValueTypical use
StringPlain-text previews, titles, or body fallbacks
DataBinary payloads when you control serialization
URLLinks and file references
Color (SwiftUI)Color transfer; semantic colors resolve against a default environment
Codable modelsCustom JSON representations via CodableRepresentation

TransferRepresentation types

CodableRepresentation

For types conforming to Codable. Serializes to JSON by default:

static var transferRepresentation: some TransferRepresentation {
    CodableRepresentation(contentType: .myType)
}

ProxyRepresentation

Delegate to another Transferable type. Ideal for quick text or URL fallbacks:

ProxyRepresentation(exporting: \.title)            // export only
ProxyRepresentation(\.url)                          // import + export via URL

DataRepresentation

Full control over binary serialization:

DataRepresentation(contentType: .png) { image in
    try image.pngData()
} importing: { data in
    try MyImage(data: data)
}

Use DataRepresentation(exportedContentType:) for export-only representations.

FileRepresentation

For large content best transferred as files:

FileRepresentation(contentType: .movie) { video in
    SentTransferredFile(video.fileURL)
} importing: { receivedFile in
    let dest = FileManager.default.temporaryDirectory.appendingPathComponent(receivedFile.file.lastPathComponent)
    try FileManager.default.copyItem(at: receivedFile.file, to: dest)
    return Video(url: dest)
}

ShareLink

Present the system share sheet with a Transferable item:

ShareLink(item: note, preview: SharePreview(note.title)) {
    Label("Share", systemImage: "square.and.arrow.up")
}

// Multiple items
ShareLink(items: selectedNotes) { note in
    SharePreview(note.title)
}

// Simple string sharing
ShareLink(item: "Check out this app!", subject: Text("Cool App"))

ShareLink requires the item to conform to Transferable. The preview provides a title, optional image, and optional icon for the share sheet.

Drag and drop

Making views draggable

struct NoteCard: View {
    let note: Note

    var body: some View {
        Text(note.title)
            .draggable(note) // Note must be Transferable
    }
}

Use .draggable(note) { DragPreview(note) } to provide a custom drag preview.

Drop destination

struct NoteBoard: View {
    @State private var notes: [Note] = []

    var body: some View {
        VStack {
            ForEach(notes) { NoteCard(note: $0) }
        }
        .dropDestination(for: Note.self) { droppedNotes, location in
            notes.append(contentsOf: droppedNotes)
            return true
        } isTargeted: { isOver in
            // Highlight drop zone
        }
    }
}

For reordering within a list, combine .draggable with .dropDestination or use onMove on ForEach inside List.

Handling multiple types

Accept multiple content types with separate .dropDestination modifiers or use DropDelegate for advanced logic:

.dropDestination(for: String.self) { strings, _ in
    notes.append(contentsOf: strings.map { Note(id: UUID(), title: $0, body: "") })
    return true
}

Pasteboard integration

For direct clipboard access outside SwiftUI's drag/drop system, use UIPasteboard:

// Copy
UIPasteboard.general.string = note.title

// Paste
if let text = UIPasteboard.general.string {
    // use text
}

For Transferable types with custom content types, export to Data first:

let data = try await note.exported(as: .note)
UIPasteboard.general.setData(data, forPasteboardType: UTType.note.identifier)

On macOS 13+, prefer SwiftUI's .copyable, .cuttable, and command-based .pasteDestination(for:action:validator:) modifiers over direct pasteboard usage when you are wiring Edit menu commands and keyboard shortcuts. Current Apple docs list those command modifiers as iOS/iPadOS/Mac Catalyst 27 beta, not iOS 26 defaults. On iOS 26 targets, use UIPasteboard directly for custom clipboard commands, or use SwiftUI drag/drop and ShareLink for Transferable-driven sharing flows.

Docs: copyable(_:) · cuttable(for:action:) · pasteDestination(for:action:validator:)

Common patterns

Transferable enum with multiple representations

enum SharedContent: Transferable {
    case text(String)
    case url(URL)

    static var transferRepresentation: some TransferRepresentation {
        ProxyRepresentation { content in
            switch content {
            case .text(let s): return s
            case .url(let u): return u.absoluteString
            }
        }
    }
}

Export-only conformance

When your type should be sharable but not importable:

extension Report: Transferable {
    static var transferRepresentation: some TransferRepresentation {
        DataRepresentation(exportedContentType: .pdf) { report in
            try report.renderPDF()
        }
    }
}

Pitfalls

  • Always declare custom UTType identifiers in Info.plist under Exported/Imported Type Identifiers.
  • Representation order matters — the first matching representation wins. Put the richest format first.
  • FileRepresentation files are temporary; copy them if you need to persist.
  • Keep custom UTType declarations and transfer code in modules that are visible to every app target or extension that imports, exports, drags, or shares the type.
  • Test drag and drop on device — Simulator haptics and drop targeting differ from hardware.

Media Patterns

Intent

Use consistent patterns for loading images, previewing media, and presenting a full-screen viewer.

Core patterns

  • Use AsyncImage for simple remote images. LazyImage is from the third-party Nuke library if you need advanced caching and prefetching.
  • Prefer a lightweight preview component for inline media.
  • Use a shared viewer state (e.g., QuickLook) to present a full-screen media viewer.
  • Use openWindow for desktop/visionOS and a sheet for iOS.

Example: inline media preview

struct MediaPreviewRow: View {
  @Environment(QuickLook.self) private var quickLook

  let attachments: [MediaAttachment]

  var body: some View {
    ScrollView(.horizontal, showsIndicators: false) {
      HStack {
        ForEach(attachments) { attachment in
          Button {
            quickLook.prepareFor(
              selectedMediaAttachment: attachment,
              mediaAttachments: attachments
            )
          } label: {
            LazyImage(url: attachment.previewURL) { state in
              if let image = state.image {
                image.resizable().aspectRatio(contentMode: .fill)
              } else {
                ProgressView()
              }
            }
            .frame(width: 120, height: 120)
            .clipped()
          }
          .buttonStyle(.plain)
        }
      }
    }
  }
}

Example: global media viewer sheet

struct AppRoot: View {
  @State private var quickLook = QuickLook.shared

  var body: some View {
    content
      .environment(quickLook)
      .sheet(item: $quickLook.selectedMediaAttachment) { selected in
        MediaUIView(selectedAttachment: selected, attachments: quickLook.mediaAttachments)
      }
  }
}

Design choices to keep

  • Keep previews lightweight; load full media in the viewer.
  • Use shared viewer state so any view can open media without prop-drilling.
  • Use a single entry point for the viewer (sheet/window) to avoid duplicates.

Pitfalls

  • Avoid loading full-size images in list rows; use resized previews.
  • Don’t present multiple viewer sheets at once; keep a single source of truth.

Top Bar Overlays

Intent

Provide a custom top selector or pill row that sits above scroll content, using safeAreaBar(edge: .top) on iOS 26 and a compatible fallback on earlier OS versions.

iOS 26+ approach

Use safeAreaBar(edge: .top) to attach the view to the safe area bar. It insets the modified view, adjusts the safe area, and extends affected scroll edge effects.

if #available(iOS 26.0, *) {
  content
    .safeAreaBar(edge: .top) {
      TopSelectorView()
        .padding(.horizontal, .layoutPadding)
    }
}

Fallback for earlier iOS

Use .safeAreaInset(edge: .top) and hide the toolbar background to avoid double layers.

content
  .toolbarBackground(.hidden, for: .navigationBar)
  .safeAreaInset(edge: .top, spacing: 0) {
    VStack(spacing: 0) {
      TopSelectorView()
        .padding(.vertical)
        .padding(.horizontal, .layoutPadding)
        .background(Color.primary.opacity(0.06))
        .background(Material.ultraThin)
      Divider()
    }
  }

Design choices to keep

  • Use safeAreaBar when available; it integrates better with the navigation bar.
  • Use a subtle background + divider in the fallback to keep separation from content.
  • Keep the selector height compact to avoid pushing content too far down.

Pitfalls

  • Don’t stack multiple top insets; it can create extra padding.
  • Avoid heavy, opaque backgrounds that fight the navigation bar.

Title Menus

Intent

Use a title menu in the navigation bar to provide context‑specific filtering or quick actions without adding extra chrome.

Core patterns

  • Use ToolbarTitleMenu to attach a menu to the navigation title.
  • Keep the menu content compact and grouped with dividers.

Example: title menu for filters

@ToolbarContentBuilder
private var toolbarView: some ToolbarContent {
  ToolbarTitleMenu {
    Button("Latest") { timeline = .latest }
    Button("Resume") { timeline = .resume }
    Divider()
    Button("Local") { timeline = .local }
    Button("Federated") { timeline = .federated }
  }
}

Example: attach to a view

NavigationStack {
  TimelineView()
    .toolbar {
      toolbarView
    }
}

Example: title + menu together

struct TimelineScreen: View {
  @State private var timeline: TimelineFilter = .home

  var body: some View {
    NavigationStack {
      TimelineView()
        .toolbar {
          ToolbarItem(placement: .principal) {
            VStack(spacing: 2) {
              Text(timeline.title)
                .font(.headline)
              Text(timeline.subtitle)
                .font(.caption)
                .foregroundStyle(.secondary)
            }
          }

          ToolbarTitleMenu {
            Button("Home") { timeline = .home }
            Button("Local") { timeline = .local }
            Button("Federated") { timeline = .federated }
          }
        }
        .navigationBarTitleDisplayMode(.inline)
    }
  }
}

Example: title + subtitle with menu

ToolbarItem(placement: .principal) {
  VStack(spacing: 2) {
    Text(title)
      .font(.headline)
    Text(subtitle)
      .font(.caption)
      .foregroundStyle(.secondary)
  }
}

Design choices to keep

  • Only show the title menu when filtering or context switching is available.
  • Keep the title readable; avoid long labels that truncate.
  • Use secondary text below the title if extra context is needed.

Pitfalls

  • Don’t overload the menu with too many options.
  • Avoid using title menus for destructive actions.

Input Toolbar

Intent

Use a bottom-anchored input bar for chat, composer, or quick actions without fighting the keyboard.

Core patterns

  • Use .safeAreaInset(edge: .bottom) to anchor the toolbar above the keyboard.
  • Keep the main content in a ScrollView or List.
  • Drive focus with @FocusState and set initial focus when needed.
  • Avoid embedding the input bar inside the scroll content; keep it separate.

Example: scroll view + bottom input

@MainActor
struct ConversationView: View {
  @FocusState private var isInputFocused: Bool
  @State private var scrollPosition = ScrollPosition(edge: .bottom)
  @State private var draft = ""

  var body: some View {
    ScrollView {
      LazyVStack {
        ForEach(messages) { message in
          MessageRow(message: message)
        }
      }
      .scrollTargetLayout()
      .padding(.horizontal, .layoutPadding)
    }
    .scrollPosition($scrollPosition)
    .safeAreaInset(edge: .bottom) {
      InputBar(text: $draft)
        .focused($isInputFocused)
    }
    .scrollDismissesKeyboard(.interactively)
    .onAppear { isInputFocused = true }
  }
}

Design choices to keep

  • Keep the input bar visually separated from the scrollable content.
  • Use .scrollDismissesKeyboard(.interactively) for chat-like screens.
  • Ensure send actions are reachable via keyboard return or a clear button.

Pitfalls

  • Avoid placing the input view inside the scroll stack; it will jump with content.
  • Avoid nested scroll views that fight for drag gestures.

Menu Bar Commands

Contents

Intent

Use this when adding or customizing the macOS/iPadOS menu bar with SwiftUI commands.

Core patterns

  • Add commands at the Scene level with .commands { ... }.
  • Use SidebarCommands() when your UI includes a navigation sidebar.
  • Use CommandMenu for app-specific menus and group related actions.
  • Use CommandGroup to insert items before/after system groups or replace them.
  • Use FocusedValue for context-sensitive menu items that depend on the active scene.

Example: basic command menu

@main
struct MyApp: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
    .commands {
      CommandMenu("Actions") {
        Button("Run", action: run)
          .keyboardShortcut("R")
        Button("Stop", action: stop)
          .keyboardShortcut(".")
      }
    }
  }

  private func run() {}
  private func stop() {}
}

Example: insert and replace groups

WindowGroup {
  ContentView()
}
.commands {
  CommandGroup(before: .systemServices) {
    Button("Check for Updates") { /* open updater */ }
  }

  CommandGroup(after: .newItem) {
    Button("New from Clipboard") { /* create item */ }
  }

  CommandGroup(replacing: .help) {
    Button("User Manual") { /* open docs */ }
  }
}

Example: focused menu state

@MainActor @Observable
final class DataModel {
  var items: [String] = []
}

struct ContentView: View {
  @State private var model = DataModel()

  var body: some View {
    List(model.items, id: \.self) { item in
      Text(item)
    }
    .focusedSceneValue(model)
  }
}

struct ItemCommands: Commands {
  @FocusedValue(DataModel.self) private var model: DataModel?

  var body: some Commands {
    CommandGroup(after: .newItem) {
      Button("New Item") {
        model?.items.append("Untitled")
      }
      .disabled(model == nil)
    }
  }
}

Menu bar and Settings

  • Defining a Settings scene adds the Settings menu item on macOS automatically.
  • If you need a custom entry point inside the app, use OpenSettingsAction or SettingsLink.

Pitfalls

  • Avoid registering the same keyboard shortcut in multiple command groups.
  • Don’t use menu items as the only discoverable entry point for critical features.

macOS Settings

Intent

Use this when building a macOS Settings window backed by SwiftUI's Settings scene.

Core patterns

  • Declare the Settings scene in the App and compile it only for macOS.
  • Keep settings content in a dedicated root view (SettingsView) and drive values with @AppStorage.
  • Use TabView to group settings sections when you have more than one category.
  • Use Form inside each tab to keep controls aligned and accessible.
  • Use OpenSettingsAction or SettingsLink for in-app entry points to the Settings window.

Example: settings scene

@main
struct MyApp: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
    #if os(macOS)
    Settings {
      SettingsView()
    }
    #endif
  }
}

Example: tabbed settings view

@MainActor
struct SettingsView: View {
  @AppStorage("showPreviews") private var showPreviews = true
  @AppStorage("fontSize") private var fontSize = 12.0

  var body: some View {
    TabView {
      Tab("General", systemImage: "gear") {
        Form {
          Toggle("Show Previews", isOn: $showPreviews)
          Slider(value: $fontSize, in: 9...96) {
            Text("Font Size (\(fontSize, specifier: "%.0f") pts)")
          }
        }
      }

      Tab("Advanced", systemImage: "star") {
        Form {
          Toggle("Enable Advanced Mode", isOn: .constant(false))
        }
      }
    }
    .scenePadding()
    .frame(maxWidth: 420, minHeight: 240)
  }
}

Skip navigation

  • Avoid wrapping SettingsView in a NavigationStack unless you truly need deep push navigation.
  • Prefer tabs or sections; Settings is already presented as a separate window and should feel flat.
  • If you must show hierarchical settings, use a single NavigationSplitView with a sidebar list of categories.

Pitfalls

  • Don’t reuse iOS-only settings layouts (full-screen stacks, toolbar-heavy flows).
  • Avoid large custom view hierarchies inside Form; keep rows focused and accessible.

skills

.mcp.json

README.md

tile.json