如何在 SwiftUI (macOS) 中拖放未捆绑的图像

Posted

技术标签:

【中文标题】如何在 SwiftUI (macOS) 中拖放未捆绑的图像【英文标题】:How to drag and drop images not in bundle in SwiftUI (macOS) 【发布时间】:2020-05-09 23:38:14 【问题描述】:

我正在尝试在 macOS 上的 SwiftUI 中实现拖放,我可以在其中以编程方式生成图像并将它们作为可拖动项显示在视图中,或者出于相同目的从 Assets.xcassets 加载图像。我的代码基于this。我试图参考this question,但我无法让它工作。

我的图像显示正常,但由于我引用的是图像本身,因此我无法为拖放 API 返回 URL(见下文):

//Value of type 'ContentView.DragableImage' has no member 'url' so I cannot use this
.onDrag  return NSItemProvider(object: self.url as NSURL) 

这里是代码。我已经通过代码中的 cmets 指出了一些事情:

import SwiftUI

let img1url = Bundle.main.url(forResource: "grapes", withExtension: "png") // < -- CAN pass this in because it is by url
let img2url = Bundle.main.url(forResource: "banana", withExtension: "png")
let img3url = Bundle.main.url(forResource: "peach", withExtension: "png")
let img4url = Bundle.main.url(forResource: "kiwi", withExtension: "png")

struct ContentView: View 

    var body: some View 
        HStack 
            VStack 
                //DragableImage(url: img1url!)
                //DragableImage(url: img3url!)

                DragableImage()
                DragableImage()
            

            VStack 
                  //DragableImage(url: img2url!)
                 // DragableImage(url: img4url!)

               DragableImage()
               DragableImage()
            

            DroppableArea()
        .padding(40)
    

    struct DragableImage: View 
        //let url: URL

        var body: some View 
            Image("grapes") //<--- Takes in image without url fine
           //Image(nsImage: NSImage(byReferencing: url)) //<--- Taking in image by URL (I don't want that)
                .resizable()
                .frame(width: 150, height: 150)
                .clipShape(Circle())
                .overlay(Circle().stroke(Color.white, lineWidth: 2))
                .padding(2)
                .overlay(Circle().strokeBorder(Color.black.opacity(0.1)))
                .shadow(radius: 3)
                .padding(4)
                .onDrag  return NSItemProvider(object: self.url as NSURL)  //<--- MAIN ISSUE: "Value of type 'ContentView.DragableImage' has no member 'url'" (there is now no URL to reference the image by in the return)

        
    

    struct DroppableArea: View 
        @State private var imageUrls: [Int: URL] = [:]
        @State private var active = 0

        var body: some View 
            let dropDelegate = MyDropDelegate(imageUrls: $imageUrls, active: $active)

            return VStack 
                HStack 
                    GridCell(active: self.active == 1, url: imageUrls[1])

                    GridCell(active: self.active == 3, url: imageUrls[3])
                

                HStack 
                    GridCell(active: self.active == 2, url: imageUrls[2])

                    GridCell(active: self.active == 4, url: imageUrls[4])
                

            
            .background(Rectangle().fill(Color.gray))
            .frame(width: 300, height: 300)
            .onDrop(of: ["public.file-url"], delegate: dropDelegate)

        
    

    struct GridCell: View 
        let active: Bool
        let url: URL?

        var body: some View 
            let img = Image(nsImage: url != nil ? NSImage(byReferencing: url!) : NSImage())
                .resizable()
                .frame(width: 150, height: 150)

            return Rectangle()
                .fill(self.active ? Color.green : Color.clear)
                .frame(width: 150, height: 150)
                .overlay(img)
        
    

    struct MyDropDelegate: DropDelegate 
        @Binding var imageUrls: [Int: URL]
        @Binding var active: Int

        func validateDrop(info: DropInfo) -> Bool 
            return info.hasItemsConforming(to: ["public.file-url"])
        

        func dropEntered(info: DropInfo) 
            NSSound(named: "Morse")?.play()
        

        func performDrop(info: DropInfo) -> Bool 
            NSSound(named: "Submarine")?.play()

            let gridPosition = getGridPosition(location: info.location)
            self.active = gridPosition

            if let item = info.itemProviders(for: ["public.file-url"]).first 
                item.loadItem(forTypeIdentifier: "public.file-url", options: nil)  (urlData, error) in
                    DispatchQueue.main.async 
                        if let urlData = urlData as? Data 
                            self.imageUrls[gridPosition] = NSURL(absoluteURLWithDataRepresentation: urlData, relativeTo: nil) as URL
                        
                    
                

                return true

             else 
                return false
            

        

        func dropUpdated(info: DropInfo) -> DropProposal? 
            self.active = getGridPosition(location: info.location)

            return nil
        

        func dropExited(info: DropInfo) 
            self.active = 0
        

        func getGridPosition(location: CGPoint) -> Int 
            if location.x > 150 && location.y > 150 
                return 4
             else if location.x > 150 && location.y < 150 
                return 3
             else if location.x < 150 && location.y > 150 
                return 2
             else if location.x < 150 && location.y < 150 
                return 1
             else 
                return 0
            
        
    



我还尝试了以下方法并取得了更大的成功。它在图像拖动时突出显示,但在放置时不会显示:

import SwiftUI

let image1 = NSImage(named: "green")!
let image2 = NSImage(named: "blue")!

struct ContentView: View 

    var body: some View 
        HStack 
            VStack 
                DragableImage(image: image1)
                DragableImage(image: image2)
            

            VStack 
               DragableImage(image: image1)
               DragableImage(image: image2)
            

            DroppableArea()
        .padding(40)
    

    struct DragableImage: View 
        @State var image: NSImage
        @State private var dragOver = false

        var body: some View 
            Image(nsImage: image)
                .onDrop(of: ["public.file-url"], isTargeted: $dragOver)  providers -> Bool in
                    providers.first?.loadDataRepresentation(forTypeIdentifier: "public.file-url", completionHandler:  (data, error) in
                        if let data = data, let path = NSString(data: data, encoding: 4), let url = URL(string: path as String) 
                            let imageLocal = NSImage(contentsOf: url)
                            DispatchQueue.main.async 
                                self.image = imageLocal!
                            
                        
                    )
                    return true
                
                .onDrag 
                    let data = self.image.tiffRepresentation
                    let provider = NSItemProvider(item: data as NSSecureCoding?, typeIdentifier: kUTTypeTIFF as String)
                    provider.previewImageHandler =  (handler, _, _) -> Void in
                        handler?(data as NSSecureCoding?, nil)
                    
                    return provider
                
                .border(dragOver ? Color.red : Color.clear)
        
    

    struct DroppableArea: View 
        @State private var imageUrls: [Int: URL] = [:]
        @State private var active = 0

        var body: some View 
            let dropDelegate = MyDropDelegate(imageUrls: $imageUrls, active: $active)

            return VStack 
                HStack 
                    GridCell(active: self.active == 1, url: imageUrls[1])

                    GridCell(active: self.active == 3, url: imageUrls[3])
                

                HStack 
                    GridCell(active: self.active == 2, url: imageUrls[2])

                    GridCell(active: self.active == 4, url: imageUrls[4])
                

            
            .background(Rectangle().fill(Color.gray))
            .frame(width: 300, height: 300)
            .onDrop(of: ["public.file-url"], delegate: dropDelegate)

        
    

    struct GridCell: View 
        let active: Bool
        let url: URL?

        var body: some View 
            let img = Image(nsImage: url != nil ? NSImage(byReferencing: url!) : NSImage())
                .resizable()
                .frame(width: 150, height: 150)

            return Rectangle()
                .fill(self.active ? Color.green : Color.clear)
                .frame(width: 150, height: 150)
                .overlay(img)
        
    

    struct MyDropDelegate: DropDelegate 
        @Binding var imageUrls: [Int: URL]
        @Binding var active: Int

        func validateDrop(info: DropInfo) -> Bool 
            return info.hasItemsConforming(to: ["public.file-url"])
        

        func dropEntered(info: DropInfo) 
            NSSound(named: "Morse")?.play()
        

        func performDrop(info: DropInfo) -> Bool 
            NSSound(named: "Submarine")?.play()

            let gridPosition = getGridPosition(location: info.location)
            self.active = gridPosition

            if let item = info.itemProviders(for: ["public.file-url"]).first 
                item.loadItem(forTypeIdentifier: "public.file-url", options: nil)  (urlData, error) in
                    DispatchQueue.main.async 
                        if let urlData = urlData as? Data 
                            self.imageUrls[gridPosition] = NSURL(absoluteURLWithDataRepresentation: urlData, relativeTo: nil) as URL
                        
                    
                

                return true

             else 
                return false
            

        

        func dropUpdated(info: DropInfo) -> DropProposal? 
            self.active = getGridPosition(location: info.location)

            return nil
        

        func dropExited(info: DropInfo) 
            self.active = 0
        

        func getGridPosition(location: CGPoint) -> Int 
            if location.x > 150 && location.y > 150 
                return 4
             else if location.x > 150 && location.y < 150 
                return 3
             else if location.x < 150 && location.y > 150 
                return 2
             else if location.x < 150 && location.y < 150 
                return 1
             else 
                return 0
            
        
    

【问题讨论】:

【参考方案1】:

和参考题几乎一样,关键是用NSImage作为模型,而不是直接使用Image,才能有能力访问图像数据以使用NSItemProvider

struct DragableImage: View 
    var image = NSImage(named: "grapes")

    var body: some View 
        Image(nsImage: image ?? NSImage())
            .resizable()
            .frame(width: 150, height: 150)
            .clipShape(Circle())
            .overlay(Circle().stroke(Color.white, lineWidth: 2))
            .padding(2)
            .overlay(Circle().strokeBorder(Color.black.opacity(0.1)))
            .shadow(radius: 3)
            .padding(4)
            .onDrag 
                let data = self.image?.tiffRepresentation
                let provider = NSItemProvider(item: data as NSSecureCoding?, typeIdentifier: kUTTypeTIFF as String)
                provider.previewImageHandler =  (handler, _, _) -> Void in
                    handler?(data as NSSecureCoding?, nil)
                
                return provider
            
    

【讨论】:

唯一的问题是可拖动的图像(绿色和蓝色)不会在放置时显示(它甚至不会播放来自 dropEntered 或 performDrop 的声音 (图像永远不会被添加到 DroppableArea)

以上是关于如何在 SwiftUI (macOS) 中拖放未捆绑的图像的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI:如何从 macOS 上的联系人中拖放联系人

如何在我的MacOS应用程序中拖放电影文件(.mov)?

如何在 Silverlight 中拖放“框”

如何在 JTable 中拖放一行?

如何使用 jQuery 在 div 中多次拖放图像以及在 drop div 中拖放图像?

如何在 React 中拖放多个元素?