可拖拽的修饰符会阻止选中项

编程语言 2026-07-11

我在研究一个带有文件夹和条目的树状数据结构。

我想在文件夹之间拖动条目和文件夹,使用 .draggabledropDestination

在我当前的代码中,拖拽可以工作,但对条目的选中不再起作用。如果我把 .draggable 注释掉,选中就会按预期工作。

如何确保同时实现选中和拖放?

我也尝试使用 TransferableCodable,但结果一样。

编辑:我发现如果对 FolderRow(即子文件夹)禁用 .draggable,我就可以拖拽并选中所有条目。看起来,一旦在视图层级中存在一个 .draggable 修饰符位于 NavigationLink 之上,链接就不再工作。


下面是我的模型:

@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)
                }
            }
            .listStyle(.sidebar)
        } detail: {
            Text("detail")
        }
        .onAppear {
            seed()
        }
    }

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

            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])

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

struct FolderRow: View {
    // recursive
    @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)
            }

            // Entries (leaf nodes)
            ForEach(folder.entries) { entry in
                NavigationLink {
                    VStack {
                        Text(entry.name)
                        Text(entry.detail)
                    }
                } label: {
                    EntryRow(entry: entry)
                }
                .draggable(entry.name)
            }

        } label: {
            Label(folder.name, systemImage: "folder")
        }
        .dropDestination(for: String.self) { names, _ in
            return handleDrop(of: names)
        }
    }

    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.parentFolder = folder
                        print("dropped \(droppedFolder.name) on \(folder.name)")
                    }
                }
            }
            return true
        }
        catch {
            debugPrint(error.localizedDescription)
            return false
        }
    }
}

struct EntryRow: View {
    var entry: Entry

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

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

解决方案

我找到了一个解决方案,我把用于 FolderRow.draggable 修饰符移到了 DisclosureGroupLabel

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

            // 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")
                .draggable(folder.name) // <<<==== MOVE HERE
        }
        .dropDestination(for: String.self) { names, _ in
            return handleDrop(of: names)
        }
    }
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章