I am writing an app based around DoubleColumnNavigationViewStyle (eg. SplitView).
I would like to drag entities from the MasterList onto entities in the Detail View to form coredata relationships between them.
My models implement NSItemProviderWriting and NSItemProviderReading.
I find I can make a NavigationLink draggable by adding this to it:
.itemProvider { () -> NSItemProvider? in
return NSItemProvider(object: self.model) }
When a drag is instigated from here, the model's NSItemProviderWriting protocol functions are invoked.
What I cannot work out is how to make a View 'Dropable'.
All of the likely looking ViewModifiers are implemented on MacOS only, as far as I can see.
There must be something that takes a list of supported type identifiers and invokes NSItemProviderReading.
There is also something strange going on ...
Not all of my NavigationLinks have a .itemProvider but once one of them does, they all act draggable, which is not the desired outcome.
Are these known bugs or omissions?
There's a `.onInsert(of:perform:)` view modifier on DynamicContentView (to which only ForEach currently conforms) that does this. You pass in an array of accepted type identifiers, and then your block is called. It's passed the index on which the drop occurred (within the content of the ForEach) and an array of NSItemProvider instances.
Here's an iPad app which demonstrates its use:
fileprivate let dataUTI = "data.raw.dragdata"
fileprivate final class DragData: NSObject, NSItemProviderWriting, NSItemProviderReading {
var text: String
init(text: String) {
self.text = text
}
static var writableTypeIdentifiersForItemProvider: [String] { [dataUTI] }
func loadData(
withTypeIdentifier typeIdentifier: String,
forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void
) -> Progress? {
let progress = Progress(totalUnitCount: 1)
DispatchQueue.global(qos: .userInitiated).async {
progress.completedUnitCount = 1
let data = self.text.data(using: .utf8)
completionHandler(data, nil)
}
return progress
}
static var readableTypeIdentifiersForItemProvider: [String] { [dataUTI] }
static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> DragData {
guard let str = String(data: data, encoding: .utf8) else {
throw CocoaError(CocoaError.Code.fileReadInapplicableStringEncoding)
}
return DragData(text: str)
}
}
fileprivate func provider(for text: String) -> NSItemProvider {
NSItemProvider(object: DragData(text: text))
}
struct ContentView: View {
@State private var leftItems = (1...20).map { "Left \($0)" }
@State private var rightItems = (1...20).map { "Right \($0)" }
var body: some View {
HStack {
ItemListView(items: $leftItems)
Divider()
ItemListView(items: $rightItems)
}
}
}
struct ItemListView: View {
@Binding var items: [String]
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
.itemProvider { provider(for: item) }
}
.onInsert(of: [dataUTI]) { idx, providers in
self.insert(at: idx, from: providers)
}
}
}
private func insert(at index: Int, from providers: [NSItemProvider]) {
// simple async load of all items
let group = DispatchGroup()
var strings = [String](repeating: "", count: providers.count)
DispatchQueue.concurrentPerform(iterations: providers.count) { idx in
group.enter()
providers[idx].loadObject(ofClass: DragData.self) { obj, error in
if let data = obj as? DragData {
strings[idx] = data.text
} else if let error = error {
print("Drop decode error: \(error)")
}
group.leave()
}
}
group.notify(queue: .main) {
self.items.insert(contentsOf: strings, at: index)
}
}
}