visionOS - Drag and Drop to open specific WindowGroup

Hi team,

I'm running into the following issue, for which I don't seem to find a good solution.

I would like to be able to drag and drop items from a view into empty space to open a new window that displays detailed information about this item.

Now, I know something similar has been flagged already in this post (FB13545880: Support drag and drop to create a new window on visionOS)

HOWEVER, all this does, is launch the App again with the SAME WindowGroup and display ContentView in a different state (show a selected product e.g.).

What I would like to do, is instead launch ONLY the new WindowGroup, without a new instance of ContentView.

This is the closest I got so far. It opens the desired window, but in addition it also displays the ContentView WindowGroup

WindowGroup {
            ContentView()
                .onContinueUserActivity(Activity.openWindow, perform: handleOpenDetail)
        } 

WindowGroup(id: "Detail View", for: Reminder.ID.self) { $reminderId in
            ReminderDetailView(reminderId: reminderId! )
        }

.onDrag({
            let userActivity =  NSUserActivity(activityType: Activity.openWindow)
            
            let localizedString = NSLocalizedString("DroppedReminterTitle", comment: "Activity title with reminder name")
            userActivity.title = String(format: localizedString, reminder.title)
            
            userActivity.targetContentIdentifier = "\(reminder.id)"
            
            try? userActivity.setTypedPayload(reminder.id)
            // When setting the identifier
            let encoder = JSONEncoder()
            if let jsonData = try? encoder.encode(reminder.persistentModelID),
               let jsonString = String(data: jsonData, encoding: .utf8) {
                userActivity.userInfo = ["id": jsonString]
            }

            return NSItemProvider(object: userActivity)
        })
func handleOpenDetail(_ userActivity: NSUserActivity) {
        guard let idString = userActivity.userInfo?["id"] as? String else {
            print("Invalid or missing identifier in user activity")
            return
        }

        if let jsonData = idString.data(using: .utf8) {
            do {
                let decoder = JSONDecoder()
                let persistentID = try decoder.decode(PersistentIdentifier.self, from: jsonData)
                openWindow(id: "Detail View", value: persistentID)
            } catch {
                print("Failed to decode PersistentIdentifier: \(error)")
            }
        } else {
            print("Failed to convert string to data")
        }
    }

Accepted Reply

Hi @simonroetzer ,

This is very doable! I'll add some code below.

First, create your main ContentView WindowGroup as well as your DetailView WindowGroup, and mark the DetailView one with handlesExternalEvents

@main
struct DragAndDropNewWindowApp: App {
    @State private var text: String = "Drag me!"
    
    var body: some Scene {
        WindowGroup {
            ContentView(text: $text)
        }
        WindowGroup(id: "detailView") {
            DetailView(text: $text)
        }
        .handlesExternalEvents(matching: ["detailView"])
    }
}

As you were doing, create a userActivity and return an NSItemProvider with that object in your ContentView

 var body: some View {
        Rectangle()
            .fill(.green)
            .frame(width: 200, height: 200)
            .overlay {
                Text(text)
            }
            .onDrag {
                let userActivity = NSUserActivity(activityType: "drag")
                userActivity.title = text
                userActivity.targetContentIdentifier = "detailView"
                userActivity.userInfo = ["first": text as String]
                return NSItemProvider(object: userActivity)
            }
    }

Then, in your DetailView you can use .onContinueUserActivity to handle any other things you want do to, like this:

.onContinueUserActivity("drag") { activity in
        let _ = print(activity.title ?? "")
 }

One thing, test this on-device if you can. I've noticed that sometimes the simulator doesn't show the dragging (not always, but sometimes). Letting you know this in case you come across this behavior; this isn't your code, it's the simulator.

Replies

Hi @simonroetzer ,

This is very doable! I'll add some code below.

First, create your main ContentView WindowGroup as well as your DetailView WindowGroup, and mark the DetailView one with handlesExternalEvents

@main
struct DragAndDropNewWindowApp: App {
    @State private var text: String = "Drag me!"
    
    var body: some Scene {
        WindowGroup {
            ContentView(text: $text)
        }
        WindowGroup(id: "detailView") {
            DetailView(text: $text)
        }
        .handlesExternalEvents(matching: ["detailView"])
    }
}

As you were doing, create a userActivity and return an NSItemProvider with that object in your ContentView

 var body: some View {
        Rectangle()
            .fill(.green)
            .frame(width: 200, height: 200)
            .overlay {
                Text(text)
            }
            .onDrag {
                let userActivity = NSUserActivity(activityType: "drag")
                userActivity.title = text
                userActivity.targetContentIdentifier = "detailView"
                userActivity.userInfo = ["first": text as String]
                return NSItemProvider(object: userActivity)
            }
    }

Then, in your DetailView you can use .onContinueUserActivity to handle any other things you want do to, like this:

.onContinueUserActivity("drag") { activity in
        let _ = print(activity.title ?? "")
 }

One thing, test this on-device if you can. I've noticed that sometimes the simulator doesn't show the dragging (not always, but sometimes). Letting you know this in case you come across this behavior; this isn't your code, it's the simulator.

This works perfectly, thank you so much! I finally understand this workflow.