Android Test-Driven Development by Tutorials,
Second Edition – Now Updated!

Build testable, sustainable Android apps via JUnit, Mockito, and Espresso
by diving into test-driven development (TDD) in this newly-updated book.

Home iOS & Swift Tutorials

Drag and Drop Tutorial for SwiftUI

Learn how to use the drag and drop API in SwiftUI by building your own simple iPadOS and iOS bug reporting app.

5/5 4 Ratings

Version

  • Swift 5, iOS 14, Xcode 12

Drag and drop is the concept of picking up a UI element and dropping it somewhere else. Drag and drop supports the metaphor of your app as a surface to be manipulated and touched and not just a set of buttons to be poked at.

You’re not required to implement drag and drop, but doing so enhances the native feeling of your app and increases integration with iOS as a whole. You should provide drag and drop wherever you’d expect to be able to move an object.

SwiftUI provides first-class support for drag and drop in the List collection, as well as the API for any view to support drag and drop.

In this tutorial, you’ll build a simple bug-reporting iPad app that helps you track detected bugs and export a list of them in a shareable format. Along the way, you’ll learn how to:

  • Use the implicit collection drag and drop API.
  • Implement explicit drag and drop in any SwiftUI view.
  • Render a view to an image to allow inter-app dragging.
Note: You should be comfortable with reading and creating SwiftUI code to do this tutorial. If this is your first experience with SwiftUI, you might like to try another starter tutorial first, such as SwiftUI: Getting Started.

Getting Started

Download the starter material using the Download Materials button at the top or bottom of the tutorial. In the starter folder, locate and open PointyBug.xcodeproj. PointyBug is an app for you to log UI/UX bugs against any app you might be building or testing.

Demo simulator window with field for describing bugs

The starter project is a semi-complete app to which you’ll add drag and drop actions. To keep you focused on the drag and drop elements, the CRUD logic is complete and resides in a class called BugController.

Project files navigator

BugController manages a model array of Bug objects and associated DisplayElement objects. The BugController class conforms to @ObservableObject and broadcasts model changes to the SwiftUI view stack. A single BugController is instantiated in AppMain and injected into the view hierarchy as an @EnvironmentObject, allowing all the views to access the instance.

Select the Simulator iPad Pro (9.7-inch) in the active scheme settings:

Select run target

Build and run. Then, rotate the simulated device to landscape. You can see a Master-Detail split view:

Master-Detail split view of application

Tap Add Bug to add a new bug to the list. You can edit the description text, but tapping the photo button in the navigation bar doesn’t do anything. That seems like a bug!

Take a screenshot by selecting Device ▸ Trigger Screenshot from the menu. You’ll use that image later in the tutorial.

Now, you’ll learn how to add drag and drop to a SwiftUI collection view.

Adding Drop Support to a Collection

SwiftUI has a List collection view. You use ForEach in a List to iterate over a collection of data items. ForEach conforms to DynamicViewContent which provides three methods to deal with drop, move and delete operations.

In this section, you’ll update the master List to accept images and text from any dragging source.

In the Project navigator, inside the PointyBug/Views group, open BugListView.swift. Next, open the SwiftUI Preview Canvas with the key combination Command-Option-Return. Resume the preview with the key combination Command-Option-P. You have a List and a Button at the bottom of the screen. Each item in the list is a NavigationLink that opens an EditorView.

Activate SwiftUI preview update

In BugListView.swift inside List, add this modifier to the closing brace of ForEach:

.onInsert(of: [.plainText, .image]) { index, itemProviders in
  // insert here later 
}

You can make it easier to see where to add the code by “folding” the ForEach statement. To do this, click shaded area to the left of the line containing ForEach. Or, place your cursor anywhere inside the ForEach closure and select Editor ▸ Code Folding ▸ Fold from the menu.

click in the ribbon to fold the code

