Skip to content

Instantly share code, notes, and snippets.

@davidbalbert
Created August 7, 2024 18:05
Show Gist options
  • Save davidbalbert/b40bbc64fde65b86079aaefc1b1d0730 to your computer and use it in GitHub Desktop.
Save davidbalbert/b40bbc64fde65b86079aaefc1b1d0730 to your computer and use it in GitHub Desktop.
A variant of the SwiftUI dropDestination view modifier that supports an isValid callback. SwiftUI drag and drop has some annoying hangs, which this also exhibits. See here for more: https://github.com/davidbalbert/DragAndDropPerformance
//
// Transferable+Extensions.swift
// ImageWriter
//
// Created by David Albert on 8/5/24.
//
import CoreTransferable
import os
import SwiftUI
import UniformTypeIdentifiers
extension Transferable {
// static var readableContentTypes: [UTType]
@_silgen_name("$s16CoreTransferable0B0PAAE20readableContentTypesSay22UniformTypeIdentifiers6UTTypeVGvgZ")
static func _readableContentTypes() -> [UTType]
}
fileprivate struct DropDestinationModifier<T>: ViewModifier where T: Transferable {
struct Delegate: DropDelegate {
let action: ([T], CGPoint) -> Bool
let isValid: ([T], CGPoint) -> Bool
let isTargeted: (Bool) -> Void
func performDrop(info: DropInfo) -> Bool {
let items = extractItems(info: info)
let location = info.location
let res = action(items, location)
isTargeted(false)
return res
}
func extractItems(info: DropInfo) -> [T] {
let providers = info.itemProviders(for: T._readableContentTypes())
let group = DispatchGroup()
let items = OSAllocatedUnfairLock<[T]>(initialState: [])
for provider in providers {
group.enter()
_ = provider.loadTransferable(type: T.self) { result in
// TODO: do we know this is true?
dispatchPrecondition(condition: .notOnQueue(.main))
defer { group.leave() }
switch result {
case .success(let item):
items.withLock {
$0.append(item)
}
case .failure(let error):
print("Error loading transferable: \(error)")
}
}
}
group.wait()
return items.withLock {
$0
}
}
func dropEntered(info: DropInfo) {
isTargeted(true)
}
func dropExited(info: DropInfo) {
isTargeted(false)
}
func validateDrop(info: DropInfo) -> Bool {
let items = extractItems(info: info)
let location = info.location
return isValid(items, location)
}
}
let action: ([T], CGPoint) -> Bool
let isValid: ([T], CGPoint) -> Bool
let isTargeted: (Bool) -> Void
func body(content: Content) -> some View {
content
.onDrop(of: T._readableContentTypes(), delegate: Delegate(action: action, isValid: isValid, isTargeted: isTargeted))
}
}
extension View {
func dropDestination<T: Transferable>(
for type: T.Type,
action: @escaping ([T], CGPoint) -> Bool,
isValid: @escaping ([T], CGPoint) -> Bool,
isTargeted: @escaping (Bool) -> Void
) -> some View {
modifier(DropDestinationModifier(action: action, isValid: isValid, isTargeted: isTargeted))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment