SwiftUI拖放操作onDrop
SwiftUI拖放操作onDrop

SwiftUI拖放操作onDrop

onDrop 是 SwiftUI 中用于处理拖放操作(Drag & Drop)的视图修饰符,它允许让某个视图变成“拖入目标”,接收用户从别处拖进来的文件、图片、文本、URL 等数据。

基本介绍

onDrop代码:

func onDrop(of supportedTypes: [UTType], isTargeted: Binding<Bool>? = nil, perform: @escaping ([NSItemProvider]) -> Bool) -> some View

作用:使视图变成拖入目标(Drop Target)

平台:macOS / iPadOS / iOS(支持多平台)

类型安全:支持使用 UTType(如 .fileURL, .image, .plainText)

基本用途

1、拖入文件:拖动文件到窗口中。

2、拖入图片:拖动图片到 ImageView 中显示。

3、拖入文本:拖动文本更新文本框。

4、拖入自定义数据:通过 NSItemProvider 解码模型。

常用格式

常用的onDrop格式、参数:

.onDrop(of: [UTType.fileURL], isTargeted: $isHovering) { providers in
    // 处理 NSItemProvider 列表
    return true // 返回是否接受了拖入内容
}

参数说明:

1、of:支持的拖入类型,使用 [UTType]。

2、isTargeted:Binding绑定变量,在拖入悬停时自动设为 true,可用于 UI 高亮。

Text(filePath ?? "拖入文件")
    .frame(width: 300, height: 200)
    .background(isTargeted ? Color.blue.opacity(0.3) : Color.gray.opacity(0.2))
    .onDrop()

当拖动文件到该文本上时,isTargeted自动变为true,背景色会根据isTargeted进行调整。

3、perform:拖放时调用的回调函数,传入 NSItemProvider 列表

使用场景

1、拖入文件URL

import SwiftUI
import UniformTypeIdentifiers

struct DropFileView: View {
    @State private var isTargeted = false
    @State private var filePath: String?

    var body: some View {
        VStack {
            Text(filePath ?? "拖入文件")
                .frame(width: 300, height: 200)
                .background(isTargeted ? Color.blue.opacity(0.3) : Color.gray.opacity(0.2))
                .cornerRadius(12)
                .onDrop(of: [UTType.fileURL], isTargeted: $isTargeted) { providers in
                    if let provider = providers.first {
                        provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { data, _ in
                            DispatchQueue.main.async {
                                if let data = data as? Data,
                                   let url = URL(dataRepresentation: data, relativeTo: nil) {
                                    filePath = url.path
                                }
                            }
                        }
                        return true
                    }
                    return false
                }
        }
    }
}

2、拖入图片

.onDrop(of: [.image]) { providers in
    providers.first?.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { data, _ in
        if let data = data, let nsImage = NSImage(data: data) {
            DispatchQueue.main.async {
                self.image = nsImage
            }
        }
    }
    return true
}

支持类型

1、.fileURL:文件路径(如访达中拖文件)。

.onDrop(of: [.fileURL], isTargeted: $isTargeted) { providers in
    for provider in providers {
        provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { (data, _) in
            if let data = data as? Data,
               let url = URL(dataRepresentation: data, relativeTo: nil) {
                DispatchQueue.main.async {
                    print("拖入文件路径:\(url.path)")
                    // 你可以加载文件,或者保存路径
                }
            }
        }
    }
    return true
}

2、.image:图片(PNG、JPEG)。

.onDrop(of: [.image], isTargeted: $isTargeted) { providers in
    images = []
    for provider in providers {
        provider.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { data, _ in
            if let data = data, let nsImage = NSImage(data: data) {
                DispatchQueue.main.async {
                    self.images.append(nsImage)
                }
            }
        }
    }
    return true
}

3、.plainText:文本。

.onDrop(of: [.plainText], isTargeted: $isTargeted) { providers in
    for provider in providers {
        provider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { (data, _) in
            if let data = data as? Data,
               let text = String(data: data, encoding: .utf8) {
                DispatchQueue.main.async {
                    print("拖入文本:\(text)")
                }
            }
        }
    }
    return true
}

