Search code examples
swiftuidrag-and-drop

How do you mark a single container as a dropDestination for multiple Transferable types?


I'm using the new Transferable protocol with the draggable/dropDestination modifiers to let users drop content onto a ZStack. The issue I'm having is that I want to support multiple Transferable types being dropped into a single container. For example, I want users to be able to drop a String, a URL, or a Data (i.e., image data) onto a single ZStack. The problem is that the "for" parameter on the dropDestination view modifier does not accept multiple Types, like the onDrop modifier does.

I tried adding a second dropDestination modifier with a different payload, but when I drop an item corresponding to the second drop destination payload, I see an icon on the dragged image that indicates dropping is not allowed. However, if I drop a String payload, I get the + icon as I would expect, and the drop is successful.

struct ContentView: View {
    
    @State private var stringPayload: String = ""
    @State private var urlPayload: URL?
    
    var body: some View {
        VStack {
            ZStack {
                Color.yellow
                Text(stringPayload)
                if let urlPayload {
                    Image(uiImage: UIImage(data: (try? Data(contentsOf: urlPayload))!)!)
                }
            }
            .dropDestination(for: String.self) { items, location in
                stringPayload = items.first!
                return true
            }
            .dropDestination(for: URL.self) { items, location in
                return true
            }
            Text("Hello world!")
                .draggable("Hello world!")
        }
    }
}

Solution

  • Thanks to @user1046037's suggestion to look at ProxyRepresentation, I was able to put together some code that allows me to accept multiple drop types within a single dropDestination receiver.

    First, I created a separate enum to represent the different types of data that could be dropped:

    import CoreTransferable
    
    enum DropItem: Codable, Transferable {
        case none
        case text(String)
        case url(URL)
        
        static var transferRepresentation: some TransferRepresentation {
            ProxyRepresentation { DropItem.text($0) }
            ProxyRepresentation { DropItem.url($0) }
        }
        
        var text: String? {
            switch self {
                case .text(let str): return str
                default: return nil
            }
        }
        
        var url: URL? {
            switch self {
                case.url(let url): return url
                default: return nil
            }
        }
    }
    

    Then in the View, just tell the dropDestination to accept items of type DropItem.self, like this:

    struct ContentView: View {
        
        @State private var payload: DropItem = .none
        @State private var urlPayload: URL?
        
        var body: some View {
            VStack {
                ZStack {
                    Color.yellow
                    if let text = payload.text {
                        Text(text)
                    } else if let url = payload.url {
                        Text(url.absoluteString)
                    }
                }
                .dropDestination(for: DropItem.self) { items, location in
                    payload = items.first!
                    return true
                }
                Text("Hello world!")
                    .draggable("Hello world!")
            }
        }
    }
    

    What's really nice about this approach is that you can rely on strongly typed DropItems to determine what to do based on the case in the enum that is received.