提问者:小点点

macOS SwiftUI 应用程序 - 从拖放 Finder 文件到停靠或视图获取 URL


作为练习,我正在尝试将一个有效的 Swift AppKit 程序移植到 SwiftUI(适用于 macOS)。我的程序将从 Finder 拖到程序坞的文件,并在后端处理它们的 URL,这在很大程度上独立于 Apple API。所以我正在尝试接收从 Finder 拖到我的 Dock 图标或视图上的文件的 URL - 我不是在查找文件内容。

在码头上:

我未能使用AppDelegateAdapter从拖到Dock的文件中捕获URL,我认为原因很明显,但我认为我可能会幸运。该程序确实接受拖到Dock上的文件,但只打开视图的另一个实例-无论文件数量如何,每次拖动都只打开一个。

import SwiftUI

class AppDelegate: NSObject, NSApplicationDelegate {
    // application receives something on drag to Dock icon - it opens a new View
    // undoubtedly ignored because there's no NSApplication instance
    func application(_ sender: NSApplication, openFiles filenames: [String]) {
        for name in filenames{
            print("This is not called when file dropped on app's Dock icon: \(name)")
        }
    }

    func applicationDidFinishLaunching(_ notification: Notification) {
        print("This works")
    }
}

@main
struct TestSwift_DnDApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    var body: some Scene {
        WindowGroup {
            DualPasteboardView().navigationTitle("Pasteboard Test")
        }
    }
}

上视图:

我的视图有两个框,分别响应URL或文本的拖动。我不知道如何从DropInfo/NSItemsProvider诱骗拖动的数据。

几个小时后,我得到了要编译的结构TextDropDelegate,但它不起作用——总是得到nil。由于我不知道字符串这种简单情况的正确语义,所以我放弃了URLDropDelegate。后者更难,因为NSSecureCoding对URL/NSURL类型支持的支持不明确。此外,可能/可能会删除多个URL;我能找到的每个示例都将itemProvider的处理限制在第一个项目。我无法在其上获取循环/迭代器进行编译。

import SwiftUI
import UniformTypeIdentifiers  // not clear if required

/*
 starting point for view design:
 https://swiftontap.com/dropdelegate
 UTType description and pasteboard example source in comment below:
 https://developer.apple.com/videos/play/tech-talks/10696 -
 */

struct DualPasteboardView: View {
    var urlString = "Drag me, I'm a URL"
    var textString = "Drag me, I'm a String"
    var body: some View {
        VStack{
            VStack{
                Text("Labels can be dragged to a box")
                Text("Red Box accepts URLs dragged").foregroundColor(.red)
                Text("(simulated or from Finder)").foregroundColor(.red)
                Text("Green Box accepts Text").foregroundColor(.green)
            }.font(.title)
            
            HStack {
                Text(urlString)
                    .font(.title)
                    .foregroundColor(.red)
                    .onDrag { NSItemProvider(object: NSURL())}
                    //bogus url for testing d'n'd, ignore errors

                // Drop URLs here
                RoundedRectangle(cornerRadius: 0)
                    .frame(width: 150, height: 150)
                    .onDrop(of: [.url], delegate: URLDropDelegate())
                    .foregroundColor(.red)
                    
            }
            HStack {
                Text(textString)
                    .font(.title)
                    .foregroundColor(.green)
                    .onDrag { NSItemProvider(object: textString as NSString)}
                
                // Drop text here
                RoundedRectangle(cornerRadius: 0)
                    .frame(width: 150, height: 150)
                    .foregroundColor(.green)
                    .onDrop(of: [.text], delegate: TextDropDelegate())
                    /*.onDrop(of: [.text], isTargeted: nil ){ providers in
                            _ = providers.first?loadObject(of: String.self){
                            string, error in
                            text = string
                            }
                        return true
                        } // see comment below for approx. syntax from Apple tech talk */
                    }
        }.frame(width: 500, height: 500) //embiggen the window
    }
}

struct URLDropDelegate: DropDelegate {
    func performDrop(info: DropInfo) -> Bool {
        //no idea
        return true
    }
}

struct TextDropDelegate: DropDelegate {
    func performDrop(info: DropInfo) -> Bool {
        var tempString: String?
        _ = info.itemProviders(for:[.text]).first?.loadObject(ofClass: String.self) {
            string, error in
            tempString = string
        }
        print(tempString ?? "no temp string")
        // always prints 'no temp string'
        // should be "Drag me, I'm a String"
        return true
    }
    
