Home » Guías » Drag & Drop (I): Introducción y Drag
Drag & Drop

Drag & Drop (I): Introducción y Drag

Aun estamos de Worldwide Developer Conference, y desde luego todo lo que estamos viendo es sumamente interesante. Hemos estado haciendo acopiado y recopilación de datos y, a partir de hoy, os daremos todos los días alguna guía o referencia sobre todo lo presentado a nivel desarrollo. Y no podíamos hacer menos que empezar por una de las estrellas de la fiesta: el Drag and Drop (arrastrar y soltar). Una implementación sencilla (o tan compleja como queramos), pero que hemos de hacer si queremos que nuestra app soporte esta función.

Vamos a ver en esta primera guía una introducción sobre cómo funciona esta nueva funcionalidad y un ejemplo de cómo implementar el drag en una vista de colección (que también funciona igual en una tabla) para arrastrar cualquier elemento a otra app que sea compatible.

Cómo funciona

La función de arrastrar y soltar se basa en un objeto del sistema esencial para su funcionamiento: NSItemProvider. Este objeto será el encargado de guardar y transportar entre aplicaciones el contenido que queramos. Será aquel que lleve fuera (o dentro) de la app un contenido y es el que recibiremos cuando alguien arrastre algo (haga drop) a nosotros. Al ser un elemento del sistema, asociado a un hilo, si hacemos múltiples drags entre apps, cada una de las selecciones será un hilo independiente y un proceso aislado, por lo que podremos hacer “drop” en múltiples apps de una misma sesión. El sistema no distingue si tiene uno o varios drags activos.

Este NSItemProvider funciona por registros de datos y, normalmente, representa la información en formato Data (básicamente, datos en bruto o raw). Por lo tanto la forma más simple de crear uno es crear un objeto vacío del mismo y luego invocar su método registerDataRepresentation donde le diremos los datos, la visibilidad de los mismos y un pequeño closure que se ejecutará cuando nuestro elemento se suelte en la otra app para que esta sepa cuales son los datos que va a recibir. Este closure recibe como parámetro otro, que es aquel que representa el proceso de la app que recibe el dato, y por lo tanto, así se consigue que quien reciba, lea los datos de quien envía, todo en un proceso asíncrono mediante conexión de closures.

Configurar la función en nuestra app supone conformarse a los protocolos que hacen que un elemento concreto de la misma soporte estas funciones. A efectos de arrastrar (el origen del dato) el permiso o conformado con el protocolo es a nivel de elemento, ya que cada vista independiente es aquella que será arrastrada. No obstante, lo más eficaz es utilizar la interacción registrada en nuestra vista, para localizar el punto donde se ha tocado, y a partir de ahí crear el elemento que vamos a arrastrar. El problema que tenemos con esto es que, para hacerlo bien, tendremos que conformarnos al protocolo UIDragInteractionDelegate y usar las funciones que nos permiten personalizar las animaciones que se harán al poner en arrastrar un elemento, la representación gráfica que tendrá aquello que se arrastra, etc.

Cuando tenemos una vista que no sea una tabla o una vista de colección, que tienen delegados para ello y ya están registradas por defecto, para soportar ambas funciones de drag y drop (si nos conformamos a estos protocolos), tenemos que fijar una configuración de UIPasteConfiguration y luego registrar la interacción a la vista.

Bastará registrar el controlador con el protocolo UIDragInteractionDelegate y usar cualquiera de las clases que nos permiten, por ejemplo, iniciar la interacción y crear el NSItemProvider con el contenido que se arrastrará, configurar la animación que hará, como será la vista previa que se arrastre, etc. En el caso del drop tenemos que conformarnos a UIDropInteractionDelegate e igualmente actuar a tal consecuencia con aquello que se nos envíe.

Como nuestra app estaría registrada para recibir imágenes, solo ese contenido activaría todas las delegaciones. Si alguien arrastra algo a nuestra app (a la vista que está registrada para recibir los drop) pero no es algo que aceptemos (como un texto, por ejemplo) el elemento arrastrado mostrará un pequeño icono de prohibición en la parte superior derecha. Si es un contenido que sí podemos recibir (como una imagen) al ser de una app a otra, se mostrará un pequeño círculo con un símbolo + que indica que se hará una copia del dato en la nueva app.

