115 lines
3.6 KiB
Swift
115 lines
3.6 KiB
Swift
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<NSSharingService.SharingContentScope>
|
|
) -> 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)
|
|
}
|
|
}
|