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
}
}