Drag and Drop Tutorial for iOS
In this drag and drop tutorial you will build drag and drop support into UICollectionViews and between two separate iOS apps. By Christine Abernathy.
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
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 Tutorial for iOS
30 mins
- Getting Started
- Drag and Drop Overview
- Adding Drag Support
- Adding Drop Support
- Responding to Drops
- Drag and Drop in the Same App
- Follow My Moves
- Optimizing the Drop Experience
- Using In-Memory Data
- Moving Items Across Collection Views
- Are You My App?
- Adding a Placeholder
- Multiple Data Representations
- Reading and Writing Geocaches
- Back to My App
- Adding Drag Support to a Custom View
- Adding Drop Support to a Custom View
- Where to Go From Here?
Adding a Placeholder
Fetching items from an external app and loading them in the destination app could take time. It’s good practice to provide visual feedback to the user such as showing a placeholder.
Replace the .copy
case in collectionView(_:performDropWith:)
with the following:
print("Copying from different app...")
// 1
let placeholder = UICollectionViewDropPlaceholder(
insertionIndexPath: destinationIndexPath, reuseIdentifier: "CacheCell")
// 2
placeholder.cellUpdateHandler = { cell in
if let cell = cell as? CacheCell {
cell.cacheNameLabel.text = "Loading..."
cell.cacheSummaryLabel.text = ""
cell.cacheImageView.image = nil
}
}
// 3
let context = coordinator.drop(item.dragItem, to: placeholder)
let itemProvider = item.dragItem.itemProvider
itemProvider.loadObject(ofClass: NSString.self) { string, error in
if let string = string as? String {
let geocache = Geocache(
name: string, summary: "Unknown", latitude: 0.0, longitude: 0.0)
// 4
DispatchQueue.main.async {
context.commitInsertion(dataSourceUpdates: {_ in
dataSource.addGeocache(geocache, at: destinationIndexPath.item)
})
}
}
}
This is what’s going on:
- Create a placeholder cell for the new content.
- Define the block that configures the placeholder cell.
- Insert the placeholder into the collection view.
- Commit the insertion to exchange the placeholder with the final cell.
Build and run the app. Drag and drop an item from Reminders. Note the brief appearance of the placeholder text as you drop the item into a collection view:
Multiple Data Representations
You can configure the types of data that you can deliver to a destination app or consume from a source app.
When you create an item provider using init(object:)
, the object you pass in must conform to NSItemProviderWriting
. Adopting the protocol includes specifying the uniform type identifiers (UTIs) for the data you can export and handling the export for each data representation.
For example, you may want to export a string representation of your geocache for apps that only take in strings. Or you might want to export an image representation for photo apps. For apps under your control that use geocaches, you may want to export the full data model.
To properly consume dropped items and turn them into geocaches, your data model should adopt NSItemProviderReading
. You then implement protocol methods to specify which data representations you can consume. You’ll also implement them to specify how to coerce the incoming data based on what the source app sends.
Thus far, you’ve worked with strings when dragging and dropping geocaches between apps. NSString
automatically supports NSItemProviderWriting
and NSItemProviderReading
so you didn’t have to write any special code.
To handle multiple data types, you’ll change the geocache data model. You’ll find this in the Geocache project, which is part of the Xcode workspace you have open..
In the Geocache project, open Geocache.swift and add the following after the Foundation
import:
import MobileCoreServices
You need this framework to use predefined UTIs such as those representing PNGs.
Add the following right after your last import:
public let geocacheTypeId = "com.razeware.geocache"
You create a custom string identifier that will represent a geocache.
Reading and Writing Geocaches
Add the following extension to the end of the file:
extension Geocache: NSItemProviderWriting {
// 1
public static var writableTypeIdentifiersForItemProvider: [String] {
return [geocacheTypeId,
kUTTypePNG as String,
kUTTypePlainText as String]
}
// 2
public func loadData(
withTypeIdentifier typeIdentifier: String,
forItemProviderCompletionHandler completionHandler:
@escaping (Data?, Error?) -> Void)
-> Progress? {
if typeIdentifier == kUTTypePNG as String {
// 3
if let image = image {
completionHandler(image, nil)
} else {
completionHandler(nil, nil)
}
} else if typeIdentifier == kUTTypePlainText as String {
// 4
completionHandler(name.data(using: .utf8), nil)
} else if typeIdentifier == geocacheTypeId {
// 5
do {
let archiver = NSKeyedArchiver(requiringSecureCoding: false)
try archiver.encodeEncodable(self, forKey: NSKeyedArchiveRootObjectKey)
archiver.finishEncoding()
let data = archiver.encodedData
completionHandler(data, nil)
} catch {
completionHandler(nil, nil)
}
}
return nil
}
}
Here you conform to NSItemProviderWriting
and do the following:
- Specify the data representations you can deliver to the destination app. You want to return a string array ordered from the highest fidelity version of the object to the lowest.
- Implement the method for delivering data to the destination app when requested. The system calls this when an item is dropped and passes in the appropriate type identifier.
- Return the geocache’s image in the completion handler if a PNG identifier is passed in.
- Return the geocache’s name in the completion handler if a text identifier is passed in.
- If the custom geocache type identifier is passed in, return a data object corresponding to the entire geocache.
Now, add the following enum right after geocacheTypeId
is assigned:
enum EncodingError: Error {
case invalidData
}
You’ll use this to return an error code when there are problems reading in data.
Next, add the following to the end of the file:
extension Geocache: NSItemProviderReading {
// 1
public static var readableTypeIdentifiersForItemProvider: [String] {
return [geocacheTypeId,
kUTTypePlainText as String]
}
// 2
public static func object(withItemProviderData data: Data,
typeIdentifier: String) throws -> Self {
if typeIdentifier == kUTTypePlainText as String {
// 3
guard let name = String(data: data, encoding: .utf8) else {
throw EncodingError.invalidData
}
return self.init(
name: name,
summary: "Unknown",
latitude: 0.0,
longitude: 0.0)
} else if typeIdentifier == geocacheTypeId {
// 4
do {
let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
guard let geocache =
try unarchiver.decodeTopLevelDecodable(
Geocache.self, forKey: NSKeyedArchiveRootObjectKey) else {
throw EncodingError.invalidData
}
return self.init(geocache)
} catch {
throw EncodingError.invalidData
}
} else {
throw EncodingError.invalidData
}
}
}
Here you conform to NSItemProviderReading
to specify how to handle incoming data. This is what’s going on:
- Specify the types of incoming data the model can consume. The UTIs listed here represent a geocache and text.
- Implement the required protocol method for importing data given a type identifier.
- For a text identifier, create a new geocache with the name based on the incoming text and placeholder information.
- For the geocache identifier, decode the incoming data and use it to create a full geocache model.
Errors or unrecognized type identifiers throw the error you defined before.
Back to My App
Change the active scheme to Geocache and build the project. Then change the active scheme back to CacheMaker.
In CacheMaker, go to CachesDataSource.swift and inside dragItems(for:)
change the itemProvider
assignment to:
let itemProvider = NSItemProvider(object: geocache)
Here you can initialize your item provider with a geocache since your model adopts NSItemProviderWriting
to properly export data.
Open CachesViewController.swift and find collectionView(_:performDropWith:)
. In the .copy
case, replace the item provider’s loadObject
call with the following:
itemProvider.loadObject(ofClass: Geocache.self) { geocache, _ in
if let geocache = geocache as? Geocache {
DispatchQueue.main.async {
context.commitInsertion(dataSourceUpdates: {_ in
dataSource.addGeocache(geocache, at: destinationIndexPath.item)
})
}
}
}
You’ve modified the drop handler to load objects of type Geocache
. The completion block now returns a geocache that you can use directly.
Build and run the app. Place Reminders in Split View if necessary. Check that dragging and dropping items between Reminders and CacheMaker works as before:
Bring Photos in Split View to replace Reminders. Drag a geocache from your in-progress lane and drop it into Photos to verify that you can export an image representation of the geocache:
You can test the full data model export path with a temporary hack. Go to CachesViewController.swift and in collectionView(_:dropSessionDidUpdate:withDestinationIndexPath:)
replace the line that returns the move operation with the following:
return UICollectionViewDropProposal(
operation: .copy,
intent: .insertAtDestinationIndexPath)
You’re configuring drag-and-drops within the same app as copy operations. This triggers the code that should export and import the full data model.
Build and run the app. Test that moving an item within the app makes a proper copy of the geocache:
Revert your temporary hack in collectionView(_:dropSessionDidUpdate:withDestinationIndexPath:)
so that in-app drag-and-drops executes a move operation:
return UICollectionViewDropProposal(
operation: .move,
intent: .insertAtDestinationIndexPath)
Build and run the app to get back to pre-hack conditions.