Although the word drop isn’t mentioned, you told List to accept pasted content of UTType.image and UTType.plainText. When the content is dropped, you run a closure with parameters of index that describe where in the list to put the content and itemProviders that serves as an array of NSItemProvider objects.

What are these UTType types? Before you finish your list drop implementation, you’ll go down a side road for a while.

What Are Uniform Type Identifiers?

A Uniform Type Identifier (UTI) is a way of describing the types of data your system uses. UTI’s are a class hierarchy describing data formats:

Organizational chart of UTI types

By using UTType.image, you told your list to accept any pasteboard content that looks like image data, such as a JPEG (UTType.jpeg) or a PNG (UTType.png). Both these types conform to UTType.image. There’s a UTType declared for most data types you’ll encounter during iOS development.

Later in the tutorial, you’ll create a new UTType for private use in the app.

Decoding Dropped Data

In this section, you’ll find out how to extract the dropped data and add that data to your model. The array of NSItemProvider objects wraps the data that’s passed to the closure in onInsert via the itemProviders parameter.

The first thing to do is create an object to deal with this unwrapping task. You never want to place model management logic inside your SwiftUI views.

Create a new Swift file called ContentDropController.swift in the Controller group.

First, define the class by placing this declaration at the top of ContentDropController.swift:

import SwiftUI

class ContentDropController {
  let bugController: BugController
  let bugID: BugID?

  init(bugID: BugID?, bugController: BugController) {
    self.bugID = bugID
    self.bugController = bugController
  }
}

ContentDropController talks to BugController. You want to know about the specific bug you’re updating, so you pass the bugID.

Next, you’ll create some utility methods that unwrap an NSItemProvider and ask BugController to create or update an existing bug.

Creating a Bug With Image

Now, add this extension to the end of the file:

extension ContentDropController {
  private func unwrapImage(
    from provider: NSItemProvider,
    completion: @escaping (UIImage?) -> Void
  ) {
    _ = provider.loadObject(ofClass: UIImage.self) { image, error in
      var unwrappedImage: UIImage?
      defer {
        completion(unwrappedImage)
      }
      if let error = error {
        print("image drop failed -", error.localizedDescription)
      } else {
        unwrappedImage = image as? UIImage
      }
    }
  }
}

In unwrapImage(from:completion:), you ask an NSItemProvider that holds a UIImage to load that image. You then call completion with that optional image.

Next, add this method to the same extension:

private func createBugWithImage(
  from provider: NSItemProvider,
  at dropIndex: Int
) {
  // 1
  unwrapImage(from: provider) { image in
    if let image = image {
      // 2
      let imageName = ImageController.store(image)
      // 3
      var bug = self.bugController.createBug()
      bug.imageName = imageName
      // 4
      DispatchQueue.main.async {
        self.bugController.insert(bug, at: dropIndex)
      }
    }
  }
}

createBugWithImage(from:at:) creates a bug that has an image:

  1. First, you use unwrapImage(from:completion:) to get the image from NSItemProvider.
  2. Store image as Data in the app documents folder and receive the storage name back.
  3. Create a new Bug and set the image’s name.
  4. Ask BugController to add the new bug to the model list at the requested index. Because the drop request occurs off the main queue, you need to dispatch the call to BugController back to the main queue.

Updating a Bug and Creating a Bug with Text

Finally, add these two methods to the extension:

private func updateBugWithImage(
  from provider: NSItemProvider,
  at dropIndex: Int
) {
  guard 
    let bugID = bugID,
    let selectedBug = self.bugController.bug(withID: bugID) 
  else {
    return
  }

  unwrapImage(from: provider) { image in
    guard 
      let image = image,
      let imageName = ImageController.store(image) 
    else {
      return
    }
    var selectedBug = selectedBug
    selectedBug.imageName = imageName
    DispatchQueue.main.async {
      self.bugController.update(selectedBug)
    }
  }
}

updateBugWithImage(from:at:) handles the case where you’re adding an image to a bug that already exists. You also store the dropped image, but instead of creating a new bug, you update an existing one.

