When dragging pictures from the new Photos.app, no URL is passed in the pasteboard as part of the dragging info. My app already correctly handles images passed from e.g. iPhoto, Photo Booth, Aperture,...
I tried dragging pictures from Photos.app: Finder or Pages handle that properly, but not TextEdit or Preview. There seems to be something different about the way Photos.app works with the pictures stored in its library.
After digging into NSPasteboard and stepping through the app, I realized Photos.app is passing "promised files" in the pasteboard, and found this thread in an Apple's mailing list with some answers: http://prod.lists.apple.com/archives/cocoa-dev/2015/Apr/msg00448.html
Here is how I finally solved it, in the class that handles drag and drop of files into a document. The class is a view controller that handles the usual drag/drop methods because it's in the responder chain.
A convenience method detects wether the sender of a drag has any file-related content:
- (BOOL)hasFileURLOrPromisedFileURLWithDraggingInfo:(id <NSDraggingInfo>)sender
{
NSArray *relevantTypes = @[@"com.apple.pasteboard.promised-file-url", @"public.file-url"];
for(NSPasteboardItem *item in [[sender draggingPasteboard] pasteboardItems])
{
if ([item availableTypeFromArray:relevantTypes] != nil)
{
return YES;
}
}
return NO;
}
I also have a method to extract the URL in the case where it's not a "promised file":
- (NSURL *)fileURLWithDraggingInfo:(id <NSDraggingInfo>)sender
{
NSPasteboard *pasteboard = [sender draggingPasteboard];
NSDictionary *options = [NSDictionary dictionaryWithObject:@YES forKey:NSPasteboardURLReadingFileURLsOnlyKey];
NSArray *results = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]] options:options];
return [results lastObject];
}
Here is finally the method used to handle a drop. It's not quite exactly my code, as I simplified the internal handling of dragging into convenience methods that allow me to hide the parts specific for the app. I also have a special class for handling file system events FileSystemEventCenter
left as an exercise to the reader. Also, in the case presented here, I only handle dragging one file. You'll have to adapt those parts to your own case.
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
{
if ([self hasFileURLOrPromisedFileURLWithDraggingInfo:sender])
{
[self updateAppearanceWithDraggingInfo:sender];
return NSDragOperationCopy;
}
else
{
return NSDragOperationNone;
}
}
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
{
return [self draggingEntered:sender];
}
- (void)draggingExited:(id <NSDraggingInfo>)sender
{
[self updateAppearanceWithDraggingInfo:nil];
}
- (void)draggingEnded:(id <NSDraggingInfo>)sender
{
[self updateAppearanceWithDraggingInfo:nil];
}
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
{
return [self hasFileURLOrPromisedFileURLWithDraggingInfo:sender];
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
// promised URL
NSPasteboard *pasteboard = [sender draggingPasteboard];
if ([[pasteboard types] containsObject:NSFilesPromisePboardType])
{
// promised files have to be created in a specific directory
NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]];
if ([[NSFileManager defaultManager] createDirectoryAtPath:tempPath withIntermediateDirectories:NO attributes:nil error:NULL] == NO)
{
return NO;
}
// the files will be created later: we keep an eye on that using filesystem events
// `FileSystemEventCenter` is a wrapper around FSEvent
NSArray *filenames = [sender namesOfPromisedFilesDroppedAtDestination:[NSURL fileURLWithPath:tempPath]];
DLog(@"file names: %@", filenames);
if (filenames.count > 0)
{
self.promisedFileNames = filenames;
self.directoryForPromisedFiles = tempPath.stringByStandardizingPath;
self.targetForPromisedFiles = [self dropTargetForDraggingInfo:sender];
[[FileSystemEventCenter defaultCenter] addObserver:self selector:@selector(promisedFilesUpdated:) path:tempPath];
return YES;
}
else
{
return NO;
}
}
// URL already here
NSURL *fileURL = [self fileURLWithDraggingInfo:sender];
if (fileURL)
{
[self insertURL:fileURL target:[self dropTargetForDraggingInfo:sender]];
return YES;
}
else
{
return NO;
}
}
- (void)promisedFilesUpdated:(FDFileSystemEvent *)event
{
dispatch_async(dispatch_get_main_queue(),^
{
if (self.directoryForPromisedFiles == nil)
{
return;
}
NSString *eventPath = event.path.stringByStandardizingPath;
if ([eventPath hasSuffix:self.directoryForPromisedFiles] == NO)
{
[[FileSystemEventCenter defaultCenter] removeObserver:self path:self.directoryForPromisedFiles];
self.directoryForPromisedFiles = nil;
self.promisedFileNames = nil;
self.targetForPromisedFiles = nil;
return;
}
for (NSString *fileName in self.promisedFileNames)
{
NSURL *fileURL = [NSURL fileURLWithPath:[self.directoryForPromisedFiles stringByAppendingPathComponent:fileName]];
if ([[NSFileManager defaultManager] fileExistsAtPath:fileURL.path])
{
[self insertURL:fileURL target:[self dropTargetForDraggingInfo:sender]];
[[FileSystemEventCenter defaultCenter] removeObserver:self path:self.directoryForPromisedFiles];
self.directoryForPromisedFiles = nil;
self.promisedFileNames = nil;
self.targetForPromisedFiles = nil;
return;
}
}
});
}
Apple made this a bit easier in 10.12 with the NSFilePromiseReceiver. It's still a long a fiddly process, but a little less so.
Here's how I'm doing it. I've actually split this out into an extension, but I've simplified it for this example.
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
let pasteboard: NSPasteboard = sender.draggingPasteboard()
guard let filePromises = pasteboard.readObjects(forClasses: [NSFilePromiseReceiver.self], options: nil) as? [NSFilePromiseReceiver] else {
return
}
var images = [NSImage]()
var errors = [Error]()
let filePromiseGroup = DispatchGroup()
let operationQueue = OperationQueue()
let newTempDirectory: URL
do {
let newTempDirectory = (NSTemporaryDirectory() + (UUID().uuidString) + "/") as String
let newTempDirectoryURL = URL(fileURLWithPath: newTempDirectory, isDirectory: true)
try FileManager.default.createDirectory(at: newTempDirectoryURL, withIntermediateDirectories: true, attributes: nil)
}
catch {
return
}
filePromises.forEach({ filePromiseReceiver in
filePromiseGroup.enter()
filePromiseReceiver.receivePromisedFiles(atDestination: newTempDirectory,
options: [:],
operationQueue: operationQueue,
reader: { (url, error) in
if let error = error {
errors.append(error)
}
else if let image = NSImage(contentsOf: url) {
images.append(image)
}
else {
errors.append(PasteboardError.noLoadableImagesFound)
}
filePromiseGroup.leave()
})
})
filePromiseGroup.notify(queue: DispatchQueue.main,
execute: {
// All done, check your images and errors array
})
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With