Delightful SwiftUI image drag & drop for a macOS app

Sponsored
RevenueCat logo
Develop with RocketSim, Ship with Helm.

Helm Pro yearly subscribers now get a 30% discount on RocketSim thanks to contingent pricing on the App Store.

I have recently finished a new feature for my app QReate that allows users to add images to their custom QR codes.

I wanted to make the experience as native and intuitive as possible, so I decided to add support for dragging and dropping images directly onto the editor and, much to my surprise coming from a UIKit background, it was a breeze to implement in SwiftUI:

The view

The view I added support for dragging and dropping images to is simple and acts as a canvas for the QR code the user is creating:

QRCodeCanvas.swift
import SwiftUI

struct QRCodeCanvas: View {
    @Binding var qrCode: QRCodeModel

    var body: some View {
        QRCodeView(qrCode: $qrCode)
    }
}

Adding support for dragging and dropping

Adding support for dragging and dropping images onto a SwiftUI view is rather simple. You just need to add a view modifier, specify the file types (UTTypes) you’d like to support (in this case just .image) and provide a closure that will be called when the user drops an image onto the view.

We will also create a @State property to keep track of whether the user is currently dragging an image over the view or not so that we can update our UI accordingly:

QRCodeCanvas.swift
import SwiftUI
import QRCode

struct QRCodeCanvas: View {
    @Binding var qrCode: QRCodeModel
    @State private var isTargeted: Bool = false

    var body: some View {
        QRCodeView(qrCode: $qrCode)
            .onDrop(of: [.image], isTargeted: $isTargeted, perform: { providers in
                return true
            })
    }
}

While we are already supporting dragging and dropping images onto the view, we are not making it clear to the user that such a feature is available:

Updating the UI

Let’s now provide an overlay that is only shown when the isTargeted property is set to true.

To make things look as smooth as possible, we will also add a view modifier to animate any changes to the view affected by the isTargeted property:

QRCodeCanvas.swift
import SwiftUI

struct QRCodeCanvas: View {
    @Binding var qrCode: QRCodeModel
    @State private var isTargeted: Bool = false

    var body: some View {
        QRCodeView(qrCode: $qrCode)
            .onDrop(of: [.image], isTargeted: $isTargeted, perform: { providers in
                return true
            })
            .overlay {
                if isTargeted {
                    ZStack {
                        Color.black.opacity(0.7)

                        VStack(spacing: 8) {
                            Image(systemName: "plus.circle.fill")
                                .font(.system(size: 60))
                            Text("Drop your image here...")
                        }
                        .font(.largeTitle)
                        .fontWeight(.heavy)
                        .foregroundColor(.white)
                        .frame(maxWidth: 250)
                        .multilineTextAlignment(.center)
                    }
                }
            }
            .animation(.default, value: isTargeted)
    }
}

When we next run the app and drag an image over the view, we should see the following:

Hadling a drop

Last but not least, we need to do something whenever the user drops an image onto the view.

In the closure we provide to the onDrop view modifier, we need to make sure we can get the first provider from the closure’s parameter and then load the data representation for the .image type. We can then initialise an NSImage instance with that data and assign it to the QR code model so it displays on top of the QR code.

QRCodeCanvas.swift
import SwiftUI
import QRCode

struct QRCodeCanvas: View {
    @Binding var qrCode: QRCodeModel
    @State private var isTargeted: Bool = false

    var body: some View {
        QRCodeView(qrCode: $qrCode)
            .onDrop(of: [.image], isTargeted: $isTargeted, perform: { providers in
                guard let provider = providers.first else { return false }

                _ = provider.loadDataRepresentation(for: .image) { data, error in
                    if error == nil, let data {
                        DispatchQueue.main.async {
                            qrCode.image.image = NSImage(data: data)
                        }
                    }
                }

                return true
            })
            .overlay {
                if isTargeted {
                    ZStack {
                        Color.black.opacity(0.7)

                        VStack(spacing: 8) {
                            Image(systemName: "plus.circle.fill")
                                .font(.system(size: 60))
                            Text("Drop your image here...")
                        }
                        .font(.largeTitle)
                        .fontWeight(.heavy)
                        .foregroundColor(.white)
                        .frame(maxWidth: 250)
                        .multilineTextAlignment(.center)
                    }
                }
            }
            .animation(.default, value: isTargeted)
    }
}

And with that, we are done! We can now drag and drop images onto our view and they will be shown on top of the QR code. 🎉