In my previous post we’ve had a first hands-on experience with the Transferable protocol; a new API that was introduced in WWDC 2022, and is meant to reduce dramatically the required efforts on our part in order to copy-paste, or drag and drop data within the same, or different applications.
Becoming more particular, in that first post I demonstrated how to drag and drop objects of custom types that conform to Codable protocol, with several interesting concepts to be covered along the way:
- content types (UTIs) and how to declare custom ones,
- proper conformance to Transferable, so objects of the presented custom type to be capable of being dragged,
- how to start a drag operation in SwiftUI,
- how to handle dropping in SwiftUI.
This post is pretty much a follow-up on the last one, as it focuses on another feature of Transferable; how to specify additional content to transfer, on top of the primary transferable content representation.
I strongly recommend to read the previous post about Transferable if you have not done so. Not only there are important concepts to understand there, but we will also continue building on the demo project of that post, which you can download from this link.
Before getting into the actual point, let me summarize what the demo app is all about. With a collection of color items being presented on the one side of the screen, the original purpose was to drag each item and drop it to the other side onto a VStack container; to display a duplicate of any color item into the target view by drag and dropping.
In this post we are going to extend that by adding a TextField view as an additional drop area. Ultimately, we’ll make it possible to drag a color item to the TextField, and drop the color’s name on it; a String value only, and not the entire color item object.
Here is a demonstration of all that:
The ProxyRepresentation
As it was made clear in the previous post, any custom type that conforms to Transferable protocol must be mandatorily implementing the following static property:
1 2 3 4 5 |
static var transferRepresentation: some TransferRepresentation { } |
That’s the place to specify what the transferable item is going to be. In the particular case that is being demonstrated in this and the last post, we have a custom type that describes color items programmatically and conforms to Codable protocol. Therefore, we need to make explicit that objects of this type are going to be the transferable items. Here is how we did that last time in the sample ColorItem
custom type:
1 2 3 4 5 6 7 8 9 |
struct ColorItem: Identifiable, Codable, Transferable { static var transferRepresentation: some TransferRepresentation { CodableRepresentation(for: ColorItem.self, contentType: .color) } // rest of the implementation… } |
With the first argument above we explicitly specify the type of the objects that we are planning to transfer. And although this is an optional value that we can avoid to supply, the next one is required; it’s the content type, or in other words, the Uniform Type Identifier (UTI) of the data that will be transferred. In the above example we have a custom content type, and most of the times that you’ll be implementing Transferable in custom types you’ll also need to define your own UTIs as well. Have a look to the previous post to find out more about all that.
With the above, an entire color item can be dragged from one place to another, with the actual instance to be serialized when dragging starts and deserialized on drop. Now, we are going to add something new to that; we are going to make it possible to also transfer just the name of a color item and not the entire object, or to rephrase that, we will specify an additional transferable representation for the color’s name only:
1 2 3 4 5 6 7 8 |
static var transferRepresentation: some TransferRepresentation { CodableRepresentation(for: ColorItem.self, contentType: .color) // The ProxyRepresentation is the new addition here: ProxyRepresentation(exporting: \.name) } |
A few important observations:
- The provided argument above is the key path to the
name
property, the value of which we want to transfer. - The
ProxyRepresentaton(exporting:)
must be always called after the primary representation. For instance, it would be wrong to callProxyRepresentation(exporting:)
beforeCodableRepresentation(for:contentType:)
. - The proxy representation actually uses the main representation of another type as if it was its own. Here, that other type is the String, because the
name
property is of that type. Notice that in contrast to theCodableRepresentation(for:contentType:)
we don’t have to specify a content type here. It’s taken from the String type, which is plain text.
That one single line is all we need in order to make the ColorItem
type capable of transferring a color item’s name as well. The next step is to add a TextField to the SwiftUI view, and enable dropping there so we can actually receive that name when an item is dragged.
Adding a TextField to the view
In the following code segment you can see the SwiftUI view implementation, taken directly from the original project implemented in the previous post:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
struct ContentView: View { @StateObject private var colors = Colors() @State private var draggedColorItem: ColorItem? @State private var borderColor: Color = .black @State private var borderWidth: CGFloat = 1.0 var body: some View { HStack { VStack { ForEach(colors.items, id: .id) { colorItem in ColorView(colorItem: colorItem) } } .frame(width: 250) .frame(maxHeight: 750) .padding(.leading) Divider().padding(.horizontal, 75) VStack { if draggedColorItem != nil { ColorView(colorItem: draggedColorItem!) } else { Text(“Drag and Drop a color here!”) .foregroundColor(.secondary) } } .frame(width: 280, height: 220) .background(Color.gray.opacity(0.25)) .border(borderColor, width: borderWidth) .padding(.trailing) .dropDestination(for: ColorItem.self) { items, location in draggedColorItem = items.first print(location) return true } isTargeted: { inDropArea in print(“In drop area”, inDropArea) borderColor = inDropArea ? .accentColor : .black borderWidth = inDropArea ? 10.0 : 1.0 } } } } |
The main container view is an HStack
so visual elements to be laid out horizontally on screen. On the left side, and in a ForEach
container, all sample color items are listed one after the other. On the right side there is a VStack
that constitutes the drop destination of a dragged color item. That VStack
contains either a dropped color item, or a prompt text message if an item has not been dropped yet.
We want to add a TextField
, so we are going to modify the above view. Leaving everything as it currently is up until the divider, we are going to embed the VStack
into another VStack
like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
HStack { … VStack { VStack { if draggedColorItem != nil { ColorView(colorItem: draggedColorItem!) } else { Text(“Drag and Drop a color here!”) .foregroundColor(.secondary) } } // … view modifiers … // Add the TextField here… } } |
We can now implement the TextField
right where the “Add the TextField here..” comment is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
HStack { … VStack { VStack { // Original VStack content… } // … view modifiers … // The implementation of the new TextField. TextField(“”, text: $colorName) .multilineTextAlignment(.center) .frame(width: 180, height: 60) .border(Color.gray, width: 2) .padding(.trailing, 20) .padding(.top, 50) } } |
There are a few view modifiers that specify the frame, border and padding of the TextField
. Notice that the binding value of the property called colorName
is passed as argument to the TextField
, but this property currently does not exist; we have to declare it in the view struct before going any further:
1 2 3 4 5 6 7 |
struct ContentView: View { @State private var colorName: String = “” … } |
The preparation of the SwiftUI view is now complete, so let’s allow dropping on this brand new TextField.
Setting the TextField as a drop destination for the color name
In order to make the text field capable of accepting dropping, it’s necessary to use a particular view modifier; a view modifier that we already have met in the previous post and you can see in the original SwiftUI view implementation listed at the beginning of the last part. That is the dropDestination(for:action:isTargeted:)
modifier.
Going through the provided arguments:
- The first argument is the content type of the transferable item. It’s optional and we can omit it.
- The second argument is a closure with two parameter values; an array with transferred items, and the location of the finger in iOS or the mouse in macOS within the drop area. We have to return true or false from this closure, depending on whether we accept the drop or not. This argument is required.
- The last argument is another closure, which is optional as well. Its parameter value informs us whether a dragged item is inside or outside the drop destination; an information that can be proved quite useful often.
Here, and for the sake of demonstration, we are going to provide all arguments. Besides, we are going to need the last one in order to clear the previous color name when another one gets inside the area of the text field.
1 2 3 4 5 6 7 8 9 |
TextField(“”, text: $colorName) // … other view modifiers … .dropDestination(for: String.self) { items, location in } isTargeted: { inDropArea in } |
See the first argument above, where we specify the String type as the expected one for the dragged items in the text field. Next, let’s add the missing content in the isTargeted
closure:
1 2 3 4 5 6 7 8 9 10 11 |
TextField(“”, text: $colorName) // … other view modifiers … .dropDestination(for: String.self) { items, location in } isTargeted: { inDropArea in if inDropArea { colorName = “” } } |
When a color item will be dragged above the text field, the inDropArea
above will become true. In such cases, we want to empty the value of the colorName
property, so the new color name to be set in the first closure.
Speaking of that, let’s add the last piece of the puzzle:
1 2 3 4 5 6 7 8 9 10 11 12 |
TextField(“”, text: $colorName) // … other view modifiers … .dropDestination(for: String.self) { items, location in colorName = items.first ?? “” return items.first != nil } isTargeted: { inDropArea in if inDropArea { colorName = “” } } |
Notice here that the colorName
gets either the value of the first item, which is going to be the dragged color item’s name, or an empty string value if no items exist in the items
parameter value. The return value of the closure also depends on the existence of elements in the items
array; if the first
property is not nil, then it’s returned true, otherwise false. Lastly, we don’t care about the location of the dragged item in this particular case, so we simply ignore it.
With this few lines of code only, we managed to make the TextField
a drop destination for the name of dragged color items:
Conclusion
It is an undeniable fact that the actual required implementation we made here is far from calling it long, even though the post became a bit lengthy because each step had to be explained along the way. Stepping on the project built in the previous, first tutorial about the Transferable protocol, we just met what it takes in order to specify an additional representation type, what to watch out for, and how to eventually make it possible to transfer alternative content on top of the original one. I hope you found this post useful, and if so, please don’t hesitate to share!
Thanks for reading, take care! ????
Download the final project from this link.
Related Posts
First Experience With Transferable Implementing Drag And Drop In SwiftUI