Skip to content

Instantly share code, notes, and snippets.

@gboyegadada
Last active March 8, 2024 15:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gboyegadada/43fc5950187a3417cc9c81a6ef9f5ec5 to your computer and use it in GitHub Desktop.
Save gboyegadada/43fc5950187a3417cc9c81a6ef9f5ec5 to your computer and use it in GitHub Desktop.
[Mac OS] Custom Copy / Paste with Swift UI Transferable + support for NSPasteboard (NSPasteboardWriting, NSPasteboardReading) and drag / drop
//
// TransferableItem.swift
//
// Created by Gboyega Dada on 22/11/2023.
//
// @see https://stackoverflow.com/a/57648296/1661299
// @see https://exploringswift.com/blog/creating-a-nsitemprovider-for-custom-model-class-drag-drop-api
// @see https://stackoverflow.com/a/66169874/1661299
//
import Foundation
import SwiftUI
import UniformTypeIdentifiers
final class TransferableItem: NSObject, Identifiable, Codable {
var id: String = UUID().uuidString
let name: String
let email: String
init(name: String, email: String) {
self.name = name
self.email = email
}
convenience init?(pasteboardPropertyList propertyList: Any, ofType type: NSPasteboard.PasteboardType) {
guard let data = propertyList as? Data,
let instance = try? PropertyListDecoder().decode(TransferableItem.self, from: data) else { return nil }
self.init(name: instance.name, email: instance.email)
}
}
extension TransferableItem {
static func == (lhs: TransferableItem, rhs: TransferableItem) -> Bool {
return
lhs.id == rhs.id &&
lhs.name == rhs.name &&
lhs.email == rhs.email
}
override func isEqual(_ object: Any?) -> Bool {
guard let other = object as? TransferableItem else {
return false
}
return id == other.id &&
name == other.name &&
email == other.email
}
override var hash: Int {
var hasher = Hasher()
hasher.combine(id)
hasher.combine(name)
hasher.combine(email)
return hasher.finalize()
}
}
extension TransferableItem: Transferable {
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(contentType: .transferableItem) { item in
try PropertyListEncoder().encode(item)
} importing: { data in
try PropertyListDecoder().decode(TransferableItem.self, from: data)
}
/// ⚠️ Draggable won't work without this ⚠️
DataRepresentation(contentType: .data) { item in
try PropertyListEncoder().encode(item)
} importing: { data in
try PropertyListDecoder().decode(TransferableItem.self, from: data)
}
}
}
extension TransferableItem: NSPasteboardWriting, NSPasteboardReading {
public func writingOptions(forType type: NSPasteboard.PasteboardType, pasteboard: NSPasteboard) -> NSPasteboard.WritingOptions {
return .promised
}
public func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
return [.transferableItem]
}
public func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? {
if type == .transferableItem {
return try? PropertyListEncoder().encode(self)
}
return nil
}
static func readableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
return [.transferableItem]
}
public static func readingOptions(forType type: NSPasteboard.PasteboardType, pasteboard: NSPasteboard) -> NSPasteboard.ReadingOptions {
return .asData
}
}
extension UTType {
static var transferableItem: UTType { UTType(exportedAs: "com.example.MyApp.TransferableItem") }
}
extension NSPasteboard.PasteboardType {
static var transferableItem = NSPasteboard.PasteboardType("com.example.MyApp.TransferableItem")
}
//
// USAGE
// -----------------------------------------
// TransferableItemExampleView.swift
//
// Created by Gboyega Dada on 24/11/2023.
//
import SwiftUI
struct TransferableItemExampleView: View {
@State var listItems: [TransferableItem] = [
TransferableItem(name: "Sam", email: "sam@example.com"),
TransferableItem(name: "Quorra", email: "quorra@example.com"),
TransferableItem(name: "Kevin", email: "kevin@example.com"),
TransferableItem(name: "Tron", email: "tron@example.com"),
TransferableItem(name: "Zuse", email: "zuse@example.com"),
TransferableItem(name: "Gem", email: "gem@example.com"),
]
@State var selected: Set<TransferableItem> = []
/// For toolbar copy / paste buttons
private let nspasteboard = NSPasteboard.general
var body: some View {
VStack(alignment: .leading) {
Text("My Copyable Items")
.font(.title2)
List(listItems) { item in
Label(item.name, systemImage: "person")
/// Draggable !
.draggable(item) {
Text(item.name) // Preview
}
}
/// This will work with system pasteboard COPY command
.copyable(Array(selected))
/// This will work with system pasteboard CUT command
.cuttable(for: TransferableItem.self) {
for item in selected {
listItems.removeAll(where: { $0 == item })
}
return Array(selected)
}
/// This will work with system pasteboard PASTE command
.pasteDestination(for: TransferableItem.self) { values in
listItems.append(contentsOf: values)
} validator: { values in
values.filter { !$0.name.isEmpty }
}
}
/// This allows you drop transferable items into the list
.dropDestination(for: TransferableItem.self) { values, location in
listItems.append(contentsOf: values)
return true
} isTargeted: { isTargeted in
// Highlight target e.t.c
}
/// For copy / paste buttons in your toolbar, you may need to use NSPasteboard directly
.toolbar {
Button("Copy Selected Items") {
/// @see https://developer.apple.com/forums/thread/730619
nspasteboard.prepareForNewContents()
nspasteboard.writeObjects(Array(selected))
}
.disabled(selected.count == 0)
Button("Cut Selected Items") {
for item in selected {
listItems.removeAll(where: { $0 == item })
}
/// @see https://developer.apple.com/forums/thread/730619
nspasteboard.prepareForNewContents()
nspasteboard.writeObjects(Array(selected))
}
.disabled(selected.count == 0)
Button {
guard let values = nspasteboard.readObjects(forClasses: [TransferableItem.self]) as? [TransferableItem] else { return }
listItems.append(contentsOf: values)
} label: {
Label("Paste Items", systemImage: "doc.on.clipboard")
}
.disabled(!nspasteboard.canReadObject(forClasses: [TransferableItem.self]))
}
}
}
#Preview {
TransferableItemExampleView()
}
@gboyegadada
Copy link
Author

gboyegadada commented Nov 24, 2023

Psst! I'm open to work opportunities LinkedIn 👀. I've done mostly full stack web development for 10+ years but I'm currently in-between jobs. Also check out my new macOS app 🥳

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment