Implementing Drag and Drop in SwiftUI

SwiftUI provides a declarative way to build user interfaces for Apple’s platforms. Implementing drag and drop functionality enhances user experience by allowing users to intuitively move and rearrange items within the app. In this guide, we’ll explore how to implement drag and drop in SwiftUI, covering essential concepts and code examples.

Understanding Drag and Drop in SwiftUI

Drag and drop in SwiftUI involves two main components:

  • Drag Source: The view from which the drag operation originates. The user initiates the drag from this view.
  • Drop Target: The view that receives the dragged content. This is where the dragged content is dropped.

Basic Implementation

To implement drag and drop, we’ll use the following modifiers:

  • .onDrag: Makes a view a drag source.
  • .onDrop: Makes a view a drop target.

Example 1: Simple Drag and Drop

Let’s start with a basic example where we can drag a text view and drop it onto another.


import SwiftUI

struct ContentView: View {
    @State private var text: String = "Drag Me!"
    @State private var receivedText: String = "Drop Here"

    var body: some View {
        VStack {
            Text(text)
                .padding()
                .background(Color.blue)
                .foregroundColor(.white)
                .onDrag {
                    NSItemProvider(object: NSItemProviderWriting {
                        func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
                            completionHandler(text.data(using: .utf8), nil)
                            return nil
                        }

                        static var writableTypeIdentifiersForItemProvider: [String] {
                            return ["public.utf8-plain-text"]
                        }
                    })
                }

            Text(receivedText)
                .padding()
                .background(Color.green)
                .foregroundColor(.white)
                .onDrop(of: ["public.utf8-plain-text"], isTargeted: nil) { providers in
                    providers.first()?.loadItem(forTypeIdentifier: "public.utf8-plain-text", options: nil) { (data, error) in
                        if let data = data as? Data, let newText = String(data: data, encoding: .utf8) {
                            DispatchQueue.main.async {
                                self.receivedText = newText
                            }
                        }
                    }
                    return true
                }
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Explanation:

  • .onDrag creates an NSItemProvider that handles the data being dragged. We specify that the dragged data is a UTF-8 plain text string.
  • .onDrop specifies the types of items the target can accept (public.utf8-plain-text in this case). It loads the item, converts it back to a string, and updates the receivedText state.

Advanced Drag and Drop

For more complex scenarios, you might want to drag and drop more structured data. Let’s implement an example where we drag and drop custom Item objects.

Example 2: Dragging and Dropping Custom Objects


import SwiftUI

struct Item: Identifiable, Codable {
    let id = UUID()
    var name: String
}

struct ComplexDragAndDropView: View {
    @State private var items: [Item] = [
        Item(name: "Item 1"),
        Item(name: "Item 2"),
        Item(name: "Item 3")
    ]
    @State private var droppedItems: [Item] = []

    var body: some View {
        HStack {
            VStack {
                Text("Items to Drag")
                    .font(.headline)
                List {
                    ForEach(items) { item in
                        Text(item.name)
                            .padding()
                            .background(Color.orange)
                            .foregroundColor(.white)
                            .onDrag {
                                do {
                                    let data = try JSONEncoder().encode(item)
                                    let itemProvider = NSItemProvider(item: data as NSData, typeIdentifier: "com.example.item")
                                    return itemProvider
                                } catch {
                                    print("Error encoding item: (error)")
                                    return NSItemProvider()
                                }
                            }
                    }
                }
            }

            VStack {
                Text("Dropped Items")
                    .font(.headline)
                List {
                    ForEach(droppedItems) { item in
                        Text(item.name)
                            .padding()
                            .background(Color.gray)
                            .foregroundColor(.white)
                    }
                }
                .onDrop(of: ["com.example.item"], isTargeted: nil) { providers in
                    providers.first()?.loadDataRepresentation(forTypeIdentifier: "com.example.item") { (data, error) in
                        if let data = data {
                            do {
                                let item = try JSONDecoder().decode(Item.self, from: data)
                                DispatchQueue.main.async {
                                    self.droppedItems.append(item)
                                    self.items.removeAll { $0.id == item.id }
                                }
                            } catch {
                                print("Error decoding item: (error)")
                            }
                        }
                    }
                    return true
                }
            }
        }
        .padding()
    }
}

struct ComplexDragAndDropView_Previews: PreviewProvider {
    static var previews: some View {
        ComplexDragAndDropView()
    }
}

Explanation:

  • Item struct is defined to represent custom objects. It conforms to Identifiable and Codable to enable identification and serialization/deserialization.
  • In the drag source, the Item is encoded to JSON data and provided through NSItemProvider. A custom type identifier “com.example.item” is used.
  • In the drop target, the data is loaded, decoded back into an Item, and added to the droppedItems list.

Drag and Drop with Reordering

A common use case is reordering items within a list. We’ll use the same Item structure but allow items to be reordered in a list.

Example 3: Reordering Items in a List


import SwiftUI

struct ReorderableDragAndDropView: View {
    @State private var items: [Item] = [
        Item(name: "Item 1"),
        Item(name: "Item 2"),
        Item(name: "Item 3"),
        Item(name: "Item 4")
    ]

    @State private var draggingItem: Item?

    var body: some View {
        List {
            ForEach(items) { item in
                Text(item.name)
                    .padding()
                    .background(draggingItem?.id == item.id ? Color.gray.opacity(0.5) : Color.white)
                    .onDrag {
                        draggingItem = item
                        return NSItemProvider(object: NSItemProviderWriting {
                            func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
                                do {
                                    let data = try JSONEncoder().encode(item)
                                    completionHandler(data, nil)
                                } catch {
                                    completionHandler(nil, error)
                                }
                                return nil
                            }

                            static var writableTypeIdentifiersForItemProvider: [String] {
                                return ["com.example.item"]
                            }
                        })
                    }
                    .onDrop(of: ["com.example.item"], isTargeted: nil) { providers in
                        providers.first()?.loadDataRepresentation(forTypeIdentifier: "com.example.item") { (data, error) in
                            if let data = data, let draggingItem = draggingItem {
                                do {
                                    let droppedItem = try JSONDecoder().decode(Item.self, from: data)
                                    if droppedItem.id != item.id {
                                        DispatchQueue.main.async {
                                            let from = items.firstIndex(where: { $0.id == draggingItem.id })!
                                            let to = items.firstIndex(where: { $0.id == item.id })!

                                            withAnimation {
                                                items.move(fromOffsets: IndexSet(integer: from), toOffset: to > from ? to + 1 : to)
                                            }
                                        }
                                    }
                                    self.draggingItem = nil
                                } catch {
                                    print("Error decoding item: (error)")
                                }
                            }
                        }
                        return true
                    }
            }
        }
        .padding()
    }
}

struct ReorderableDragAndDropView_Previews: PreviewProvider {
    static var previews: some View {
        ReorderableDragAndDropView()
    }
}

Explanation:

  • We introduce a draggingItem state variable to keep track of the item being dragged.
  • The .onDrag modifier sets the draggingItem when the drag starts.
  • The .onDrop modifier loads the dragged data and moves the item in the items array. We use withAnimation for a smooth visual effect.
  • Background color changes to gray when an item is being dragged over to give visual feedback.

Best Practices and Considerations

  • Performance: When dealing with large lists, ensure your drag and drop operations are performant. Avoid complex computations or UI updates on the main thread.
  • User Feedback: Provide visual feedback during the drag and drop operation to enhance the user experience (e.g., highlighting the drop target, changing the appearance of the dragged item).
  • Accessibility: Consider users with disabilities. Ensure drag and drop interactions are accessible via alternative input methods.
  • Error Handling: Properly handle potential errors during data loading or decoding in .onDrop.

Conclusion

Implementing drag and drop in SwiftUI is a powerful way to enhance the interactivity and usability of your applications. By understanding the basics of .onDrag and .onDrop modifiers and implementing appropriate data handling, you can create intuitive and engaging user experiences. Whether it’s simple text transfers or complex object reordering, SwiftUI provides the tools to implement a wide range of drag and drop interactions. Always consider user experience, performance, and accessibility when adding this functionality to your app.