SwiftUI onDrag。如何提供多个NSItemProviders?

12

在 SwiftUI 在 MacOs 上实现 onDrop(of supportedTypes: [String], isTargeted: Binding<Bool>?, perform action: @escaping ([NSItemProvider]) -> Bool) -> some View 时,我们会收到一个 NSItemProvider 数组,这使得我们可以在视图内拖放多个项目。

在实现 onDrag(_ data: @escaping () -> NSItemProvider) -> some View 时,如何提供多个要拖动的项?

我没有找到任何在线示例说明如何进行多个项目拖动,我想知道是否有其他方法可以实现允许我提供多个 NSItemProvider 的拖动操作,或者使用上述方法进行操作的方式。

我的目标是能够选择多个项目并将它们拖动,就像在 Finder 中一样。为了做到这一点,我想以 [NItemProvider] 形式提供 [URL],但是目前我每次只能提供一个 URL 进行拖动操作。


我正在处理同样的挑战,在SwiftUI中找不到任何相关信息。 - Gal Yedidovich
3
很遗憾,.onDrag不适用于拖动多个项目。就像SwiftUI中的许多拖放功能一样,这种功能仍未实现。 - lupinglade
你找到了一种拖动多个项目/文件的方法吗? - JCB
@user1046037 你尝试过创建一个单一的JSON字符串吗?如果没有提供最小可复现示例,我们无法帮助您进行故障排除。我们将只能猜测您要重现什么,从而创建所有内容。 - lorem ipsum
1
onDrag 在 iPad 上支持拖动 List 的多个项目,但在 iPhoneOS(v14.x)或 macOS(v11.x 以上)上似乎不支持。如果在 iOS 15 和 macOS 12 上没有修复的话,可能需要将其报告为错误。 - Dad
显示剩余2条评论
2个回答

2
实际上,在SwiftUI中处理多个项目的拖放时,您不需要[NSItemProvider]。由于您必须在自己的选择管理器中跟踪多个选定项目,因此在生成自定义拖动预览和处理拖放时,请使用该选择。将一个新的MacOS App项目的ContentView替换为下面的所有代码。这是一个完整的工作示例,演示如何使用SwiftUI拖放多个项目。要使用它,您必须选择一个或多个项目以启动拖动,然后可以将它们拖到任何其他未选定的项目上。在控制台上打印出了拖放操作的结果。我相当快地把这些东西组合起来了,所以我的示例可能存在一些低效之处,但它似乎工作得很好。
import SwiftUI
import Combine

struct ContentView: View {
    
    private let items = ["Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7"]
    
    @StateObject var selection = StringSelectionManager()
    @State private var refreshID = UUID()
    @State private var dropTargetIndex: Int? = nil
    
    var body: some View {
        VStack(alignment: .leading) {
            ForEach(0 ..< items.count, id: \.self) { index in
                HStack {
                    Image(systemName: "folder")
                    Text(items[index])
                }
                .opacity((dropTargetIndex != nil) && (dropTargetIndex == index) ? 0.5 : 1.0)
                // This id must change whenever the selection changes, otherwise SwiftUI will use a cached preview
                .id(refreshID)
                .onDrag { itemProvider(index: index) } preview: {
                    DraggingPreview(selection: selection)
                }
                .onDrop(of: [.text], delegate: MyDropDelegate(items: items,
                                                              selection: selection,
                                                              dropTargetIndex: $dropTargetIndex,
                                                              index: index) )
                .padding(2)
                .onTapGesture { selection.toggle(items[index]) }
                .background(selection.isSelected(items[index]) ?
                            Color(NSColor.selectedContentBackgroundColor) : Color(NSColor.windowBackgroundColor))
                .cornerRadius(5.0)
            }
        }
        .onReceive(selection.objectWillChange, perform: { refreshID = UUID() } )
        .frame(width: 300, height: 300)
    }
    
    private func itemProvider(index: Int) -> NSItemProvider {
        // Only allow Items that are part of a selection to be dragged
        if selection.isSelected(items[index]) {
            return NSItemProvider(object: items[index] as NSString)
        } else {
            return NSItemProvider()
        }
    }
    
}

