Search code examples
macostypesswiftuidrop

is anyone able to restrict the type of the objects dropped on the mac in SwiftUI 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.


Solution

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