NavigationLink selection in DisclosureGroup not working with .draggable modifier

NavigationLink selection in DisclosureGroup not working with .draggable modifier

This was recently also posted here: https://stackoverflow.com/questions/79914290/

I am playing around with a tree data structure with folders and entries.I would like to add dragging of entries and folders between folders, using .draggable and dropDestination. In my current code, dragging works, but selection of entries no longer works, except if I click outside of the Text If I comment out .draggable(subfolder.name) in func FolderRow(), selection works as expected.

How can I make sure both selection and drag and drop works for both folders and entries?

I also tried using Transferable and Codable, but I get the same result.

Here is an MRE:

import SwiftData
import SwiftUI

@Model
final class Folder {
    @Attribute(.unique) var name: String

    // Parent
    var parentFolder: Folder?

    // Child folders
    @Relationship(deleteRule: .cascade, inverse: \Folder.parentFolder)
    var subfolders: [Folder] = []

    // Leaf entries
    @Relationship(deleteRule: .cascade, inverse: \Entry.folder)
    var entries: [Entry] = []

    init(name: String, parentFolder: Folder? = nil) {
        self.name = name
        self.parentFolder = parentFolder
    }
}

@Model
final class Entry {
    @Attribute(.unique) var name: String
    var detail: String

    var folder: Folder? // recursive relationship

    init(name: String, detail: String) {
        self.name = name
        self.detail = detail
    }
}

@main
struct TestMacApp: App {
    var body: some Scene {
        WindowGroup {
            SidebarView()
                .modelContainer(for: Folder.self)
        }
    }
}

struct SidebarView: View {
    @Environment(\.modelContext) private var context

    @Query(filter: #Predicate<Folder> { $0.parentFolder == nil })
    private var rootFolders: [Folder]

    var body: some View {
        NavigationSplitView {
            List {
                ForEach(rootFolders) { folder in
                    FolderRow(folder: folder)
                        .draggable(folder.name)
                }
            }
        } detail: {
            Text("detail")
        }
        .onAppear {
            seed()
        }
    }
}

struct FolderRow: View {
    @Environment(\.modelContext) private var context

    var folder: Folder

    @State private var isExpanded: Bool = true

    var body: some View {
        DisclosureGroup(isExpanded: $isExpanded) {
            // Subfolders
            ForEach(folder.subfolders) { subfolder in
                FolderRow(folder: subfolder)
                    .draggable(subfolder.name) // disabling this line fixes the selection
            }

            // Entries (leaf nodes)
            ForEach(folder.entries) { entry in
                NavigationLink(destination: EntryDetail(entry: entry)) {
                    EntryRow(entry: entry)
                }
                .draggable(entry.name)
            }
        } label: {
            Label(folder.name, systemImage: "folder")
        }
        .dropDestination(for: String.self) { names, _ in
            return handleDrop(of: names)
        }
    }
}

struct EntryRow: View {
    var entry: Entry

    var body: some View {
        Text(entry.name)
    }
}

struct EntryDetail: View {
    var entry: Entry

    var body: some View {
        Text(entry.detail)
    }
}

extension FolderRow {
    private func handleDrop(of names: [String]) -> Bool {
        do {
            for name in names {
                if let droppedEntry = try context.fetchFilteredModel(filter: #Predicate<Entry> { x in x.name == name }) {
                    droppedEntry.folder = folder
                    print("dropped \(droppedEntry.name) on \(folder.name)")
                }

                else if let droppedFolder = try context.fetchFilteredModel(filter: #Predicate<Folder> { x in x.name == name }) {
                    if droppedFolder.parentFolder != nil && droppedFolder != folder {
                        droppedFolder.parentFolder = folder
                        print("dropped \(droppedFolder.name) on \(folder.name)")
                    }
                }
            }
            return true
        }
        catch {
            debugPrint(error.localizedDescription)
            return false
        }
    }
}

extension SidebarView {
    private func seed() {
        do {
            // delete current models
            for folder: Folder in try context.fetchAllModels() {
                context.delete(folder)
            }
            try context.save()

            let rootFolder = Folder(name: "Root")

            let entry1 = Entry(name: "One", detail: "Detail One")
            let entry2 = Entry(name: "Two", detail: "Detail Two")

            rootFolder.entries.append(contentsOf: [entry1, entry2])

            let subFolder1 = Folder(name: "Sub1", parentFolder: rootFolder)

            let entry3 = Entry(name: "Three", detail: "Detail Three")
            let entry4 = Entry(name: "Four", detail: "Detail Four")

            subFolder1.entries.append(contentsOf: [entry3, entry4])

            let subFolder2 = Folder(name: "Sub2", parentFolder: rootFolder)

            let entry5 = Entry(name: "Five", detail: "Detail Five")
            let entry6 = Entry(name: "Six", detail: "Detail Six")

            subFolder2.entries.append(contentsOf: [entry5, entry6])

            context.insert(rootFolder)
        }
        catch {
            debugPrint(error)
        }
    }
}

extension ModelContext { // convenience methods
    func fetchAllModels<M>() throws -> [M] where M: PersistentModel {
        let fetchDescriptor = FetchDescriptor<M>()

        return try fetch(fetchDescriptor)
    }

    func fetchFilteredModels<M>(filter: Predicate<M>) throws -> [M] where M: PersistentModel {
        let fetchDescriptor = FetchDescriptor<M>(predicate: filter)

        return try fetch(fetchDescriptor)
    }

    func fetchFilteredModel<M>(filter: Predicate<M>) throws -> M? where M: PersistentModel {
        return try fetchFilteredModels(filter: filter).first
    }
}
NavigationLink selection in DisclosureGroup not working with .draggable modifier
 
 
Q