跳转至
Protocol

DropDelegate

An interface to easily perform drag & drop operations.

Declaration

protocol DropDelegate

Overview

The DropDelegate protocol offers functionality to customize drag and drop behaviors. It is preffered over onDrop(of:isTargeted:perform:) view modifier when your drop behavior requires non-standard implementations.

DropDelegate heavily utilizes NSItemProvider, which provides information about the dragged data.

Setup

DropDelegate has one required implementation and four optional implementations.

Required:

Optional:

Creating a simple Drag & Drop

Create a draggable View

Make a view draggable with the .onDrag(_:) modifier.

Use NSItemProvider to define the specific data dragged from that view.

Text(text)
    .font(.title)
    .onDrag { NSItemProvider(object: "🍌🍌" as NSString) }

Creating a drop View

Use onDrop to create a view that accepts "drops" from dragged data. There are three versions of the onDrop modifier:

struct ExampleView: View {
    @State var text: String = "🍌🍌"

    var body: some View {
        HStack {
            // Text to drag
            Text(text)
                .font(.title)
                .onDrag { NSItemProvider(object: self.text as NSItemProviderWriting) }

            // Area to drop
            RoundedRectangle(cornerRadius: 10)
                .frame(width: 150, height: 150)
                .onDrop(of: ["text"], isTargeted: nil, perform: { \_ in
                    self.text = "Dropped My Bananas 🍌🍌!"
                    return true
                })
        }
    }
}
Simple Drop

Conforming to DropDelegate

Implement performDrop(info:) to create a structure that conforms to DropDelegate.

struct ExampleView: View {
    @State var text: String = "🍌🍌"

    var body: some View {
        HStack {
            // Text to drag
            Text(text)
                .font(.title)
                .onDrag { NSItemProvider(object: self.text as NSString) }

            // Area to drop
            RoundedRectangle(cornerRadius: 10)
                .frame(width: 150, height: 150)
                .onDrop(of: ["text"], delegate: MyDropDelegate(text: $text))
        }
    }
}

struct MyDropDelegate: DropDelegate {
    @Binding var text: String

    func performDrop(info: DropInfo) -> Bool {
        self.text = "Dropped My Bananas 🍌🍌!"
        return true
    }
}
Simple Drop

Using DropInfo for custom logic

DropInfo provides information about the drop and is used to create custom drop behaviors.

For example, say your user drags & drops NSString data. Use the itemProviders(for:) to get an array of NSItemProvider data (recall all dragged data arrives in this format).

Next, use NSItemProvider's property loadItem to extract an NSSecureCoding from your dragged data.

Finally, cast your NSSecureCoding data to the more Swift-friendly Data object. From here your program can decode that data into a string and run any custom behaviors from that string.

The view in the example below is conditionally colored depending on the dragged string.

struct ExampleView: View {
    @State var backgroundColor: Color = .black
    let fruits: [String] = ["🍌🍌", "🍏🍏", "🍑🍑"]

    var body: some View {
        VStack {
            HStack {
                ForEach(self.fruits, id: \.self, content: { fruit in
                    Text(fruit)
                        .font(.title)
                        .onDrag { NSItemProvider(object: fruit as NSString) }
                })
            }

            HStack {
                RoundedRectangle(cornerRadius: 10)
                    .fill(backgroundColor)
                    .frame(width: 150, height: 150)
                    .onDrop(of: ["public.text"], delegate: MyDropDelegate(color: $backgroundColor))
            }
        }

    }
}

struct MyDropDelegate: DropDelegate {
    @Binding var color: Color

    // This function is executed when the user "drops" their object
    func performDrop(info: DropInfo) -> Bool {
        // Check if there's an array of items with the URI "public.text" in the DropInfo
        if let item = info.itemProviders(for: ["public.text"]).first {
            // Load the item
            item.loadItem(forTypeIdentifier: "public.text", options: nil) { (text, err) in
                // Cast NSSecureCoding to Ddata
                if let data = text as? Data {
                    // Extract string from data
                    let inputStr = String(decoding: data, as: UTF8.self)

                    // Conditionally change color given text string
                    if inputStr == "🍌🍌" {
                        self.color = .yellow
                    } else if inputStr == "🍏🍏" {
                        self.color = .green
                    } else if inputStr == "🍑🍑" {
                        self.color = .pink
                    } else {
                        self.color = .gray
                    }
                }
            }
        } else {
            // If no text was received in our drop, return false
            return false
        }

        return true
    }
}
Simple Drop

