简体   繁体   中英

macOS SwiftUI application - Get URL from Drag and drop Finder Files onto Dock or View

As an exercise, I'm trying to port a working Swift AppKit program to SwiftUI (for macOS ). My program takes files dragged from the Finder onto the Dock and processes their URLs on the backend, largely independent of Apple APIs. So I'm trying to receive the URLs of files dragged from the Finder onto my Dock icon or View - I'm not looking for file contents .

Onto Dock:

I failed to capture URLs from files dragged to the Dock using an AppDelegateAdapter, I think for obvious reasons but I thought I might get lucky. The program does accept file/files dragged onto the Dock, but just opens another instance of the View - exactly one per drag, regardless of number of files.

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

Onto View:

My View has two boxes which respond to drags of URLs or Text, respectively. I cannot figure out how to coax the dragged data from the DropInfo/NSItemsProvider.

After many hours, I got the struct TextDropDelegate to compile, but it doesn't work - always comes up with nil. Since I have no idea of the correct semantics for the simple case of Strings, I gave up on URLDropDelegate. The latter is harder because of the unclear support for URL/NSURL type support with NSSecureCoding. Plus, it is possible/likely that more than one URL will be dropped; every example I can find limits the processing of itemProviders to the first item. I can't get loops/iterators on it to compile.

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
             }
     }
 }
*/ 

I am not an experienced Swift programmer, so gentle help is most welcome.

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

Optional Rant : The sluggish uptake of SwiftUI on macOS is no great surprise. The documentation is poor and heavily emphasizes iOS even where crossplatform usage can differ. It was not clear to me (until I stumbled on the Apple video mentioned in source code) that familiar UTIs ("public.jpeg") are not the same as UTTypes, which are documented as "Uniform Type Identifiers". (Xcode still uses the old style UTIs in the.plist Document Types.)

Consider this fragment from my code: ...info.itemProviders(for:[ .text ])... It compiles without "import UniformTypeIdentifiers". But making the type explicit -...info.itemProviders(for:[ UTType.text ])... - won't compile without the import. What does the compiler consider to be the type/value of [ .text ] without the import?

This is one of the many frustrations - limited discussion of desktop-oriented features on the web, barebones Apple sample files that don't compile/run (at least with my setup), etc. - that make using SwiftUI on macOS a chore.

Thanks to this tool, https://swiftui-lab.com/companion/ , and this site, https://swiftui-combine.com/posts/ultimate-guide-to-swiftui2-application-lifecycle/ , I found a partial solution to the first part of my question - opening files from drag onto app's Dock icon.

Starting with a fresh project, Project->Info->Document Types->Identifier set to "public.item" (opens anything), and

import SwiftUI

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

Simple enough. Unfortunately, it only responds 1-3 times (seemingly random) regardless of number of files (>=3) dragged. Unless I'm missing something, this SwiftUI inadequacy is a show stopper for my project. Plus I'd really want to receive an Open message in the app's model, not create a stack of new Views. And ideally the dragged items would arrive grouped in an array, like NSApplicationDelegate, for pre-processing.

SwiftUI is elegant, and useful for gadgets, but spotty implementation and woeful documentation (should have been like https://swiftui-lab.com/companion/ from the start) suggest it's not ready to stand on its own for full-featured desktop programming. Thanks to all who responded!

A modicum of improvement from the earlier (self-)answer - replace the content of the body scene with:

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

.handlesExternalEvents(preferring: ...) suppresses the creation of new content views (= windows in macOS) with each drop. Using "*" matches every external event. This probably is not a good idea. Using a better method is left as an exercise to the reader, but please share.

In .onOpenURL(...) , I bypass the view and send the url straight to the ViewModel - not documented here. This is useful for my application, but...

Only one url per drag is passed, even if many files are part of the drag operation. An NSApplicationDelegate in AppKit (eg, in OP, "Onto Dock" section) receives an array of filenames that includes every url dragged onto the Dock in a single drag action.

Apparently, SwiftUI.onOpenURL() doesn't have this capacity. Still a big problem. Any ideas?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM