How to support drag and drop in SwiftUI
How to support drag and drop in SwiftUI êŽë š
Updated for Xcode 15
Improved in iOS 16
SwiftUI's Transferable
protocol allows us to add drag and drop support to our apps without a great deal of code, using the dropDestination()
and draggable()
modifiers.
For example, this will accept text dragged into our app, and render it onto a Canvas
:
struct ContentView: View {
@State private var message = ""
var body: some View {
Canvas { context, size in
let formattedText = Text(message).font(.largeTitle).foregroundStyle(.red)
context.draw(formattedText, in: CGRect(origin: .zero, size: size))
}
.dropDestination(for: String.self) { items, location in
message = items.first ?? ""
return true
}
}
}
The key part there is the dropDestination()
modifier, which tells SwiftUI four things:
- We accept only strings.
- We expect to receive an array of items that were dropped on the app. This will automatically be an array of
String
. - We want to be told where they were dropped. This will be a
CGPoint
in the canvas's coordinate space. - We consider the drop operation to be successful, so we return
true
.
Handling images is a little trickier because we'll be given a Data
instance representing the contents of the image. We need to convert that to a UIImage
, and put the result into an image to render.
Fortunately, if we support Data
then both will work, so code like this lets the user drag an image from Photos right into our app:
struct ContentView: View {
@State private var image = Image(systemName: "photo")
var body: some View {
image
.resizable()
.scaledToFit()
.frame(width: 300, height: 300)
.dropDestination(for: Data.self) { items, location in
guard let item = items.first else { return false }
guard let uiImage = UIImage(data: item) else { return false }
image = Image(uiImage: uiImage)
return true
}
}
}
Accepting arrays of data â for example letting the user drag multiple images into our â follows the same procedure: using dropDestination(for: Data.self)
, but now rather than just reading the first item you should use them all.
For example, this shows several pictures in a ScrollView
by converting each Data
instance into a UIImage
, and then into a SwiftUI Image
:
struct ContentView: View {
@State private var images = [Image]()
var body: some View {
ScrollView {
VStack {
ForEach(0..<images.count, id: \.self) { i in
images[i]
.resizable()
.scaledToFit()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.dropDestination(for: Data.self) { items, location in
images = items.compactMap {
UIImage(data: $0).map(Image.init)
}
return true
}
}
}
When you want to add dragging to your views, add the draggable()
modifier using whatever Transferable
content you want. By default SwiftUI will use the view itself for the drag preview, and if you're dragging an image from within your app you'll find you can use the drop type of Image.self
rather than having to convert Data
to UIImage
first.
For example, this shows three different SF Symbols and lets the user drag any of them into the box below:
struct ContentView: View {
let sports = ["figure.badminton", "figure.cricket", "figure.fencing"]
@State private var dropImage = Image(systemName: "photo")
var body: some View {
VStack {
HStack {
ForEach(sports, id: \.self) { sport in
Image(systemName: sport)
.frame(minWidth: 50, minHeight: 50)
.background(.red)
.foregroundStyle(.white)
.draggable(Image(systemName: sport))
}
}
.frame(minWidth: 300, minHeight: 70)
.background(.gray)
dropImage
.frame(width: 150, height: 150)
.background(.green)
.foregroundStyle(.white)
.dropDestination(for: Image.self) { items, location in
dropImage = items.first ?? Image(systemName: "photo")
return true
}
}
}
}
Important
When you're dragging an SF Symbol image, SwiftUI will send the image pixel data and not the neatly scalable vector we're used to. This means dropped Image
data won't respond to things like font()
or foregroundStyle()
like you might expect.
If you want to show a custom drag preview, add a trailing closure with some SwiftUI views. For example, this makes a draggable golf image and adds the text âFigure playing golfâ next to it:
Image(systemName: "figure.golf")
.frame(minWidth: 50, minHeight: 50)
.background(.red)
.foregroundStyle(.white)
.draggable(Image(systemName: "figure.golf")) {
Label("Figure playing golf", systemImage: "figure.golf")
}