Cuando hacemos un drop no obstante, tenemos que detectar si este viene de fuera o por el contrario se recibe un dato desde dentro de la propia app. Incluso, si tenemos diferentes vistas, tablas o colecciones, tenemos que buscar si la procedencia es la misma vista origen de la que procede el dato o es de otra diferente. Para ello usamos recogemos la sesión de drop con el delegado correspondiente y dependiendo de dónde venga (mirando su propiedad localDragSession) tendremos que actuar haciendo un UIDropProposal cuya operación puede ser .move (si viene de la misma app) o .copy (si viene desde fuera). En fin, aquí es cuando tenemos que tener más cuidado en cómo configuramos pero lo iremos viendo poco a poco. Este UIDropProposal definirá el pequeño icono que marcará la operación que puede realizar al usuario (la prohibición, el +, etc.)

Como hemos visto, somos nosotros los que determinamos qué elementos son los susceptibles de ser arrastrados y en caso de soportar que alguien suelte algo en nuestra app (en una vista contenedora de la misma), también debemos autorizar qué tipo de contenido es el que podemos recibir. Podría ser que nuestra vista fuera de imágenes, y por lo tanto solo aceptaríamos imágenes o tal vez solo texto… depende del contenido. Cuando activamos arrastrar un elemento, nosotros le decimos al sistema qué es lo que se arrastra, porque podemos tener una celda de una colección con más de un dato, pero no todos ellos pueden arrastrarse a la vez (como una imagen y un texto a la vez). Hemos de determinar y configurar qué parte de esa celda será la que se active y tenga el dato que será arrastrado.

Y también, al igual que haríamos si hacemos un move en una tabla o una colección para poner reordenar elementos, el sistema solo da la funcionalidad a nivel de interfaz. Somos nosotros los responsables de manejar ese dato. Por lo tanto, si nuestra app acepta recibir imágenes en una vista, tabla o vista de colección, tenemos que recepcionar el dato convenientemente y ser nosotros quien lo inserte en nuestro contexto de datos para que las animaciones y funcionalidad sean coherentes tanto en lo que ve el usuario como lo que hace la app.

Otra cosa que podemos configurar, por seguridad, es en qué contextos queremos que nuestros datos se arrastren o de quién queremos recibir datos. Si tenemos unas apps que forman un grupo como tal, podemos hacer que arrastrar y soltar solo funcione entre nuestras apps y que los datos no sean visibles por nada fuera de nuestro grupo de apps. O por ejemplo, apps in-house de empresas, que pueden querer el drag and drop entre sus propias apps de equipo, pero que por seguridad no se puedan sacar datos fuera de estas a otro contexto. Esto lo configuramos a la hora de qué podemos recibir y qué tipo de de objeto vamos a enviar.

Poco a poco iremos viendo cómo ir implementando esta funcionalidad, pero ahora vamos a ver un ejemplo con una colección, donde parte del trabajo duro nos lo da ya hecho el sistema.

Arrastrando desde una vista de colección

Vamos a suponer que tenemos una vista de colección con una celda personalizada que tiene una imagen y un texto, de forma que cuando ejecutamos nuestra app tenemos esto.

App Pokémon

Queremos poder arrastrar a nuestros Pokémons a otra app que soporte arrastrar imágenes, como por ejemplo, la propia app de Fotos o las notas. Lo primero que hemos de hacer es conformar nuestro controlador de la colección al protocolo UICollectionViewDragDelegate y en el viewDidLoad() registrar el delegado a nosotros mismos.

Hecho esto el sistema nos obliga a poner una función delegada para conformarnos, así que dejamos a Xcode 9 que la cree vacía para que deje de dar errores. Se trata de la función itemForBeginning que nos envía la sesión de drag (que en principio no vamos a usar) y el IndexPath donde se ha pulsado para hacer el drag. Lo que tenemos que devolver es un array de objetos UIDragItem cuyo contenido es el NSItemProvider que llevará el dato a arrastrar. En principio vamos a hacer que sea solo un elemento a la vez, para ir poco a poco.

