3

as per the documentation, it should be pretty straightforward. example for a List: https://developer.apple.com/documentation/swiftui/list/ondrop(of:istargeted:perform:)-75hvy#

the UTType should be the parameter restricting what a SwiftUI object can receive. in my case i want to accept only Apps. the UTType is .applicationBundle: https://developer.apple.com/documentation/uniformtypeidentifiers/uttype/3551459-applicationbundle

but it doesn't work. the SwiftUI object never changes status and never accepts the drop. the closure is never run. whether on Lists, H/VStacks, Buttons, whatever. the pdf type don't seem to work either, as well as many others. the only type that i'm able to use if fileURL, which is mainly like no restriction.

i'm not sure if i'm doing something wrong or if SwiftUI is half working for the mac.

here's the code:

List(appsToIgnore, id: \.self, selection: $selection) {
    Text($0)
}
.onDrop(of: [.applicationBundle, .application], isTargeted: isTargeted) { providers in
    print("hehe")
    
    return true
}

replacing or just adding .fileURL in the UTType array makes the drop work but without any type restriction.

i've also tried to use .onInsert on a ForEach instead (https://developer.apple.com/documentation/swiftui/foreach/oninsert(of:perform:)-2whxl#), and to go through a proper DropDelegate (https://developer.apple.com/documentation/swiftui/dropdelegate#) but keep getting the same results. it would seem the SwiftUI drop for macOS is not yet working, but i can't find any official information about this. in the docs it is written macOS 11.0+ so i would expect it to work?

any info appreciated! thanks.

godbout
  • 1,765
  • 1
  • 12
  • 11
  • 2
    If you drag from Finder then there is no `.application` in pasteboard, but `.fileURL` does exists. Why do you expect `.application`? – Asperi Oct 29 '21 at 15:47
  • @Asperi so that's the thing. i was afraid i misunderstood something and it seems to be the case. i thought that specifying the UTType in the .onDrop was sufficient. i have/had no idea about what type showed up in the pasteboard. thanks for pointing this out. will investigate. – godbout Oct 30 '21 at 05:42
  • Can't you just check the dropped file's path to see if it has the .app extension or not? – arata Oct 30 '21 at 19:45
  • 1
    @arata the idea is to prevent the ability to drop a file that is not an application, with the UI correctly updated (no + sign, etc...). the way to achieve that is through the `validateDrop` func, as @Asperi stated. if you return false from `validateDrop`, then the user can't drop the file and the UI is updated accordingly. – godbout Oct 31 '21 at 03:55

1 Answers1

4

You need to validate manually, using DropDelegate of what kind of file is dragged over.

Here is a simplified demo of possible approach. Tested with Xcode 13 / macOS 11.6

let delegate = MyDelegate()

...


List(appsToIgnore, id: \.self, selection: $selection) {
    Text($0)
}
.onDrop(of: [.fileURL], delegate: delegate) // << accept file URLs

and verification part like

class MyDelegate: DropDelegate {

    func validateDrop(info: DropInfo) -> Bool {
        // find provider with file URL
        guard info.hasItemsConforming(to: [.fileURL]) else { return false }
        guard let provider = info.itemProviders(for: [.fileURL]).first else { return false }

        var result = false
        if provider.canLoadObject(ofClass: String.self) {
            let group = DispatchGroup()
            group.enter()     // << make decoding sync

            // decode URL from item provider
            _ = provider.loadObject(ofClass: String.self) { value, _ in
                defer { group.leave() }
                guard let fileURL = value, let url = URL(string: fileURL) else { return }

                // verify type of content by URL
                let flag = try? url.resourceValues(forKeys: [.contentTypeKey]).contentType == .applicationBundle
                result = flag ?? false
            }

            // wait a bit for verification result
            _ = group.wait(timeout: .now() + 0.5)
        }
        return result
    }

    func performDrop(info: DropInfo) -> Bool {
        // handling code is here
        return true
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • thanks. i definitely overlooked that `validateDrop` thing. will investigate, and mark the answer if that's what i'm looking for. – godbout Oct 30 '21 at 05:43
  • that works beautifully, thank you. i clearly misunderstood the `onDrop` parameters, and overlooked the `validateDrop`. this is exactly what i was looking for. thanks again! – godbout Oct 30 '21 at 06:00
  • i'm trying to reach that solution by myself from scratch and holy cow i'm realizing how this is golden. thanks a lot @Asperi :D – godbout Oct 31 '21 at 04:16
  • any reason why loadingObject of Class String rather than URL? wouldn't the second guard of the `validateDrop` func take care of making sure we get a URL? (had no idea about DispatchGroup also. thanks. quite a few new learnings from your answer.) – godbout Oct 31 '21 at 14:21
  • @godbout I also recommend you to read about`DispatchSemaphore` if you haven't heard of it. You can read more here about `DispatchGroup` vs `DispatchSemaphore`: https://stackoverflow.com/a/49925414/6458677 – arata Nov 01 '21 at 04:21
  • i'm finding that this code is leaking. no idea yet where's the issue, but it seems related to DispatchGroup. – godbout Nov 09 '21 at 16:22