Back to Blog Home
← all posts

How to enable drag and drop with iOS...from even outside the app!

August 21, 2023 — by Technical Steering Committee (TSC)

Enabling drag and drop on iOS can be powerful in connecting desirable user experience flows. Let's look at how straight forward it can be to enable this behavior in your NativeScript app.

The basis of the details provided here were all taken directly from the official iOS platform documentation, Apple Developer Docs.

You can even try it right now in your hands on StackBlitz: 👉 https://stackblitz.com/edit/nativescript-drag-n-drop-ios

Setup a drop zone utility

Let's setup a utility that will take any View and wire up the drop interaction for us.

import { View } from '@nativescript/core'

export function createDropZone(
  view: View,
  callback: (image: UIImage) => void
) {
  // 1. Create a delegate with our callback reference
  const dropDelegate = DropDelegate.new() as DropDelegate
  dropDelegate.callback = callback

  // 2. Add desired interaction configured with the delegate to our View
  const drop = UIDropInteraction.alloc().initWithDelegate(dropDelegate)
  (view.ios as UIView).addInteraction(drop)
}

The first argument will allow us to pass in any NativeScript View instance. The second argument will allow us to pass in a callback with the image that was dropped on our View.

Following Apple Developer Docs, we want to setup a UIDropInteractionDelegate with the methods outlined and create a UIDropInteraction bound to that delegate.

Create a delegate with our callback

We can setup such a delegate directly in TypeScript as follows:

@NativeClass()
export class DropDelegate
  extends NSObject
  implements UIDropInteractionDelegate
{
  // This tells iOS our object conforms to UIDropInteractionDelegate
  static ObjCProtocols = [UIDropInteractionDelegate]

  // A reference to our callback to communicate when the drop occurred
  callback: (image: UIImage) => void

  // iOS delegate implementation details
  dropInteractionCanHandleSession(
    interaction: UIDropInteraction,
    session: UIDropSession
  ): boolean {
    return session.canLoadObjectsOfClass(UIImage.class())
  }

  dropInteractionSessionDidUpdate(
    interaction: UIDropInteraction,
    session: UIDropSession
  ): UIDropProposal {
    return UIDropProposal.alloc().initWithDropOperation(UIDropOperation.Copy)
  }

  dropInteractionPerformDrop(
    interaction: UIDropInteraction,
    session: UIDropSession
  ): void {
    session.loadObjectsOfClassCompletion(UIImage.class(), (imageItems) => {
      const image = imageItems.objectAtIndex(0)
      this.callback(image as UIImage)
    });
  }
}

Let's break down what's happening in this implementation:

  1. We decorate the class with @NativeClass() since we are extending a platform native class, NSObject.
  2. We use implements UIDropInteractionDelegate to provide rich TypeScript intellisense on all methods which are available from this interface to use.
  3. We declare static ObjCProtocols = [UIDropInteractionDelegate] to tell iOS our object conforms to UIDropInteractionDelegate.
  4. We declare callback: (image: UIImage) => void mainly for nice TypeScript usage to reference our callback in communicating when the drop occurred.
  5. We implement the methods as described by the official platform Apple Developer Docs.

What our utility does

With those details setup, our JavaScript utility function can now create delegates for any View desired to add a Drop interaction to.

// 1. Create a delegate with our callback reference
const dropDelegate = DropDelegate.new() as DropDelegate
dropDelegate.callback = callback

// 2. Add desired interaction configured with the delegate to our View
const drop = UIDropInteraction.alloc().initWithDelegate(dropDelegate)
(view.ios as UIView).addInteraction(drop)

Wire up a View FTW 🎉

We can take any View and use it's loaded event to setup the wiring.

<GridLayout (loaded)="loadedDropZone($event)">
    <Image [src]="droppedImage" />
</GridLayout>

When the loaded event fires it passes along the View reference to our utility:

function loadedDropZone(event) {
    createDropZone(event.object, image => {
        this.droppedImage = image
    })
}

Using any flavor of choice with it's own binding syntax, this will work anywhere under any conditions.

Try for yourself on StackBlitz

The amazing thing is that it all works on StackBlitz right now! 👉 https://stackblitz.com/edit/nativescript-drag-n-drop-ios