    func validateDrop(info: DropInfo) -> Bool {
        //deliberately incorrect UTType still returns true
        return info.itemProviders(for: [.fileURL]).count > 0
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        DualPasteboardView()
    }
}

/* syntax used in  https://developer.apple.com/videos/play/tech-talks/10696/ @22:27
(this generates compiler errors, including in .onDrop(...), and doesn't make sense to me either)
 
 struct MyGreatView:View{
     @State var text: String? = nil
     
     var body: some View{
         MyGreatView(content: $text)
             .onDrop(of: [.text], isTargeted:nil){providers in
                 _ = providers.first?loadObject(of: String.self){
                     string, error in
                     text = string
                 }
                 return true
             }
     }
 }
*/ 

我不是一个有经验的Swift程序员,所以温柔的帮助是最受欢迎的。

(使用XCode 13.2.1,Big Sur 11.6.2,SwiftUI 2.x?)

可选咆哮:macOS上SwiftUI的缓慢使用并不令人惊讶。留档很差,非常强调iOS即使跨平台使用可能不同。我不清楚(直到我偶然发现源代码中提到的Apple视频)熟悉的UTI(“public. jpeg”)与UTTypes不同,UTTypes被记录为“统一类型标识符”。(Xcode仍然在.plist文档类型中使用旧式UTI。)

考虑我的代码中的这个片段:...info.itemProviders(用于:[。文本])...它在没有“导入UniformTypeIdentifiers”的情况下编译。但是将类型显式化-...info.itemProviders(用于:[UTType.text])...-没有导入将无法编译。编译器认为什么是[的类型/值。text]没有导入?

这是许多令人沮丧的事情之一——网上对面向桌面的特性的有限讨论,不能编译/运行(至少在我的设置下)的苹果示例文件等。-这使得在macOS上使用SwiftUI变得很麻烦。

与之前的(自我)回答相比有一点改进-将身体场景的内容替换为:

        WindowGroup {
            let vm = ViewModel()
            ContentView(viewModel: vm)
                .frame(minWidth: 800, minHeight: 600)
                .handlesExternalEvents(preferring: ["*"], allowing: ["*"])
                .onOpenURL{ url in
                    vm.appendToModel(url: url)
                }
        }

.handlesExternalEvents(preferring: ...)禁止在每次放置时创建新内容视图(= macOS 中的窗口)。使用“*”匹配每个外部事件。这可能不是一个好主意。使用更好的方法留给读者作为练习,但请分享。

.onOpenURL(…)中,我绕过视图并将url直接发送到ViewModel-此处未记录。这对我的应用程序很有用,但。。。

每次拖动只传递一个url,即使许多文件是拖动操作的一部分。AppKit中的NSApplication ationAgent ate(例如,参见上面的“Onto Dock”部分)接收一个文件名数组,其中包括在单个拖动操作中拖到Dock上的每个url。

很明显,斯威夫特。onOpenURL()没有这个能力。还是个大问题。有什么想法吗?


共2个答案

匿名用户

多亏了这个工具,https://swiftui-lab.com/companion/ ,以及这个网站,https://swiftui-combine.com/posts/ultimate-guide-to-swiftui2-application-lifecycle/,我找到了我问题第一部分的部分解决方案 - 从拖动到应用程序的 Dock 图标打开文件。

从一个新的项目开始,项目-

import SwiftUI

@main
struct TestOnOpenURLApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView().onOpenURL{url in
                print(url.lastPathComponent)
            }
        }
    }
}

很简单。不幸的是,无论文件数量如何,它只响应1-3次(似乎是随机的)(

SwiftUI很优雅,对小工具很有用,但是参差不齐的实现和糟糕的留档(从一开始就应该是https://swiftui-lab.com/companion/)表明它还没有准备好独立进行全功能桌面编程。感谢所有回复的人!

匿名用户

不知道这样会不会有帮助,最近一直在解决这个问题。事实证明,从2023年1月11日起,有一种方法可以使用app delegate在doc图标或应用程序的finder图标上放置多个URL。在我的例子中,我只对搜索删除的目录感兴趣,所以我把URL保存在一个数组中

struct MyApp: App {
    @NSApplicationDelegateAdaptor private var appDelegate: AppDelegate
    var body: some Scene {
        Window("MyApp", id: "main") {
            ContentView()
        }
    }
}
    
class AppDelegate: NSObject, NSApplicationDelegate {
    func application(_ sender: NSApplication, open urls: [URL]) {
    // Use open, not openFiles!
        print("appDelegate dropped urls:", urls) // diagnostic
    }
}