Finally, add the following method to the extension:

private func createBugWithString(
  from provider: NSItemProvider,
  at dropIndex: Int
) {
  _ = provider.loadObject(ofClass: String.self) { text, _ in
    var newBug = self.bugController.createBug()
    newBug.text = text ?? ""
    DispatchQueue.main.async {
      self.bugController.insert(newBug, at: dropIndex)
    }
  }
}

createBugWithString(from:at:) is like createBugWithImage(from:at:). You ask an NSItemProvider that holds a String to load that string. Then you create a new bug with that string.

You now have a way to create and update bugs with dropped images. Next, you’ll add a way to handle a list of different item providers and create or update bugs accordingly.

Receiving Dropped Data

Add this method to the end of the extension in ContentDropController.swift:

@discardableResult
func receiveDrop(
  dropIndex: Int,
  itemProviders: [NSItemProvider],
  create: Bool = true
) -> Bool {
  var result = false
  for provider in itemProviders {
    if provider.canLoadObject(ofClass: String.self) {
      result = true
      createBugWithString(from: provider, at: dropIndex)
    } else if provider.canLoadObject(ofClass: UIImage.self) {
      result = true
      if create {
        createBugWithImage(from: provider, at: dropIndex)
      } else {
        updateBugWithImage(from: provider, at: dropIndex)
      }
    }
  }
  return result
}

receiveDrop(dropIndex:itemProviders:create:) is the public face of ContentDropController. In this method, you iterate through the array of NSItemProviders. If a provider holds a String, you create a Bug with that text. Otherwise, if the provider contains a UIImage, you create or update a bug with that image. The return value says whether you found something you wanted to use or not.

Next, you’ll use ContentDropController in BugListView to receive the drop.

Finishing the Drop

Open BugListView.swift and locate the onInsert(of:perform:) call that you added before. In the trailing closure, add the following lines:

let dropController = ContentDropController(
  bugID: nil,
  bugController: bugController)
dropController.receiveDrop(
  dropIndex: index,
  itemProviders: itemProviders)

Here, you instantiate a ContentDropController and then ask it to receive the drop. All the hard work you did in the previous section has paid off in the form of a tidy calling site in your view. You’re going to use ContentDropController again later.

Build and run to see your drag support in action. You need to drag an image into the master list view. When the app has launched:

  • Rotate your simulated device to landscape.
  • Go to the home screen by pressing Command-Shift-H. This simulates the hardware home button.
  • Open Photos.app.
  • Invoke multitasking by pressing Command-Shift-H twice quickly.

iPad screen showing activated multitasking

Drag the Photos icon from the Dock over the PointyBug window. Drop the Photos icon near the right side of the PointyBug window to add a slide-over Photos window to your screen. You can hide a slide-over window by grabbing the handle at the top and dragging it to the right off the screen. To reveal it again swipe in from the edge of the screen.

Slide Over view

If you end up with a Split View instead of a Slide Over window, that’s cool, too. Drag an image from Photos to the bug list view.

Note: In Xcode 12.5, you’ll get a crash like this when you drag to an empty List. It looks like someone forgot to check for an empty list. Oh no!

Crash log

If you’re shipping an app with a List and drag support anytime soon, you might need to mitigate this bug for your customers. One approach could be to make the List presence conditional on the content array not being empty.

Add an if statement just above the List inside the body and wrap the List inside the else block.

if bugController.bugs.isEmpty {
  Rectangle()
    .fill(Color.white)
    .onDrop(of: [.image, .text], isTargeted: nil) { providers in
      let dropController = ContentDropController(
        bugID: nil,
        bugController: bugController)
      return dropController.receiveDrop(
        dropIndex: 0,
        itemProviders: providers)
    }
} else {
//  List { ...
}

