Effortless Image Input on macOS: Drag, Drop, and Paste Done Right
Make your SwiftUI app feel native by handling images from Finder, browsers, and the clipboard
Adding an image into your macOS app should be easy to accomplish.
However, it is not always clear how users will expect it to work - or how you should implement it.
On iOS, we rely on to grab images direct from the user's photo library, but macOS is quite different.
Users will copy images straight from Finder, a browser, or just about anywhere else.
Sometimes they drag-and-drop; other times it's just a quick Command + V.
Understanding how users work with these methods is key to getting feel of "native" into your app on Mac.
The following guide goes through each of the methods so that you can handle images from Finder, browsers, and more when the users wants to paste them into their app.
Drag and drop: the classic way
Mac users drag and drop all the time; it's second nature. When someone drags a PNG or JPEG from Finder or their browser, they expect it to "just work".
Here is a snippet in SwiftUI that handles images that are dropped using :
@State private var pastedImage: NSImage?
.onDrop(of: [.png, .jpeg], isTargeted: nil) { providers in
for provider in providers {
drop(from: provider)
}
return true
}
func drop(from provider: NSItemProvider) {
if provider.canLoadObject(ofClass: NSImage.self) {
provider.loadObject(ofClass: NSImage.self) { obj, error in
if let image = obj as? NSImage {
Task { @MainActor in
self.pastedImage = image
}
} else {
print("Couldn’t load NSImage", error ?? "")
}
}
}
}This allows your users to take any image from the web or Finder and drag it right into your SwiftUI view. It feels instant—and it saves them clicks.
Paste from clipboard
Copy-paste is universal, but clipboard behaviour varies on macOS depending on where the image comes from:
From browsers or other apps: The clipboard retains the actual image data.
From Finder: the clipboard only retains the file path, rather than the image data.
Now let's look at how you can support both use cases.
Copying images from browsers or apps
If your user copies an image from, let's say, Safari or Chrome, the image is put on the clipboard directly. Here's how to read it:
.onKeyPress { event in
let pb = NSPasteboard.general
// Check for Command + V
if event.key == KeyEquivalent("v"), event.modifiers.contains(.command) {
if pb.canReadObject(forClasses: [NSImage.self], options: nil),
let images = pb.readObjects(forClasses: [NSImage.self], options: nil) as? [NSImage],
let image = images.first {
Task { @MainActor in
pastedImage = image
}
}
return .handled // Optional: suppress default paste
}
return .ignored
}Copying image files from Finder
Copying an image file from Finder works differently: the clipboard retains file URLs rather than the image data.
So you will need to check for URLs and load the image from disk.
let pb = NSPasteboard.general
let imageExtensions: Set<String> = ["png", "jpg", "jpeg"]
if pb.canReadObject(forClasses: [NSURL.self], options: nil),
let urls = pb.readObjects(forClasses: [NSURL.self], options: nil) as? [URL],
let url = urls.first {
let ext = url.pathExtension.lowercased()
if imageExtensions.contains(ext), let image = NSImage(contentsOf: url) {
Task { @MainActor in
pastedImage = image
}
} else {
print("Not an image file, or failed to load: \(url.lastPathComponent)")
}
}Supporting both cases: Why order matters
Previously, we examined how to process pasted images from browsers (image data) and Finder (file URLs) separately.
But in real life, users expect no matter how they copy something, it will work regardless of the specific source.
So from a design standpoint it is common to simply combine these two checks.
The order is important.
You should first check if there is a file URL in the clipboard. If you use NSImage first with a Finder copy, you typically get a generic icon, not the original image. When you load via URL, you get the original content.
If there is not a file URL, now you check for image data (from browsers and screenshot tools, design apps, etc.).
Combining the two flows in that order ensures you return your user's intended image regardless of how they copied it.
Here is what that looks like in code:
func paste() {
let pb = NSPasteboard.general
let imageExtensions: Set<String> = ["png", "jpg", "jpeg"]
if pb.canReadObject(forClasses: [NSURL.self], options: nil),
let urls = pb.readObjects(forClasses: [NSURL.self], options: nil) as? [URL],
let url = urls.first {
let ext = url.pathExtension.lowercased()
if imageExtensions.contains(ext), let image = NSImage(contentsOf: url) {
Task { @MainActor in
pastedImage = image
}
return
}
}
if pb.canReadObject(forClasses: [NSImage.self], options: nil),
let images = pb.readObjects(forClasses: [NSImage.self], options: nil) as? [NSImage],
let image = images.first {
Task { @MainActor in
pastedImage = image
}
return
}
print("No supported image found on the clipboard")
}Take pasted content anywhere
One easy-to-miss detail: onKeyPress works out of the box with List or ScrollView.
But if you’re using a VStack or HStack, nothing will happen unless the view is focusable.
Just add .focusable() to enable key handling.
VStack {
// Your views here
}
.focusable()
.onKeyPress { event in
// Handle Command + V
}
Takeaways
By supporting drag and drop, as well as clipboard pasted content, you are creating a macOS application that "feels" authentic to use, so to speak. Users expect these interactions, especially within the Mac ecosystem.
By using the patterns described, you will cover practically every edge case to allow dragging, dropping, copying, and pasting from the most utilitarian of sources including from a browser, the Finder, and any other needed sources of content.
You should add it, and your users will notice. Or better yet, they won't notice because it just works.