4、.url:网址(Safari拖出)。

.onDrop(of: [.url], isTargeted: $isTargeted) { providers in
    for provider in providers {
        provider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { (data, _) in
            if let data = data as? Data,
               let url = URL(dataRepresentation: data, relativeTo: nil) {
                DispatchQueue.main.async {
                    print("拖入 URL:\(url)")
                }
            }
        }
    }
    return true
}

5、.pdf:PDF 文档。

.onDrop(of: [.pdf], isTargeted: $isTargeted) { providers in
    for provider in providers {
        provider.loadDataRepresentation(forTypeIdentifier: UTType.pdf.identifier) { data, _ in
            if let data = data {
                DispatchQueue.main.async {
                    let pdf = NSPDFImageRep(data: data)
                    print("拖入 PDF 页数:\(pdf?.pageCount ?? 0)")
                }
            }
        }
    }
    return true
}

6、.movie:视频。

.onDrop(of: [.movie], isTargeted: $isTargeted) { providers in
    for provider in providers {
        provider.loadItem(forTypeIdentifier: UTType.movie.identifier, options: nil) { (data, _) in
            if let data = data as? Data {
                let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("temp.mov")
                try? data.write(to: tempURL)
                DispatchQueue.main.async {
                    print("拖入视频保存到:\(tempURL)")
                }
            }
        }
    }
    return true
}

7、.data:原始数据。

.onDrop(of: [.data], isTargeted: $isTargeted) { providers in
    for provider in providers {
        provider.loadDataRepresentation(forTypeIdentifier: UTType.data.identifier) { data, _ in
            if let data = data {
                DispatchQueue.main.async {
                    print("拖入原始数据大小:\(data.count)")
                }
            }
        }
    }
    return true
}

扩展知识

1、如何判断是否拖入区域?

通过绑定 @State var isTargeted 实现:

@State private var isTargeted = false
    .onDrop(of: ..., isTargeted: $isTargeted) { ... }
    .background(isTargeted ? Color.blue : Color.clear)

2、为什么onDrop必须return true?

SwiftUI 的 onDrop 要求在 closure 的结尾返回 Bool,用于告诉系统:

true:成功接受了拖入的数据(处理了)。

false:拒绝拖入的数据(没处理)。

如果return false,即使读取成功,那么系统会认为“这次拖放成功” → 无高亮动画,无释放动画。

3、上传多个图片

struct ContentView: View {
    @State private var isTargeted = true
    @State private var images: [NSImage] = []
    var body: some View {
        if !images.isEmpty {
            ForEach(images, id: \.self) { img in
                Image(nsImage: img)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 300,height:200)
            }
        } else {
            Text("没有照破")
        }
        Text("拖入文件")
            .frame(width: 300, height: 200)
            .background(isTargeted ? Color.blue.opacity(0.3) : Color.gray.opacity(0.2))
            .cornerRadius(12)
            .onDrop(of: [.image],isTargeted: $isTargeted) { providers in
                images = []
                for provider in providers {
                    provider.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { data, _ in
                        if let data = data, let nsImage = NSImage(data: data) {
                            DispatchQueue.main.async {
                                self.images.append(nsImage)
                            }
                        }
                    }
                }
                return true
            }
    }
}

4、背后原理

SwiftUI 的 onDrop 封装了 AppKit 的 NSDraggingDestination(macOS)或 UIKit 的 UIDropInteraction(iOS)。

实际上传入的数据是 NSItemProvider,可以通过它异步读取拖入的数据内容。

多平台统一接口,支持大部分常见数据类型。

常见问题

1、onDrop 不触发:没有指定正确的 UTType。

2、数据加载失败:没有在主线程中更新 UI。

3、多个 providers 忽略:providers.first 只处理一个拖入项。

4、拖入后没有 UI 提示:忘记绑定 isTargeted。

总结

onDrop支持跨平台,适用于macOS、ipadOS和iOS,支持文本、图片、文件等类型。

   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注