If there are no bugs, you’ll show an empty view and add a drop handler to the view. When a drop occurs, you’ll perform the same steps you did earlier to handle the drop, but drop the new bug at index 0. You’ll learn more about handling dragging and dropping in SwiftUI views later in this tutorial.

Adding a Bug

Build and run again. This time, tap Add Bug to add a bug to the database, then drag the screenshot you took in the Getting Started section from Photos to the list. Tap the new bug in the list to show the image:

Showing bug image

Tap the description field to edit the text. Press Return to commit the text change, then press Command-Shift-H to save your changes. If you want to see how the save works, look at AppMain.swift in the Views folder. The bugs are persisted in a file when the app goes to the background, which is invoked by pressing Command-Shift-H.

You’ve got the first part of your list drag support done. Next, you’ll find out how to add re-ordering and delete support to the list.

Adding Reorder and Delete Support

Your list should behave like a good iOS citizen and allow your content to be deleted or reordered. You’ll add this support now with a small change.

Open BugListView.swift again, then locate the closing brace of onInsert(of:perform:). Add these two modifiers to the ForEach loop after the closing brace:

.onDelete { indexSet in
  bugController.deleteBugs(atOffsets: indexSet)
}
.onMove { indexSet, i in
  bugController.moveBugs(fromOffsets: indexSet, toOffset: i)
}

Here, you tell BugController to either delete a bug from the list at the requested indices or move some bugs from one point to another in the list. BugController makes the changes to the list. SwiftUI observes the changes and then updates the on-screen display.

Build and run and add at least two bugs. Tap the Edit button in the list navigation bar:

Bug list in editing mode

Your list now shows reorder handles and delete controls. All the expected list behaviors are now available by adding onDelete and onMove to the List.

Your List looks good for now. In the next section, you’ll add drop support to the detail view.

Adding Drop Support to any View

In the previous section, you saw that you can add the methods onInsert, onDelete and onMove to any View that conforms to DynamicViewContent. To add drag and drop to views that don’t represent some sort of collection, you need to use onDrop. In this section, you’ll add image drop support to the editor.

Adding Image Drop Support

Build and run the app again, then press Add Bug. Tap the new bug in the list to select.

Drag and drop tutorial sample project add new bug screen

There’s a placeholder “Drag Image Here” text. You’ll get this feature working now. The first thing to do is upgrade ContentDropController to help with this task.

In the Project navigator, inside the Controller group, open ContentDropController.swift. Add this extension to the end of the file:

extension ContentDropController: DropDelegate {
  func performDrop(info: DropInfo) -> Bool {
    guard info.hasItemsConforming(to: [.image]) else {
      return false
    }
    return receiveDrop(
      dropIndex: 0,
      itemProviders: info.itemProviders(for: [.image]),
      create: false)
  }
}

In this extension, you conform ContentDropController to DropDelegate, which SwiftUI declares. If DropInfo contains an image, you call receiveDrop(dropIndex:itemProviders:create:) with create as false to update an existing Bug with the dropped image.

Next, in the Project navigator, in the Views group, open EditorView.swift. In the body of EditorView, locate PhotoView, then add this modifier on PhotoView:

.onDrop(of: [.image], delegate: ContentDropController(
  bugID: bug.bugID,
  bugController: bugController))

onDrop(of:delegate:) is available for any View. This code tells PhotoView to accept drops of the type UTType.image and to use ContentDropController as the DropDelegate. When you drop an image, SwiftUI calls the DropDelegate protocol method performDrop(info:).

Build and run, then tap Add Bug. Tap the new bug in the list to select, then drag an image from Photos to EditorView:

New image of two cats on a rug dropped

Add a description for your chosen image, and you now have another bug logged. Press Command-Shift-H to save.

You now have a list of several bugs with images. Wouldn’t it be nice if you could point at what the problem is in your image? In the next section, you’ll add the ability to drag a marker to your image.

Dragging Within the App

