NEW: My new book Pro SwiftUI is out now – level up your SwiftUI skills today! >>

How to support drag and drop in SwiftUI

Paul Hudson    @twostraws   

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:

  1. We accept only strings.
  2. We expect to receive an array of items that were dropped on the app. This will automatically be an array of String.
  3. We want to be told where they were dropped. This will be a CGPoint in the canvas’s coordinate space.
  4. 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
            }

    }
}

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

Hacking with Swift is sponsored by Play

SPONSORED Play is the first native iOS design tool created for designers and engineers. You can install Play for iOS and iPad today and sign up to check out the Beta of our macOS app with SwiftUI code export. We're also hiring engineers!

Click to learn more about Play!

Sponsor Hacking with Swift and reach the world's largest Swift community!

Similar solutions…

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.