SwiftUI onDrag。如何提供多个 NSItemProviders?

Posted

技术标签:

【中文标题】SwiftUI onDrag。如何提供多个 NSItemProviders?【英文标题】:SwiftUI onDrag. How to provide multiple NSItemProviders? 【发布时间】:2020-11-06 10:32:52 【问题描述】:

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

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

我无法在网上找到多个项目拖动的任何示例,我想知道是否有另一种方法可以实现拖动操作,允许我提供多个 NSItemProvider 或使用上述方法来实现方法

我的目标是能够选择多个项目并在 Finder 中准确地拖动它们。为此,我想提供一个 [URL] 作为 [NItemProvider],但目前我只能为每个拖动操作提供一个 URL。

【问题讨论】:

我正在处理同样的挑战,我在 SwiftUI 中找不到任何相关信息。 .onDrag 并不意味着用于拖动多个项目,不幸的是。这个功能,和其他东西中的大部分拖放一样,仍然没有在 SwiftUI 中实现。 你找到拖动多个项目/文件的方法了吗? @user1046037 您是否尝试过制作单个 JSON 字符串?如果没有Minimal Reproducible Example,就无法帮助您进行故障排除。我们将创建所有内容以尝试猜测您要复制的内容。 您使用的是列表,而您要拖放的是列表项? 【参考方案1】:

可能值得检查一下在 macOS 12 中添加的 View 的 exportsItemProviders 函数是否能满足我们的需要。如果您使用支持多选的List 版本(List(selection: $selection) where @State var selection: Set<UUID> = [](或其他)。

不幸的是,我的 Mac 仍在 macOS 11.x 上,所以我无法对此进行测试:-/

【讨论】:

【参考方案2】:

实际上,您不需要[NSItemProvider] 来处理 SwiftUI 中的多个项目的拖放操作。由于无论如何您都必须在自己的选择管理器中跟踪多个选定的项目,因此在生成自定义拖动预览和处理拖放时使用该选择。

将新 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://***.com/a/69325742/899918
                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)
    
    

【讨论】:

如果我们想将项目拖到应用程序之外怎么办,例如到取景器窗口?例如。对于单个项目(具有url 属性),我们可以使用.onDrag NSItemProvider(contentsOf: item.url) 。它如何与多个项目一起使用?

以上是关于SwiftUI onDrag。如何提供多个 NSItemProviders?的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI | onDrag - 自定义 (dragItem) 预览图像外观

SwiftUI onDrag 在设备上运行时不显示预览图像

SwiftUI Drag 事件如何限制仅检测水平/垂直滚动

SwiftUI 从一个列表拖到另一个列表

在 SwiftUI NavigationView 中添加多个标题

GDI 图形对象上的鼠标 OnDrag 事件