import AppKit import ShotdeckCore @MainActor enum Sharing { /// Presents the AirDrop picker for `fileURL`, anchored to `view`. /// Throws `ShotdeckError.airDropUnavailable` when the service cannot be created, /// `canPerform` is false, or `view` is not in a visible window (a detached view /// never produces an on-screen sheet). /// /// `onFinished` is invoked on the main actor when the sheet completes: `true` for /// `didShareItems`, `false` for `didFailToShareItems` (including user cancel). static func airDrop( fileURL: URL, from view: NSView, onFinished: @escaping @MainActor @Sendable (Bool) async -> Void ) throws { guard let service = NSSharingService(named: .sendViaAirDrop), service.canPerform(withItems: [fileURL]) else { throw ShotdeckError.airDropUnavailable } // Presenting from a detached NSView (no window) yields a sheet that never appears. guard let window = view.window, window.isVisible else { throw ShotdeckError.airDropUnavailable } NSApp.activate() window.makeKeyAndOrderFront(nil) service.subject = fileURL.lastPathComponent let session = AirDropSession( service: service, window: window, view: view, onFinished: onFinished ) AirDropSession.keepAlive(session) service.delegate = session service.perform(withItems: [fileURL]) } } /// Retains the sharing service for the life of the picker and supplies the real /// on-screen window as the sheet parent. `NSSharingService.delegate` is weak, so /// `live` is the strong reference that keeps this object alive until the sheet /// reports success or failure (including cancel). @MainActor private final class AirDropSession: NSObject, NSSharingServiceDelegate { static var live: [AirDropSession] = [] let service: NSSharingService let window: NSWindow let view: NSView let onFinished: @MainActor @Sendable (Bool) async -> Void private var reported = false init( service: NSSharingService, window: NSWindow, view: NSView, onFinished: @escaping @MainActor @Sendable (Bool) async -> Void ) { self.service = service self.window = window self.view = view self.onFinished = onFinished } static func keepAlive(_ session: AirDropSession) { live.append(session) } private func drop() { Self.live.removeAll { $0 === self } } private func report(_ success: Bool) { guard !reported else { return } reported = true Task { @MainActor in await self.onFinished(success) self.drop() } } func sharingService( _ sharingService: NSSharingService, sourceWindowForShareItems items: [Any], sharingContentScope: UnsafeMutablePointer ) -> NSWindow? { sharingContentScope.pointee = .item return window } func sharingService( _ sharingService: NSSharingService, sourceFrameOnScreenForShareItem item: Any ) -> NSRect { let inWindow = view.convert(view.bounds, to: nil) return window.convertToScreen(inWindow) } func sharingService(_ sharingService: NSSharingService, didShareItems items: [Any]) { report(true) } func sharingService( _ sharingService: NSSharingService, didFailToShareItems items: [Any], error: any Error ) { report(false) } }