Drag and Drop Editable Lists: Tutorial for SwiftUI
Grow your SwiftUI List skills beyond the basics. Implement editing, moving, deletion and drag-and-drop support. Then, learn how custom views can support draggable content. By Bill Morefield.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Drag and Drop Editable Lists: Tutorial for SwiftUI
30 mins
- Getting Started
- Understanding Data Flow in Drag Todo
- Editing Lists
- Deleting Items
- Using EditButton
- Moving Items
- Getting the Current EditMode
- Introducing Drag and Drop
- Building a Drop Target
- Implementing Your View
- Preparing for Drag Support
- Making a View Draggable
- Preparing for Drop Support
- Conforming to DropDelegate
- Dropping Between Views
- Trying Out Another Way to Drop
- Where to Go From Here
Preparing for Drag Support
SwiftUI — and UIKit — use NSItemProvider
to represent objects in a drag-and-drop session. While some built-in types, such as NSString
, NSURL
, UIImage
and UIColor
, support this out of the box, you’ll need to do a little bit of extra work to offer TodoItem
to the drag-and-drop gods.
To start, you’ll use NSItemProviderWriting
to convert the TODO item into a data representation upon request.
Open TodoItem.swift and add the following code to the bottom of the file:
extension TodoItem: NSItemProviderWriting {
// 1
static let typeIdentifier = "com.raywenderlich.DragTodo.todoItem"
// 2
static var writableTypeIdentifiersForItemProvider: [String] {
[typeIdentifier]
}
// 3
func loadData(
withTypeIdentifier typeIdentifier: String,
forItemProviderCompletionHandler completionHandler:
@escaping (Data?, Error?) -> Void
) -> Progress? {
// 4
do {
// 5
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
completionHandler(try encoder.encode(self), nil)
} catch {
// 6
completionHandler(nil, error)
}
// 7
return nil
}
}
In the code above, you:
- Define a new constant called
typeIdentifier
containing a string unique to your project. This becomes the item type used to distinguish your TODO item from other objects thatNSItemProvider
represents. - Implement
writableTypeIdentifiersForItemProvider
forNSItemProviderWriting
and return your unique type identifier. - Implement
loadData(withTypeIdentifier:forItemProviderCompletionHandler:)
. The framework will call this method to get the representation of this object, when necessary. In your implementation,typeIdentifier
will always come fromwritableTypeIdentifiersForItemProvider
. Because you only listed one type, you don’t need to worry about checking this argument. - Define a
do-catch
statement to catch any errors while encoding. - Create a
JSONEncoder
instance, set it to produce human-friendly output and then attempt to encode the current object. If encoding succeeds, you call the completion handler for the block and pass the encoded object to it. You’re able to pass theTodoItem
intoJSONEncoder
because it already conforms toCodable
. - If anything went wrong during the encoding, call the completion handler, but use the
error
object instead. - Return
nil
forProgress
because your encode operation was not asynchronous. It doesn’t need to report any progress back to the caller because it has already finished by this point.
Codable
, see our tutorial, Encoding and Decoding in Swift.Now that the system knows how to handle TodoItem
, you’ll implement drag support.
Making a View Draggable
Once you have a supported model object, adding drag support to a SwiftUI view is simple. All you need to do is implement onDrag(_:)
on the view you want to be draggable.
Open ActiveTodoView.swift and add the following modifier to TodoItemView
:
.onDrag {
NSItemProvider(object: item)
}
This lets TodoItemView support drag and drop gestures. The closure returns the draggable data.
onDelete(perform:)
and onMove(perform:)
, you can apply onDrag(_:)
to any view. Take care to add this modifier to the TodoItemView
within ForEach
, rather than where the other two modifiers are located.
Build and run, but this time, use an iPad or an iPad simulator. In the Active list, hold your finger down on an item until you see it lift out of the List
, then drag it around inside the app. It will look like this:
Why on an iPad?
Currently a SwiftUI List
isn’t compatible with drag and drop on the iPhone. If you were to try this on an iPhone, unfortunately, nothing would happen.
Until a future version of SwiftUI fixes this problem, the simplest fix is to get rid of the List
. Open ContentView.swift and replace List
around the active and completed TODO items with a ScrollView
.
Build and run back on the iPhone:
You can now drag items on an iPhone, but there’s a cost to this simple fix. You lost the editing functionality you built in the first part of this tutorial.
Tap Edit and notice the indicators for deleting and moving items no longer appear. That’s because only List
supports onDelete(perform:)
and onMove(perform:)
.
Hopefully, future versions of SwiftUI will let you have both at the same time. For now, you’ll need to choose one or the other on the iPhone.
For the rest of this tutorial, you can ignore the loss of editing support in List
.
Preparing for Drop Support
Since you’ve implemented NSItemProviderWriting
to support dragging, you might have guessed you now need to implement NSItemProviderReading
to implement dropping.
Open TodoItem.swift and add the following to the end of the file:
extension TodoItem: NSItemProviderReading {
// 1
static var readableTypeIdentifiersForItemProvider: [String] {
[typeIdentifier]
}
// 2
static func object(
withItemProviderData data: Data,
typeIdentifier: String
) throws -> TodoItem {
// 3
let decoder = JSONDecoder()
return try decoder.decode(TodoItem.self, from: data)
}
}
As you might expect, the implementation is similar to NSItemProviderWriting
except it performs the conversion in the opposite direction. Here, you:
- Specify
readableTypeIdentifiersForItemProvider
, a list of the type identifiers this implementation can manage. Again, only specify the customtypeIdentifier
from earlier. - Let
object(withItemProviderData:typeIdentifier:)
receive the data along with the type identifier used when writing it. Since you only support one type of data, you can ignore this second parameter. This method returnsTodoItem
, which only works becauseTodoItem
is afinal
class. - Create a
JSONDecoder
instance and attempt to decode the provided data into theTodoItem
type, then return it.
Unlike before, you have a bit more work to do before you can add drop support to a view. In the next section, you’ll look at what you need to do to offer a flexible approach to dropping an object.
Conforming to DropDelegate
The most flexible drop modifier for SwiftUI requires you provide a custom type conforming to DropDelegate
.
Create a new file for the delegate. Highlight the Models group, then select File ▸ New ▸ File… ▸ Swift File and call it TodoDropDelegate.swift. Replace the contents with the following:
import SwiftUI
struct TodoDropDelegate: DropDelegate {
// 1
@Binding var focusId: Int?
// 2
func performDrop(info: DropInfo) -> Bool {
// 3
guard info.hasItemsConforming(to: [TodoItem.typeIdentifier]) else {
return false
}
// 4
let itemProviders = info.itemProviders(for: [TodoItem.typeIdentifier])
guard let itemProvider = itemProviders.first else {
return false
}
// 5
itemProvider.loadObject(ofClass: TodoItem.self) { todoItem, _ in
let todoItem = todoItem as? TodoItem
// 6
DispatchQueue.main.async {
self.focusId = todoItem?.id
}
}
return true
}
}
Here’s what TodoDropDelegate
does:
- Declares
focusId
as a binding of an optionalInt
, which you’ll use to pass the identifier of the dropped TODO item back up the view chain. - Implements
performDrop(info:)
, which the framework calls when the drop operation takes place. It receives a parameter of typeDropInfo
that contains information about the drop operation. It returnstrue
if the drop is handled andfalse
if it isn’t. - Ensures something of the expected type is available with the
guard
statement. If not, returnfalse
. - Returns a collection of items matching the specified type identifiers with
itemProviders(for:)
. You only care about one type identifier, so you pass it as the sole element in the array. You’ll only process a single item, obtained usingfirst
on the results. If you wanted to deal with many items, you’d need to loop over them. - You take the resulting
NSItemProvider
and callloadObject(ofClass:completionHandler:)
. It, in turn, will call through to the method you implemented as part ofNSItemProviderReading
. - Inside the completion closure, you attempt to cast the object to
TodoItem
and assign its identifier tofocusId
. Be careful to update your binding only on the main thread by usingDispatchQueue
.