All skills
simota avatar

/native

@e307415
by shingo imotasimota/agent-skills85 stars
15

Implementing production iOS/Android/macOS native features (SwiftUI, Compose) and iterating a screen against a reference design. Not for cross-platform RN/Flutter (Port) or web (Artisan).

Use this Skill: https://skilld.dev/gh/simota/agent-skills/native

This session only. Nothing lands on disk.

referencedrag-drop-services.md

≈3.2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Drag & Drop, Pasteboard, and Services

Purpose: Transferable + .draggable/.dropDestination, NSItemProvider bridging, raw NSPasteboard, NSFilePromiseProvider, NSDraggingSource/NSDraggingDestination (AppKit path), Copy/Paste + Edit-menu wiring, macOS Services, NSSharingServicePicker, Quick Look preview extensions. Read when: Building drag & drop, clipboard, or Services-menu integration (dragdrop Recipe).


1. Transferable — SwiftUI-First Path

Transferable (introduced WWDC22) is the modern data-interchange protocol backing .draggable, .dropDestination, ShareLink, and system copy/paste in SwiftUI. Prefer it over raw NSItemProvider/NSPasteboard unless a representation SwiftUI doesn't expose is required (§ 5).

Conformance styles

Representation Use for
ProxyRepresentation Delegate to an existing Transferable/Codable property (e.g. expose a URL or String view of a richer type)
CodableRepresentation Struct/enum data that round-trips through Codable, tagged with a UTType
FileRepresentation On-disk file — exports/imports a URL, SwiftUI copies the file itself during drag
DataRepresentation Raw Data blobs (images, custom binary formats)
import CoreTransferable
import UniformTypeIdentifiers

struct Note: Codable, Transferable {
    var title: String
    var body: String

    static var transferRepresentation: some TransferRepresentation {
        CodableRepresentation(contentType: .myAppNote)
        ProxyRepresentation(exporting: \.title)   // fallback: plain-text export of just the title
    }
}

struct Attachment: Transferable {
    let fileURL: URL

    static var transferRepresentation: some TransferRepresentation {
        FileRepresentation(exportedContentType: .pdf) { attachment in
            SentTransferredFile(attachment.fileURL)
        } importing: { received in
            Attachment(fileURL: received.file)
        }
    }
}

List multiple representations to offer format fallbacks — SwiftUI negotiates with the drop target/receiving app in declaration order, most-specific first.

.draggable / .dropDestination

struct NoteRow: View {
    let note: Note

    var body: some View {
        Text(note.title)
            .draggable(note) {
                NoteDragPreview(note: note)   // custom preview; omit for the default snapshot
            }
    }
}

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

    var body: some View {
        List(notes) { NoteRow(note: $0) }
            .dropDestination(for: Note.self) { droppedNotes, location in
                notes.append(contentsOf: droppedNotes)
                return true   // return false to reject and animate the drop back
            }
    }
}

dropDestination(for:action:isTargeted:) adds an isTargeted binding for hover-state styling; dropDestination(for:isEnabled:action:) gates acceptance dynamically (e.g. disable drop while a save is in flight).

Multi-item drags

.draggable on a ForEach/List row participates automatically in multi-item drag when the user has a multi-selection active — SwiftUI bundles the Transferable payload for every selected row. No extra API; ensure the selection binding (List(selection:)) is wired so the drag knows what's selected.


2. NSItemProvider Bridging

Transferable is backed by NSItemProvider under the hood. When interoperating with AppKit code or third-party APIs still on the old protocol, bridge explicitly:

// Transferable -> NSItemProvider (for APIs expecting the legacy type)
let provider = NSItemProvider(object: NSString(string: note.title))

// NSItemProvider -> Transferable-style loading (AppKit drag destination receiving from SwiftUI-originated drag)
provider.loadDataRepresentation(forTypeIdentifier: UTType.myAppNote.identifier) { data, error in
    guard let data else { return }
    let note = try? JSONDecoder().decode(Note.self, from: data)
}

3. Raw NSPasteboard

Needed for: multi-pasteboard workflows (find pasteboard), custom pasteboard types without a Transferable conformance, or reading pasteboard contents outside a drag/drop gesture (e.g. a paste button that isn't backed by SwiftUI's PasteButton).

Pasteboard Purpose
NSPasteboard.general Standard copy/paste
NSPasteboard(name: .drag) Active drag operations (system-managed; rarely accessed directly)
NSPasteboard(name: .find) Shared Find-panel search term across apps
// Write
let pb = NSPasteboard.general
pb.clearContents()
pb.setString(note.title, forType: .string)
pb.setData(try! JSONEncoder().encode(note), forType: NSPasteboard.PasteboardType("com.example.myapp.note"))

// Read
if let title = pb.string(forType: .string) { /* ... */ }

NSPasteboard.PasteboardType values should mirror the app's UTType identifiers for consistency with the Transferable/Uniform Type Identifiers path (reference/documents.md § 2).

Promised files — NSFilePromiseProvider

For drag sources that generate the file lazily (expensive render, network fetch) rather than materializing it up front:

final class NoteFilePromiseProvider: NSFilePromiseProvider, NSFilePromiseProviderDelegate {
    var note: Note!

    override init() {
        super.init()
        self.delegate = self
    }

    func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String {
        "\(note.title).mynote"
    }

    func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: @escaping (Error?) -> Void) {
        do {
            try JSONEncoder().encode(note).write(to: url)
            completionHandler(nil)
        } catch {
            completionHandler(error)
        }
    }
}

