How do I modify the background color and shape of drag&drop preview SwiftUI?

Viewed 515

I'm trying to implement simple list with draggable cells (iOS 15). For now I have two base solutions: List and ScrollView. None of this methods doesn't lead me to appropriate result.

First of all I'll provide some code:

List item view:

struct ListItemView: View {
    let index: Int

    var body: some View {
        HStack {
            Text("Item \(index)")
                .foregroundColor(.white)
            Spacer()
        }
        .padding()
        .background(Color.gray)
        .clipShape(RoundedRectangle(cornerRadius: 16.0))
    }
}

First solution (using List):

struct ContentView: View {
    var body: some View {
        List(0 ..< 10) { i in
            ListItemView(index: i)
                .onDrag {
                    NSItemProvider(object: String(describing: "item_\(i)") as NSString)
                }
                .padding(.bottom, 8.0)
                .listRowBackground(Color.clear)
                .listRowSeparator(.hidden)
                .listRowInsets(EdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8))
        }
        .background(Color.black)
        .listStyle(PlainListStyle())
    }
}

The result is fine, but when I start dragging action, I see some white rect around my cell, which is not what I want:

Drag&Drop preview using ScrollView

Second solution (using ScrollView)

struct ContentView: View {
    var body: some View {
        ScrollView {
            ForEach(0 ..< 10) { i in
                ListItemView(index: i)
                    .onDrag {
                        NSItemProvider(object: String(describing: "item_\(i)") as NSString)
                    }
            }
        }
        .background(Color.black)
    }
}

The result is a bit different: we don't see any coloured rects (I suppose, this is because we are not using List and there is different mechanism of drag&drop feature). Also there is scaled preview and shape without rounded corners.

Drag&Drop preview using List

So, the desirable behaviour is: a) the size of preview is the same as original size of list item b) there is rounded-corners shape without white frame

How can I achieve it?

1 Answers

Theoretically your NSItemProvider can offer a previewImageHandler which is a block that draws the image you want the system to use as the drag preview. Unfortunately that doesn't seem to work. The documentation of the onDrag modifier says that it uses a picture of the view as the preview image and it seemingly ignores the previewImageHandler. If someone finds out otherwise, leave a comment and I'll amend the answer. Here's what I tried:

import SwiftUI

struct ListItem : Identifiable {
    let id: Int
    let title: String
}

let itemsInList = (0..<10).map { ListItem(id: $0, title: "Item \($0)") }

@ViewBuilder func ListItemView(title : String) -> some View {
    ZStack(alignment: .leading) {
        RoundedRectangle(cornerRadius: 18, style: .circular)
            .foregroundColor(Color(red: 142.0/255.0, green: 142.0/255.0, blue: 142.0/255.0, opacity: 1.0))
        Text(title)
            .foregroundColor(.white)
            .padding()
    }
}

struct ContentView: View {
    var body: some View {
        List(itemsInList) {
            let itemTitle = $0.title
            ListItemView(title: itemTitle)
                .onDrag {
                    let itemProvider = NSItemProvider()
                    itemProvider.registerObject("Item \(itemTitle)" as NSString, visibility: .all)
                    itemProvider.previewImageHandler = {
                        (completionHandler, expectedClass, options) -> Void in

                        var resultImage : CGImage?

                        debugPrint(expectedClass ?? "Unknown")
                        if let cgContext = CGContext(data: nil,
                                                     width: 72,
                                                     height: 72,
                                                     bitsPerComponent: 8,
                                                     bytesPerRow: 16 * (((72 * 4) + 15) / 16),
                                                     space: CGColorSpace(name: CGColorSpace.sRGB)!,
                                                     bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue) {

                            let frame = CGRect(x: 0, y: 0, width: 72, height: 72)
                            cgContext.clear(frame)
                            cgContext.setFillColor(UIColor.red.cgColor)
                            cgContext.fillEllipse(in: frame)

                            resultImage = cgContext.makeImage()
                        }

                        if let completionHandler = completionHandler {
                            if let resultImage = resultImage {
                                completionHandler(UIImage(cgImage: resultImage), nil)
                            } else {
                                completionHandler(nil, NSError(domain: "ItemCompletion", code: -1, userInfo: nil))
                            }
                        }
                    }

                    return itemProvider
                }
        }
    }
}


struct previews : PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Related