Utilize DropDelegates optional functions to provide additional behavior.

struct ExampleView: View {
    @State var backgroundColor: Color = .black
    let fruits: [String] = ["🍌🍌", "🍏🍏", "🍑🍑"]

    var body: some View {
        VStack {
            HStack {
                ForEach(self.fruits, id: \.self, content: { fruit in
                    Text(fruit)
                        .font(.title)
                        .onDrag { NSItemProvider(object: fruit as NSString) }
                })
            }

            HStack {
                RoundedRectangle(cornerRadius: 10)
                    .fill(backgroundColor)
                    .frame(width: 150, height: 150)
                    .onDrop(of: ["public.text"], delegate: MyDropDelegate(color: $backgroundColor))
            }
        }

    }
}

struct MyDropDelegate: DropDelegate {
    @Binding var color: Color

    // Drop entered called
    func dropEntered(info: DropInfo) {
        /// Change color if color was previously black
        self.color = (self.color == .black) ? .gray : self.color
    }

    // Drop entered called
    func dropExited(info: DropInfo) {
        self.color = .init(white: 0.40)
    }

    // Drop has been updated
    func dropUpdated(info: DropInfo) -> DropProposal? {
        /// Don't allow more items to be dropped if a Banana was dropped
        if self.color == .yellow {
            return DropProposal(operation: .forbidden)
        } else {
            return nil
        }
    }

    // This function is executed when the user "drops" their object
    func performDrop(info: DropInfo) -> Bool {
        // Check if there's an array of items with the URI "public.text" in the DropInfo
        if let item = info.itemProviders(for: ["public.text"]).first {
            // Load the item
            item.loadItem(forTypeIdentifier: "public.text", options: nil) { (text, err) in
                // Cast NSSecureCoding to Ddata
                if let data = text as? Data {
                    // Extract string from data
                    let inputStr = String(decoding: data, as: UTF8.self)

                    // Conditionally change color given text string
                    if inputStr == "🍌🍌" {
                        self.color = .yellow
                    } else if inputStr == "🍏🍏" {
                        self.color = .green
                    } else if inputStr == "🍑🍑" {
                        self.color = .pink
                    } else {
                        self.color = .gray
                    }
                }
            }
        } else {
            // If no text was received in our drop, return false
            return false
        }

        return true
    }
}

This example uses dropUpdated(info:) to prevent fruits from being dropped if the background is yellow.

The example uses dropEntered(info:) to change the color the first time a user drags over the drop zone.

Finally, when a user drags out of the view, dropExited(info:) changes the background color to a dark gray.

Note: if the user deselects their dragged object while over the drop zone, dropExited(info:) will not be called. dropExited(info:) is only called when the user explicitly drags their dragged object out of the drop zone.

Bug: On iOS DropInfo provides its location in global coordinates. It should provide location in local coordinates.

Availability

iOS 13.4+

macOS 10.15+

Topics


Instance Method

dropEntered(info:) Provide custom behavior when an object is dragged over the onDrop view.

dropEntered(info:) Provide custom behavior when an object is dragged over the onDrop view.

dropExited(info:) Provide custom behavior when an object is dragged off of the onDrop view.

dropExited(info:) Provide custom behavior when an object is dragged off of the onDrop view.

dropUpdated(info:) Provide custom behavior when the drop is updated.

dropUpdated(info:) Provide custom behavior when the drop is updated.

performDrop(info:) Specifies the behavior of a drop.

performDrop(info:) Specifies the behavior of a drop.

validateDrop(info:) Validates a drop.

validateDrop(info:) Validates a drop.