【问题标题】:How can my OS X app accept drag-and-drop of picture files from Photos.app?我的 OS X 应用程序如何接受从 Photos.app 拖放图片文件?
【发布时间】:2017-12-24 20:19:27
【问题描述】:

从新的 Photos.app 中拖动图片时,粘贴板中不会传递任何 URL 作为拖动信息的一部分。我的应用程序已经正确处理从例如传递的图像。 iPhoto、Photo Booth、Aperture、...

我尝试从 Photos.app 中拖动图片:Finder 或 Pages 可以正确处理,但 TextEdit 或 Preview 不能。 Photos.app 处理存储在其库中的图片的方式似乎有所不同。

【问题讨论】:

    标签: objective-c cocoa


    【解决方案1】:

    在深入了解 NSPasteboard 并逐步浏览该应用程序后,我意识到 Photos.app 正在粘贴板中传递“承诺的文件”,并在 Apple 的邮件列表中找到了此线程并提供了一些答案:http://prod.lists.apple.com/archives/cocoa-dev/2015/Apr/msg00448.html

    这是我最终解决的方法,在处理将文件拖放到文档的类中。该类是处理常用拖放方法的视图控制器,因为它位于响应者链中。

    一种便捷的方法检测拖动的发送者是否有任何文件相关的内容:

    - (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;
    }
    

    我还有一种方法可以在它不是“承诺文件”的情况下提取 URL:

    - (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];
    }
    

    这是最后用于处理跌落的方法。这不完全是我的代码,因为我简化了拖入方便方法的内部处理,这些方法允许我隐藏特定于应用程序的部分。我还有一个处理文件系统事件的特殊类FileSystemEventCenter 留给读者作为练习。此外,在这里介绍的情况下,我只处理拖动 一个 文件。您必须根据自己的情况调整这些部分。

    - (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;
                 }
             }
        });
    }
    

    【讨论】:

    • 感谢您的回答。我已经实现了您的解决方案,并且正在通过照片应用程序将文件复制到临时路径中,但不幸的是,它似乎将它们全部转换为 JPG(而不是将它们保留为 PNG),这对我的应用程序很重要。我想知道是否有其他人遇到过同样的问题,是否有任何方法可以指定“不转换文件”? hasFileURLOrPromisedFileURLWithDraggingInfo 似乎已被弃用,但我找不到解决方案... promises API 似乎非常陈旧而且非常损坏 IMO,所以我目前的“解决方案”是放弃对它的支持...
    • @Sam 我相信这是特定于照片的。无论图片的原始格式是什么,它都会返回 JPG。尝试将图片拖到 Finder 或预览中,看看会发生什么。
    • 澄清一下,Sam:我的意思是从照片拖到 Finder 会生成 jpg。它是 jpg 的事实与 promise API 无关:这正是 Photos 所做的。它不会给你原始文件。即使它没有使用 promise API,应用程序也可以决定交出 jpg(但它必须动态生成它并在拖动开始之前准备好它)。我没有测试过,只是我的期望。
    • 啊好的。非常感谢您的澄清。听起来远离照片应用是值得的;)
    【解决方案2】:

    Apple 在 10.12 中使用 NSFilePromiseReceiver 使这变得更容易了。这仍然是一个漫长而繁琐的过程,但要少一些。

    这就是我的做法。我实际上已经将它拆分为一个扩展,但我已经为这个例子简化了它。

        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
    
                })
    }
    

    【讨论】:

    • 我用了你的方法。但是,在我手动关闭应用程序之前,该操作不会运行。我的意思是,在我关闭我的应用程序之前,图像不会显示在缓存文件夹中。有什么建议吗?
    • 有趣。听起来像僵局。您是否在其他地方重用相同的操作队列?您是否试图在文件写入之前读取文件而挂在某个地方?
    • 您的方法适用于 Safari。来自 Safari 的图像会立即出现在缓存文件夹中。但是,使用照片,直到我手动退出我的应用程序才会显示图像。我认为这可能是照片的错误。但是,Mail.app 可以完美地从照片中获取图像。因此,我请求 Apple 提供技术支持。
    • 我在照片应用中使用了这种技术,它对我很有效。也就是说,这不仅仅是从我自己的应用程序中复制和粘贴。我很确定这最终会成为您在那里遇到的一个微妙的线程问题。
    • 它适用于 Safari。所以它也必须与照片一起使用,对吧?但我的情况正好相反。如果有任何问题,Safari 也不应该工作。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-04
    • 1970-01-01
    • 1970-01-01
    • 2015-04-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多