Así que vamos a crear una función que, dado un IndexPath, nos devuelva lo que queremos devolver que es una imagen en datos raw en representación PNG. Para hacer esto más fácil, vamos a usar unas enumeraciones del sistema que identifica los diferentes posibles tipos de datos a nivel de servicios móviles del sistema. Añadimos en nuestra clase por lo tanto un import MobileCoreServices.

Hecho esto creamos nuestra función.

La función es simple: recogemos la celda que corresponde al IndexPath que nos pasan, que transformamos en la clase personalizada que tiene asignada CollectionViewCell donde hemos registrado los outlets para la imagen y la etiqueta de cada Pokémon. Luego recuperamos la imagen del Pokémon y tras esto creamos el objeto NSItemProvider.

Invocamos el método registerDataRepresentation y le decimos que el registro es para el identificador ( forTypeIdentifier) que corresponde a la enumeración en cadenas CFString que hemos importado con CoreMobileServices y que corresponde al tipo de imagen PNG: kUTTypePNG, que además hemos de convertir de forma segura en String quedando kUTTypePNG as String. Le decimos que queremos que la visibilidad del elemento sea en todos los ámbitos con .all. Aquí podríamos elegir que el drag solo se vea en apps del mismo grupo de apps, del equipo o del mismo proceso en sí. Si ponemos .all hará que se vea en cualquier contexto y se pueda arrastrar a cualquier app. No es la visibilidad del objeto a la que se refiere, si no qué procesos o apps tienen permiso para recibir este objeto que acabamos de crear.

Luego pasamos el closure que se ejecutará cuando la app destino reciba nuestro objeto. En dicho caso este closure recibe un único parámetro que es a su vez un closure (o función) que recibe dos parámetros: el dato de tipo Data? y un tipo Error? que podemos enviar como nil si queremos. Lo que estamos haciendo aquí es que cuando la persona suelte el elemento en la otra app, nuestro closure ejecutará el que la app destino le ha pasado como parámetro, lo que permitirá que esta recoja la imagen que habíamos seleccionado y preparado en la constante imagen, enviándola como parámetro. Creamos la representación PNG en tipo Data y llamamos al closure enviado a nuestro closure pasándole dichos datos y un nil para indicar que no ha pasado nada.

Mientras el closure recibido no devuelve parámetro alguno, el nuestro como parámetro devuelve un parámetro Progress? que devolvemos en nil. Este sirve **para poder enviar una progresión a la otra app que le indique que el dato aún no está disponible porque requiere un tiempo de proceso más largo para estar disponible para ella (por ejemplo, si el elemento ha de descargarse de una conexión de red o procesarse de alguna forma). En dicho caso, la app destino vería un contador con el progreso de carga del contenido que has soltado.

Ahora solo queda crear el objeto UIDragItem con el itemProvider que acabamos de crear y devolverlo él solo dentro de un array. Hacemos que devuelva el resultado de esta función como resultado de la función delegada que hemos implementado obligatoriamente y nada más.

Hecho esto, cuando ejecutemos veremos que al pulsar de forma prolongada en la celda, se nos activa arrastrar toda la celda (incluida la etiqueta) y al arrastrar a notas o a la app de Fotos (hay que tener presente que no soportará apps que no tengan integrado drag and drop) veremos que la imagen aparece en la otra app.

Fin del primer round

¿Como funciona en una sesión práctica? Aquí tenéis el vídeo de cómo se hace y cómo funciona, en nuestro primer screencast.

Espero que os haya sido útil y os prometemos que el próximo tendrá una mejor calidad de sonido. Podéis bajar o clonar el proyecto completo desde GitHub para ver cómo funciona. Un saludo y Good Apple Coding.

DragDropTest | Proyecto en GitHub

Acerca de Julio César Fernández

Analista, consultor y periodista tecnológico, desarrollador, empresario, productor audiovisual, actor de doblaje e ingeniero de vídeo y audio.

Otras recomendaciones

ARKit

Analizando ARKit

Realizamos un análisis de ARKit, sus posibilidades, cómo funciona y lo comparamos con las Microsoft Hololens para ver el alcance de sus posibilidades. Un análisis en detalle incluyendo algunas pruebas en vídeo que hemos realizado mientras preparamos el curso de ARKit en Apple Coding Academy que verá la luz en septiembre, una vez tengamos versión final de esta interesante plataforma.