struct DraggingPreview: View {
    
    var selection: StringSelectionManager
    
    var body: some View {
        VStack(alignment: .leading, spacing: 1.0) {
            ForEach(selection.items, id: \.self) { item in
                HStack {
                    Image(systemName: "folder")
                    Text(item)
                        .padding(2.0)
                        .background(Color(NSColor.selectedContentBackgroundColor))
                        .cornerRadius(5.0)
                    Spacer()
                }
            }
        }
        .frame(width: 300, height: 300)
    }
    
}

struct MyDropDelegate: DropDelegate {
    
    var items: [String]
    var selection: StringSelectionManager
    @Binding var dropTargetIndex: Int?
    var index: Int
    
    func dropEntered(info: DropInfo) {
        dropTargetIndex = index
    }
    
    func dropExited(info: DropInfo) {
        dropTargetIndex = nil
    }
    
    func validateDrop(info: DropInfo) -> Bool {
        // Only allow non-selected Items to be drop targets
        if !selection.isSelected(items[index]) {
            return info.hasItemsConforming(to: [.text])
        } else {
            return false
        }
    }
    
    func dropUpdated(info: DropInfo) -> DropProposal? {
        // Sets the proper DropOperation
        if !selection.isSelected(items[index]) {
            let dragOperation = NSEvent.modifierFlags.contains(NSEvent.ModifierFlags.option) ? DropOperation.copy : DropOperation.move
            return DropProposal(operation: dragOperation)
        } else {
            return DropProposal(operation: .forbidden)
        }
    }
    
    func performDrop(info: DropInfo) -> Bool {
        // Only allows non-selected Items to be drop targets & gets the "operation"
        let dropProposal = dropUpdated(info: info)
        if dropProposal?.operation != .forbidden {
            let dropOperation = dropProposal!.operation == .move ? "Move" : "Copy"
            
            if selection.selection.count > 1 {
                for item in selection.selection {
                    print("\(dropOperation): \(item) Onto: \(items[index])")
                }
            } else {
                // https://dev59.com/-cLra4cB1Zd3GeqPEjnx#69325742
                if let item = info.itemProviders(for: ["public.utf8-plain-text"]).first {
                    item.loadItem(forTypeIdentifier: "public.utf8-plain-text", options: nil) { (data, error) in
                        if let data = data as? Data {
                            let item = NSString(data: data, encoding: 4)
                            print("\(dropOperation): \(item ?? "") Onto: \(items[index])")
                        }
                    }
                }
                return true
            }
        }
        return false
    }
    
}

class StringSelectionManager: ObservableObject {
    
    @Published var selection: Set<String> = Set<String>()
    
    let objectWillChange = PassthroughSubject<Void, Never>()

    // Helper for ForEach
    var items: [String] {
        return Array(selection)
    }
    
    func isSelected(_ value: String) -> Bool {
        return selection.contains(value)
    }
    
    func toggle(_ value: String) {
        if isSelected(value) {
            deselect(value)
        } else {
            select(value)
        }
    }
    
    func select(_ value: String?) {
        if let value = value {
            objectWillChange.send()
            selection.insert(value)
        }
    }
    
    func deselect(_ value: String) {
        objectWillChange.send()
        selection.remove(value)
    }
    
}

1
如果我们想将项目拖到应用程序外部,例如拖到查找器窗口中怎么办? 例如对于单个具有“url”属性的项目,我们可以使用.onDrag { NSItemProvider(contentsOf: item.url) }。如何处理多个项目? - bzyr
据我理解,这只适用于应用程序内的拖放操作。 - Daniel
没错。我们仍然希望苹果在不久的将来为SwiftUI提供更完整的D&D解决方案。 - Chuck H

1

可能值得检查一下在 macOS 12 中添加的 View 的 exportsItemProviders 函数是否符合我们的需求。如果你使用支持多选的 List 版本(List(selection: $selection),其中 @State var selection: Set<UUID> = [](或其他)),那就更好了。

不幸的是,我的 Mac 还在 macOS 11.x 上,所以我无法测试这个功能 :-/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接