NSFilePromiseProvider requires a serial OperationQueue (set via .operationQueue) to serialize the write callback — the drop destination (e.g. Finder) invokes it after the user releases the drag.


4. NSDraggingSource / NSDraggingDestination (AppKit Path)

Drop to this only when: a custom NSView-based list/canvas needs per-pixel drop-target computation, or drag behavior (spring-loading, custom cursor feedback, NSDraggingSession formation control) that .draggable/.dropDestination doesn't expose.

final class CanvasView: NSView, NSDraggingSource, NSDraggingDestination {
    func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
        context == .withinApplication ? .move : .copy
    }

    override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
        sender.draggingPasteboard.canReadObject(forClasses: [NSString.self], options: nil) ? .copy : []
    }

    override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
        guard let items = sender.draggingPasteboard.readObjects(forClasses: [NSString.self], options: nil) else { return false }
        // handle drop
        return true
    }
}

Register accepted types once, typically in awakeFromNib/init: registerForDraggedTypes([.string, .fileURL]).


5. Copy/Paste + Edit-Menu Wiring

SwiftUI wires standard Edit-menu Copy/Paste/Cut automatically for text controls (TextField, TextEditor). For custom data:

struct NoteEditor: View {
    @FocusedValue(\.selectedNote) private var selectedNote

    var body: some View {
        ContentView()
            .copyable([selectedNote?.title ?? ""])   // enables Edit > Copy for non-text selection
            .pasteDestination(for: Note.self) { notes in
                // handle pasted Note payloads
            }
    }
}

.copyable/.pasteDestination (SwiftUI, macOS 13+) route through the same Transferable conformance as drag & drop — one conformance covers copy/paste, drag/drop, and ShareLink.

For NSDocument-based apps, Copy/Cut/Paste route through NSResponder's copy(_:)/cut(_:)/paste(_:) action methods — override on the first responder (typically the document's main view or window controller).


6. macOS Services

Services let the app both provide actions to other apps' Services menus and consume services others provide.

Registering a service (NSServices in Info.plist)

<key>NSServices</key>
<array>
  <dict>
    <key>NSMenuItem</key>
    <dict><key>default</key><string>Add to MyApp Notes</string></dict>
    <key>NSMessage</key><string>addToNotes</string>
    <key>NSPortName</key><string>MyApp</string>
    <key>NSSendTypes</key>
    <array><string>NSStringPboardType</string></array>
  </dict>
</array>
final class ServiceProvider: NSObject {
    @objc func addToNotes(_ pasteboard: NSPasteboard, userData: String, error: AutoreleasingUnsafeMutablePointer<NSString>) {
        guard let text = pasteboard.string(forType: .string) else { return }
        // create a note from `text`
    }
}

// App launch:
NSApp.servicesProvider = ServiceProvider()

Sharing out — NSSharingServicePicker

let picker = NSSharingServicePicker(items: [note.title, noteFileURL])
picker.show(relativeTo: sourceButton.bounds, of: sourceButton, preferredEdge: .minY)

SwiftUI equivalent for outbound sharing is ShareLink (backed by Transferable) — prefer ShareLink unless anchoring to a specific NSView/toolbar item is required.


7. Quick Look Preview Extension (Basics)

A Quick Look preview extension (separate .appex target, QLPreviewProvider) renders the app's custom document format in Finder's Quick Look/Gallery view without launching the app:

final class PreviewProvider: QLPreviewProvider, QLPreviewingController {
    func providePreview(for request: QLFilePreviewRequest) async throws -> QLPreviewReply {
        let reply = QLPreviewReply(fileURL: request.fileURL)
        return reply
    }
}

Register the extension's supported UTTypes (matching § 1 of reference/documents.md) in the extension target's Info.plist under QLSupportedContentTypes. Full preview-rendering pipeline design (custom drawing vs delegating to fileURL) is out of scope here — this is the wiring, not the rendering.


8. Security-Scoped Access for Dropped Files

Files dropped from outside the sandbox (Finder, another app) arrive as security-scoped URLs when the app is sandboxed. Call startAccessingSecurityScopedResource() before reading, and persist a bookmark if access must survive beyond the current drop handler.

.dropDestination(for: URL.self) { urls, _ in
    for url in urls {
        guard url.startAccessingSecurityScopedResource() else { continue }
        defer { url.stopAccessingSecurityScopedResource() }
        // read file
    }
    return true
}

Full bookmark lifecycle (persisting across launches, stale-bookmark refresh) → reference/sandbox-entitlements.md.


Cross-References

  • Security-scoped bookmarks, Powerbox-mediated pickers — reference/sandbox-entitlements.md
  • Document file formats / UTType declaration — reference/documents.md
  • NSViewRepresentable bridging for custom AppKit drag views hosted in SwiftUI — reference/appkit-interop.md

Sources

Source: SKILL.md on GitHub

No alerts13d4 checks · Risk SAFE
  • Gen Agent Trust Hub13d

    The skill is a high-quality engineering reference suite for native mobile and desktop development (iOS, Android, macOS). It provides detailed architectural patterns, CLI tool references, and compliance checklists. No malicious patterns, obfuscation, or insecure data handling were detected.

  • Socket13d

    No alerts

  • Snyk13d

    Risk: LOW · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at e307415. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 days ago.

Activeupdated 2 weeks ago

README badge

README badge for simota/agent-skills/native