In this section, you’ll add an icon to EditorView that you can drag onto the image to mark points of interest. First, open EditorView.swift, then add this view to ZStack after the onDrop(of:delegate:) modifier you just added:

VStack {
  Spacer()
  ToolView()
    .padding()
}

Here, you place ToolView over the top of the image. The Spacer pushes ToolView to the bottom of VStack. ToolView is an arrow image with rounded rectangle borders. It’s a simple, pre-baked view to keep you moving in the tutorial.

Build and run. Select a bug with an image, and now you have an arrow icon at the bottom edge of the image in the detail view:

Image window with arrow icon

You’re going to be able to drag that icon onto the image soon. But first, you need to do some prep work.

Creating Your Own UTType

Drop actions need UTType specifications to work and so do drag actions. To support a custom drop action, you’ll need a custom UTType.

Start by creating a new Swift file called Tool.swift inside the Model group. Add this code to the file:

import UniformTypeIdentifiers

protocol Tool {
  static var uti: UTType { get }
  static var name: String { get }
  static var itemProvider: NSItemProvider { get }
}

You’ve described a general tool protocol to allow a Tool to provide three types useful for drags. To drag something, it needs a Universal Type Identifier, a unique name as well as an item provider which will be used to handle the drop.

Next, add this code to the file:

enum ArrowTool: Tool {
  static var name: String = "arrow"
  static var uti = UTType("com.raywenderlich.pointybug.tool.arrow") ?? .data
  static var itemProvider = NSItemProvider(
    item: ArrowTool.name as NSString,
    typeIdentifier: ArrowTool.uti.identifier)
}

ArrowTool adopts Tool. The type identifier of com.raywenderlich.pointybug.tool.arrow is a reverse-coded name unique to the app. You want to be sure no one else uses that identifier.

Exporting Your Own UTType

You need to tell the other apps about this UTI type. Select the PointyBug project in the Project navigator to open the project settings and then select the PointyBug target. In the Info tab, expand Exported Type Identifiers and click + to add a new UTI.

Enter Toolbox item arrow for the description, and com.raywenderlich.pointybug.tool.arrow for the Identifier — the same value you used in your code. Under Conforms To enter public.data. Similarly to Swift protocols, UTIs can conform to other UTIs. Once you’re done, your new UTI should look like the following image:

Adding a new exported universal type identifier in Xcode

You’ve added an Exported UTI to your application’s Info.plist file and told iOS the UTI exists. Without this declaration, you can’t instantiate the UTType.

You’re now ready to set up a drag operation. In the Project navigator in the folder Views, open ArrowToolView.swift.

Add this modifier to the ZStack in the body of ArrowToolView:

.onDrag { ArrowTool.itemProvider }

Like onDrop(of:delegate:), you can call onDrag(_:) on any View. You return the NSItemProvider for the ArrowTool that you created before.

Build and run. Select any bug with an image, then hold and drag the arrow icon.

A draggable SwiftUI view

The icon will lift and gain a plus symbol. Nothing happens when you drop the arrow. The drop part is what you’ll do next.

Receiving a Custom UTType

When you drag the arrow, you place information on the dragging pasteboard. You need to register for that type of information and ask to receive it. The first thing to do is create another controller to perform all the decoding logic and update the model with that information.

Create a new Swift file called ToolDropController.swift in the Controller group. Add this class declaration to the top of the file:

import SwiftUI

class ToolDropController {
  var bugController: BugController
  var bugID: BugID
  var geometry: GeometryProxy
  var imageSize: CGSize

  init(
    bugID: BugID,
    bugController: BugController,
    geometry: GeometryProxy, 
    imageSize: CGSize
  ) {
    self.bugID = bugID
    self.bugController = bugController
    self.geometry = geometry
    self.imageSize = imageSize
  }
}

In this declaration, you create ToolDropController with everything it needs to know to update the model. Next, add this extension to ToolDropController.swift:

