Reading time: 2 min

This recipe shows how to implement drag & drop functionality in SwiftUI.

The end result looks like this:

preview

This code works starting with SwiftUI 3 (iOS 15, macOS 12).

Drag & drop in SwiftUI works somewhat out of the box, as long as you're acquanited with UTTypes you wish to handle. The recipe goes like this:

  • The draggable view should have a onDrag modifier to provide a NSItemProvider instance. This wrapper represents the data you want to provide to the drop location.
    • You can optionally provide a custom preview, which will be shown during the drag.
  • The drop location should have a onDrop modifier. There, you need to provide:
    1. List of valid UTTypes for this drop location. Only drags that contain valid data will be accepted in the drop handler.
    2. Either a DropDelegate reference or a handler that is called when the drop happens and allows you to make use of the dropped NSItemProviders. Additionally, if you use the handler variant, you can specify a Bool binding that indicates if there's an active drop above the location or not. The handler variant is all you need most of the time.

Here's some code that shows all of that in action:

import SwiftUI
import UniformTypeIdentifiers

struct DragDropTest: View {
  @State private var textInput = ""
  @State private var items = ["Apple", "Orange", "Kiwi", "Pear"]
  @State private var dragInProgress = false

  var body: some View {
    VStack(alignment: .leading) {
      TextField("New fruit", text: $textInput)
        .onDrag {
          NSItemProvider(object: textInput as NSString)
        } preview: {
          Label("Add new fruit!", systemImage: "applelogo")
        }

      Text("Fruit")
        .padding(.top, 30)
      HStack {
        ForEach(items, id: \.self) { fruit in
          Text(fruit)
        }
      }
      .background(dragInProgress ? Color.orange : nil)
      .onDrop(of: [UTType.plainText], isTargeted: $dragInProgress) { providers in
        for item in providers {
          item.loadObject(ofClass: NSString.self) { item, error in
            if let str = item as? String {
              items.append(str)
            }
          }
        }
        return true
      }
    }
  }
}

For the sake of completeness, here's the same code, but using DropDelegate:

import SwiftUI
import UniformTypeIdentifiers

struct DragDropTest: View, DropDelegate {
  @State private var textInput = ""
  @State private var items = ["Apple", "Orange", "Kiwi", "Pear"]
  @State private var dragInProgress = false

  var body: some View {
    VStack(alignment: .leading) {
      TextField("New fruit", text: $textInput)
        .onDrag {
          NSItemProvider(object: textInput as NSString)
        } preview: {
          Label("Add new fruit!", systemImage: "applelogo")
        }

      Text("Fruit")
        .padding(.top, 30)
      HStack {
        ForEach(items, id: \.self) { fruit in
          Text(fruit)
        }
      }
      .background(dragInProgress ? Color.orange : nil)
      .onDrop(of: [UTType.plainText], delegate: self)
      Spacer()
    }
    .padding()
  }

  func dropEntered(info: DropInfo) {
    dragInProgress = info.hasItemsConforming(to: [UTType.plainText])
  }

  func dropExited(info: DropInfo) {
    dragInProgress = false
  }

  func performDrop(info: DropInfo) -> Bool {
    for item in info.itemProviders(for: [UTType.plainText]) {
      item.loadObject(ofClass: NSString.self) { item, error in
        if let str = item as? String {
          items.append(str)
        }
      }
    }
    dragInProgress = false
    return true
  }
}

The drag functionality doesn't work in XCode previews - you need to run the code on either a simulator or a real device.

Next Post Previous Post