Hi and thank you in advance for the support. I’m working on a project that allows users to drag and drop files into and out of my app using the Drag and Drop API. From my understanding, dropping a file into the app requires the file to be an NSItemProvider to be eligible for a drop - like this:
func onDrop(of: [UTType], is Targeted: Binding<Bool>?, perform: ([NSItemProvider]) -> Bool) -> some View
and dragging a file out of the app requires the same:
func onDrag (() -> NSItemProvider) -> some View
The only success I’ve had with this is actually specifying the subtype of NSItemProvider the app will be receiving via drop, or dragging away - like this:
/*Example view with drop modifier*/
ScrollView {
Text(“Dropped text”)
.onDrag {
handleDragAway(for: text)
}
}
.onDrop(of: [UTType.text.identifier], isTargeted: nil) { providers in
handleDrop(providers: providers)
}
/*Handle drop of item NSItemProvider conforming to plain text*/
func handleDropInto(providers: [NSItemProvider]) -> Bool {
var didHandle = false
for provider in providers {
if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
_ = provider.loadObject(ofClass: String.self) { string, _ in
DispatchQueue.main.async {
if let string = string {
self.handlePlainText(text: string)
didHandle = true
}
}
}
} else {
didHandle = false
}
return didHandle
}
}
/*Handle dragging the item away*/
func handleDragAway(for text: String) -> NSItemProvider {
return NSItemProvider(object: text as NSString)
}
Essentially this code, albeit incomplete, gets the idea across. Drag text into the view, display the text, drag it out. Now, all the documentation I can find says you can rinse and repeat for basically any file type (with some processing) that conforms to NSItemProvider. Here is my question: what if I don’t want to specify file type - what if I just want to tell the app, the user is dragging in some file that conforms to NSItemProvider, don’t worry about its type. Display “File” to show the user they have successfully dropped a file into the view, but don’t do any with the file (this could be implemented later). Then, the user can drag that file away, conforming to NSItemProvider, and wherever they drag it can deal with its file type. Here is an implementation of this idea:
import SwiftUI
import UniformTypeIdentifiers
struct BoardItem {
let id: UUID
let provider: NSItemProvider
}
struct BoardItemView: View {
var body: some View {
Text("File")
.frame(width: 100, height: 100)
.border(Color.black)
}
}
struct BoardView: View {
@State private var boardItems: [BoardItem] = []
var body: some View {
ScrollView {
ForEach(boardItems, id: \.id) { item in
BoardItemView()
.onDrag {
return item.provider
}
}
}
.onDrop(of: [UTType.item.identifier], isTargeted: nil) { providers in
providers.forEach { provider in
let newItem = BoardItem(id: UUID(), provider: provider)
DispatchQueue.main.async {
boardItems.append(newItem)
}
}
return true
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
The code compiles and runs easily on iPadOS or visionOS. The user can drop anything into the view, “File” appears on screen, and the user can even grab the text (that represents the file). However, dragging the object into a new app does not work. There is no green “+” element to verify the object is droppable anywhere, and attempting to drop it anywhere results in a red console message in Xcode saying
Cannot retrieve a strong reference to PBItemCollection.
Does anyone know how to fix the functionality and the error?
I appreciate you engaging in a long post, thank you!
Post
Replies
Boosts
Views
Activity
SwiftData: "Illegal attempt to establish a relationship 'item' between objects in different contexts
I have run into this SwiftData issue in multiple projects and have been able to replicate it by building off of the default SwiftData launch project.
The original Item class:
class Item {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
New MyItem class to replicate the error. Notice I nest an Item object inside MyItem:
class MyItem {
var name: String
var item: Item
init(name: String, item: Item) {
self.name = name
self.item = item
}
}
I then build off of the default view for a SwiftData project. When the '+' button is pressed, a new list item for both Item and MyItem should appear in their appropriate sections.
@Environment(\.modelContext) private var modelContext
@Query private var items: [Item]
@Query private var myItems: [MyItem]
var body: some View {
NavigationSplitView {
List {
Section("All Items") {
ForEach(items) { item in
NavigationLink {
Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
} label: {
Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
}
}
}
Section("My Items") {
ForEach(myItems) { myItem in
NavigationLink {
Text("Item at \(myItem.item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
} label: {
HStack {
Text(myItem.name)
Spacer()
Text(myItem.item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
}
}
}
}
}
.toolbar {
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
} detail: {
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(timestamp: Date())
modelContext.insert(newItem)
let newMyItem = MyItem(name: "Test", item: newItem)
modelContext.insert(newMyItem)
}
}
}
The app crashes and I get the following error when I attempt to click the '+' button (which should create a new Item and MyItem in the modelContext:
Thread 1: "Illegal attempt to establish a relationship 'item' between objects in different contexts (source = <NSManagedObject: 0x600002166940> (entity: MyItem; id: 0x600000298240 x-coredata:///MyItem/t2D4951EB-0D2F-44B1-AF8C-5A1BB11659F53; data: {\n item = nil;\n name = Test;\n}) , destination = <NSManagedObject: 0x600002174000> (entity: Item; id: 0x600000232440 x-coredata:///Item/t2D4951EB-0D2F-44B1-AF8C-5A1BB11659F52; data: {\n timestamp = "2023-10-04 18:21:21 +0000";\n}))"
Can anyone help me understand the new SwiftData framework in this regard? I am still new to SwiftUI.
Hello!
I'm having a little issue with the onCommit feature for TextField.
Basically what I am trying to do is create a List with a Section header and a TextField inside, nothing crazy.
My current code looks something like this:
struct FormView: View {
@State private var myContent: String = ""
var body: some View {
Form {
Section(header: Text("Name")) {
TextField("Contents", text: $myContent, axis: .vertical, onCommit: {
// Perform action on commit
updateData()
})
}
}
}
private func updateData() {
// Perform the data update here
}
}
However, I am getting an error that says
Extra argument 'onCommit' in call
I'm able to get rid of the error by deleting either the
axis: .vertical or the onCommit: {...}.
I do want both of these functionalities, though, as the axis: .vertical essentially wraps the text to a new line when needed
and the onCommit: {...} updates my app database using UserDefaults.
Is there a way to create a TextField within a List and Section that has a $binding variable, axis, and onCommit functionality or am I dreaming?
I'm still a beginner in SwiftUI so there is probably a better way to do this, but basically what I want to do is have a form of code length management where I have all these different buttons in my app that do the same thing. So naturally I want to create a method to call in each button action so I don't have to copy and paste the action every time.
This is the method I created:
class NumberButtonLogic {
var operation : Int
var tempOp : Int
var output : String
var temp : String
var buttonNum : Int
init(operation: Int, tempOp: Int, output: String, temp: String, buttonNum: Int) {
self.operation = operation
self.tempOp = tempOp
self.output = output
self.temp = temp
self.buttonNum = buttonNum
}
func runLogic() {
if operation != 0 {
if output == "0." {
} else {
output = ""
}
tempOp = operation
operation = 0
}
if output == "0" || output == "0.0" {
if temp != "" {
} else {
output = ""
}
}
output += String(buttonNum)
}
}
Don't worry too much about the functionality of the runLogic() function, it's a mess, I know lol. But I basically want to have the different vars at the top be changed in runLogic().
From there, I go to a separate View area and create a new button:
@State private var output: String = "0"
@State private var operation: Int = 0
@State private var temp: String = ""
@State private var tempOp: Int = 0
var numButton1 : NumberButtonLogic {
NumberButtonLogic(operation: operation, tempOp: tempOp, output: output, temp: temp, buttonNum: 1)
}
Button {
numButton1.runLogic()
} label: {
Text("1")
}.buttonStyle(numberStyle())
Again don't worry about the numberStyle() style, it is working in the preview, so I don't think that's the issue with my code.
Basically what I want to do is display the number "1" on the screen when the button is pressed (but format it differently depending on what is already there). Also everything in the second code block I gave is in the struct ContentView : View {}.
Right now, when I press the button, I don't get any output, and the += modifier for output doesn't add the number "1" to the end of the string. However, if I put the code from the runLogic() function directly inside the button action, it runs fine. I hope that makes sense and thanks for your help!