extension ToolDropController {
  static func adjustedImageRect(
    geometry: GeometryProxy,
    imageSize: CGSize
  ) -> CGRect {
    let frame = geometry.frame(in: .global)
    let xScale = frame.width / imageSize.width
    let yScale = frame.height / imageSize.height
    let minScale = min(xScale, yScale)
    let finalImageSize = imageSize
      .applying(CGAffineTransform(scaleX: minScale, y: minScale))
    let xOrigin = (frame.width - finalImageSize.width) / 2.0
    let yOrigin = (frame.height - finalImageSize.height) / 2.0
    let origin = CGPoint(x: xOrigin, y: yOrigin)
    let imageRect = CGRect(origin: origin, size: finalImageSize)
    return imageRect
  }
}

This utility method takes two pieces of information: the size of the image and the size of the rectangle it’s placed inside. Next, you figure out how the image will fit in the box by using the least scale value for either axis. Finally, you use that information to return the rectangle that displays the image in its container:

Image of seashore shown in landscape and portrait layouts

Adding Display Element to Image

Finally, add this extension to the end of ToolDropController.swift:

extension ToolDropController: DropDelegate {
  func performDrop(info: DropInfo) -> Bool {
    // 1
    guard
      let selected = bugController.bug(withID: bugID),
      info.hasItemsConforming(to: [ArrowTool.uti])
    else {
      return false
    }

    // 2
    let location = info.location
    let frame = geometry.frame(in: .global)
    let frameAdjustedOrigin = CGPoint(
      x: location.x,
      y: location.y + frame.minY / 2 - 20)

    // 3
    let imageRect = ToolDropController
      .adjustedImageRect(geometry: geometry, imageSize: imageSize)
    let normalizedX = ((frameAdjustedOrigin.x - imageRect.minX)
      - imageRect.width / 2.0) / imageRect.width
    let normalizedY = ((frameAdjustedOrigin.y - imageRect.minY)
      - imageRect.height / 2.0) / imageRect.height

    // 4
    let element = DisplayElement(
      parentID: bugID,
      elementID: selected.nextElementID(),
      originx: normalizedX,
      originy: normalizedY,
      elementType: .arrow)
    bugController.add(element, to: selected)
    return true
  }
}

In this code, you conform ToolDropController to SwiftUI’s DropDelegate:

  1. First, you work out if the drop is describing ArrowTool.
  2. Then, you calculate where the drop happens relative to the container of the image.
  3. Finally, you calculate where the drop happens relative to the image itself. You then normalize that coordinate to between -0.5 and +0.5.
  4. Image of seashore with normalized coordinate

  5. Once you have all that info, you can create a DisplayElement and add that element to the selected bug.

Now that you have your control logic constructed, you can use it in your view.

Receiving a Drop

In this section, you’ll receive the drop of the arrow then render that arrow on top of the image.

In the Project navigator, in the Views folder, open PhotoView.swift. Replace the innermostZStack with this code:

// 1
GeometryReader { geometry in
  ZStack {
    Image(uiImage: uiimage)
      .resizable()
      .aspectRatio(contentMode: .fit)
      .clipped()
      // 2
      .onDrop(
        of: [ArrowTool.uti],
        delegate: ToolDropController(
          bugID: bug.bugID,
          bugController: bugController,
          geometry: geometry,
          imageSize: uiimage.size))
        // insert ElementView here
  }
}

The changes from the starter version of this view are as follows. You:

  1. Wrap the innermost ZStack in a GeometryReader. GeometryReader provides information about the frame of the view that contains the reader.
  2. Call onDrop(of:delegate:) on Image, passing an array of UTType and a DropDelegate to this method. ToolDropController conforms to DropDelegate, so you use an instance of that class here.

Last, add this view under // insert ElementView here:

ElementView(
  bugID: bug.bugID,
  imageSize: ToolDropController
    .adjustedImageRect(
      geometry: geometry,
      imageSize: uiimage.size).size)

You place ElementView above Image in the inner ZStack. ElementView draws the arrows on top of the image. The view needs to know how large the on-screen image is, so you use ToolDropController to generate that size.

You’re ready to drag, so build and run. Select the bug in the master list that shows the missing image picker. Drag the arrow icon to point at the image button in the navigation bar:

Drop display element on bug image

Here, you observe that when you drop the arrow on the image, ToolDropController updates the model by attaching a new DisplayElement to the selected Bug. SwiftUI observes the changes in the model and redraws the view.

You now have a way of marking a bug in a screenshot. This isn’t going to do much good unless you can share that information. You’ll do this in the next section.

Dragging Content Outside the App

To drag a bug out of the app, you need a way of turning your bug model into a universally readable form. In this section, you’ll turn your bug into an image, then drag it out of the app into Photos.

Note: You can’t use drag and drop between apps on iPhone. iPad is the only device that supports this interaction.

You’ll use UIKit to draw the bug description into a UIImage. That image will be the data that’s placed on the pasteboard.

Rendering Content with UIGraphicsImageRenderer

In the Project navigator, in the folder Controller, open BugRenderer.swift. BugRenderer is a utility class that takes a Bug and draws that model into a UIImage.

The method you’ll use to do the work is render(). render() uses UIGraphicsImageRenderer to create the context to draw into. UIGraphicsImageRenderer and UIGraphicsPDFRenderer are quite useful classes you can use for generating images at runtime. You can learn more about these classes from the Apple Documentation.

The image for the Bug is assembled as follows:

  1. First, draw the image.
  2. Next, on top of that, draw any DisplayElements.
  3. Finally, draw the text in the image.

Now that you know about BugRenderer, you can do the final setup task to allow you to drag your information outside the app.

Creating an NSItemProvider

Remember that you used an item provider to ask iOS to load the dropped content? This item provider comes from either your app or other apps. As a developer, you can define a content provider for draggable views.

In the Project navigator, inside the group Model open Bug.swift. Add this extension to the end of the file:

extension Bug {
  func itemProvider() -> NSItemProvider {
    if let image = BugRenderer(bug: self).render() {
      return NSItemProvider(object: image)
    } else {
      return NSItemProvider(object: text as NSString)
    }
  }
}

This code uses BugRenderer to create a UIImage. Because UIImage conforms to NSItemProviderWriting, a protocol that allows an item provider to retrieve data from it, you can return an NSItemProvider using that image.

Dragging That Thing

Open BugListView.swift. In the body of BugListView, call this method on NavigationLink inside ForEach:

.onDrag {
  bug.itemProvider()
}

That’s all you need to do to set up the drag. You return the NSItemProvider you set up before.

Build and run. Bring the Photos slide-over window back in. In Photos, select Albums and then the Recents album. You can now drag a bug with an image from the list of bugs to Photos. You might need to drag over the top of the other photos to get the drag to work:

Dragging the cell to photos

The image will appear in your Photos collection when you drop it.

Image in Photos collection

You’ve added the ability to drag and drop all over the app and even to other apps like Files or Photos. Your app is now a good iPad citizen.

Where to Go From Here?

You can download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

In this tutorial, you learned concepts including:

  • SwiftUI collections have extra drag and drop capabilities you can enable with the API you covered.
  • Any View can support drag and drop.
  • How to discover and decode different types of data from the dragging pasteboard.
  • Creating and exporting UTI values.
  • Using separate controller modules to keep logic outside of your views and increase code reuse and testability.

Adding drag and drop to your app can make it stand out and add joy for your users. Think about how you could add these features to an app. To learn more about implementing drag and drop on iOS, check out Drag and Drop Tutorial for iOS.

Hopefully, this tutorial wasn’t a drag for you. I look forward to seeing what you can do with this API in your apps. You can post any questions or comments in the forum linked below.

Average Rating

5/5

Add a rating for this content

4 ratings

More like this

Contributors

Comments