Updated for Xcode 14.2
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).foregroundColor(.red)
context.draw(formattedText, in: CGRect(origin: .zero, size: size))
}
.dropDestination(for: String.self) { items, location in
message = items.first ?? ""
return true
}
}
}
Download this as an Xcode project
The key part there is the dropDestination()
modifier, which tells SwiftUI four things:
String
.CGPoint
in the canvas’s coordinate space.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
}
}
}
Download this as an Xcode project
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
}
}
}
Download this as an Xcode project
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)
.foregroundColor(.white)
.draggable(Image(systemName: sport))
}
}
.frame(minWidth: 300, minHeight: 70)
.background(.gray)
dropImage
.frame(width: 150, height: 150)
.background(.green)
.foregroundColor(.white)
.dropDestination(for: Image.self) { items, location in
dropImage = items.first ?? Image(systemName: "photo")
return true
}
}
}
}
Download this as an Xcode project
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)
.foregroundColor(.white)
.draggable(Image(systemName: "figure.golf")) {
Label("Figure playing golf", systemImage: "figure.golf")
}
Download this as an Xcode project
SAVE 50% To celebrate WWDC23, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Link copied to your pasteboard.