(Using macOS 26 Beta 9 and Xcode 26 Beta 7) I am trying to support basic onDrop from a source app to my app. I am trying to get the closest "source" representation of a drag-and-drop, e.g. a JPEG file being dropped into my app shouldn't be converted, but stored as a JPEG in Data. Otherwise, everything gets converted into TIFFs and modern iPhone photos get huge. I also try to be a good app, and provide asynchronous support.
Alas, I've been running around for days now, where I can now support Drag-and-Drop from the Finder, from uncached iCloud files with Progress bar, but so far, drag and dropping from Safari eludes me.
My code is as follows for the onDrop support:
Image(nsImage: data.image).onDrop(of: Self.supportedDropItemUTIs, delegate: self)
The UTIs are as follows:
public static let supportedDropItemUTIs: [UTType] = [
.image,
.heif,
.rawImage,
.png,
.tiff,
.svg,
.heic,
.jpegxl,
.bmp,
.gif,
.jpeg,
.webP,
]
Finally, the code is as follows:
public func performDrop(info: DropInfo) -> Bool {
let itemProviders = info.itemProviders(for: Self.supportedDropItemUTIs)
guard let itemProvider = itemProviders.first else {
return false
}
let registeredContentTypes = itemProvider.registeredContentTypes
guard let contentType = registeredContentTypes.first else {
return false
}
var suggestedName = itemProvider.suggestedName
if suggestedName == nil {
switch contentType {
case UTType.bmp: suggestedName = "image.bmp"
case UTType.gif: suggestedName = "image.gif"
case UTType.heic: suggestedName = "image.heic"
case UTType.jpeg: suggestedName = "image.jpeg"
case UTType.jpegxl: suggestedName = "image.jxl"
case UTType.png: suggestedName = "image.png"
case UTType.rawImage: suggestedName = "image.raw"
case UTType.svg: suggestedName = "image.svg"
case UTType.tiff: suggestedName = "image.tiff"
case UTType.webP: suggestedName = "image.webp"
default: break
}
}
let progress = itemProvider.loadInPlaceFileRepresentation(forTypeIdentifier: contentType.identifier) { url, _, error in
if let error {
print("Failed to get URL from dropped file: \(error)")
return
}
guard let url else {
print("Failed to get URL from dropped file!")
return
}
let queue = OperationQueue()
queue.underlyingQueue = .global(qos: .utility)
let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges)
let coordinator = NSFileCoordinator()
coordinator.coordinate(with: [intent],
queue: queue) { error in
if let error {
print("Failed to coordinate data from dropped file: \(error)")
return
}
do {
// Load file contents into Data object
let data = try Data(contentsOf: intent.url)
Dispatch.DispatchQueue.main.async {
self.data.data = data
self.data.fileName = suggestedName
}
} catch {
print("Failed to load coordinated data from dropped file: \(error)")
}
}
}
DispatchQueue.main.async {
self.progress = progress
}
return true
}
For your information, this code is at the state where I gave up and sent it here, because I cannot find a solution to my issue.
Now, this code works everywhere, except for dragging and dropping from Safari.
Let's pretend I go to this web site: https://commons.wikimedia.org/wiki/File:Tulip_Tulipa_clusiana_%27Lady_Jane%27_Rock_Ledge_Flower_Edit_2000px.jpg
and I try to drag-and-drop the image, it will fail with the following error:
URL https://upload.wikimedia.org/wikipedia/commons/c/cf/Tulip_Tulipa_clusiana_%27Lady_Jane%27_Rock_Ledge_Flower_Edit_2000px.jpg is not a file:// URL.
And then, fail with the dreaded
Failed to get URL from dropped file: Error Domain=NSItemProviderErrorDomain Code=-1000
As far as I can tell, the problem lies in the opaque NSItemProvider receiving a web site URL from Safari. I tried most solutions, I couldn't retrieve that URL. The error happens in the callback of loadInPlaceFileRepresentation, but also fails in loadFileRepresentation. I tried hard-requesting a loadObject of type URL, but there's only one representation for the JPEG file. I tried only putting .url in the requests, but it would not transfer it.
Anyone solved this mystery?
(Re: previous answers - skip if you haven't followed the saga) Adding some context for the answer. I added AppKit specifically because it's not UIKit, as I didn't want an answer for iOS or ipadOS, but macOS.
The only SwiftUI line is the onDrag, that I faithfully added to the line as the "caller", but I didn't think I'd have a different DropHandler for macOS and ipadOS, the culprit seemingly being the onDrag SwiftUI part that tries to give me only the "good thing" on macOS, but somehow is unusable. As I repeated, I wanted to quickly iterate and used the hacks instead of trying to use the Async versions, where some versions aren't actually available. The final code will be in Async, thank you.
My first SwiftUI app was with GCD because there wasn't an alternative back then. There's one now. But I'm also butting heads with SwiftData, where models cannot be Sendable, but Async are also required to be sendable, so I've had a lot of "fun" on this topic. With the nearly 200 trials I had to do in order to make it work, I wasn't about to create good code only to suit the taste of Internet good will helpers, I'm sure you'll agree with me.
So. I did another 8 hours on this, and I was able to do it properly!
(/snip)
The answer lies in using the .onDrop
with the .image
type, where it will magically send me the Drag & Drops where images are accessible. But then, to actually ignore the provider, and do the work yourself from the .drag
pasteboard!!
In essence, the previous piece of code (still not asynchronous- that's my next work) got trimmed down on macOS only to do things through the same code I'm using for pasting things.
private func handleItemProviders(_ itemProviders: [NSItemProvider]) -> Bool {
guard let itemProvider = itemProviders.first else {
return false
}
let registeredContentTypes = itemProvider.registeredContentTypes
print("ContentTypes: \(registeredContentTypes)")
guard let contentType = registeredContentTypes.first else {
return false
}
#if canImport(AppKit)
handlePasteboard(NSPasteboard(name: .drag))
#else
[snip]
And the handlePasteboard operation is a long-winded function that processes whatever the pasteboard has in it. First by optionally getting an URL (file or web)
var url: URL?
let urlType = item.availableType(from: Self.supportedPasteboardURLTypes)
if let urlType,
let urlString = item.string(forType: urlType) {
url = URL(string: urlString)
Then by trying to get the data from the pasteboard itself (used in case of a drag-and-drop from Photos or Preview, or even Safari)
if let imageType = item.availableType(from: Self.supportedPasteboardTypes) {
guard let data = item.data(forType: imageType) else {
Then, if not functional, by trying to get the image from the URL through a coordinator (used in case of a drag-and-drop of an actual image from a file)
} else if let url {
if urlType == .fileURL {
let queue = OperationQueue()
queue.underlyingQueue = .global(qos: .utility)
let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges)
let coordinator = NSFileCoordinator()
coordinator.coordinate(with: [intent],
queue: queue) { error in
Finally, when trying to drag-and-drop an actual link to an image, by trying to retrieve it with a downloadTask (don't forget the incoming network client capability!)
} else if urlType == .URL {
let session = URLSession.shared.downloadTask(with: url) { url, response, error in
In the last part, I check for the mime type for image, and download the URL if working.
So this works perfectly on macOS, under all circumstances. For iOS, a piece of code similar to what was written in the original question actually works, since the actual image got retrieved by Safari and the system sends back a local device URL, and not a https link.
So... Problem averted for now! And the code for the pasteboard handling can be reused with .general
for a regular paste in SwiftUI.
.onDrop(of: Self.supportedDropItemUTIs, delegate: self)
.contextMenu {
Button(action: onPasteButton) {
Text("Paste")
Image(systemName: "doc.on.clipboard")
}
}
with
public func onPasteButton() {
handlePasteboard(.general)
}