Created
August 7, 2024 18:05
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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