Construyendo interfaces fluidas

Cómo crear gestos y animaciones naturales en iOS

En WWDC 2018, los diseñadores de Apple presentaron una charla titulada “Diseño de interfaces fluidas”, explicando el razonamiento de diseño detrás de la interfaz gestual del iPhone X.

Presentación WWDC18 de Apple

Es mi charla favorita de WWDC, la recomiendo.

La charla proporcionó alguna orientación técnica, que es excepcional para una presentación de diseño, pero fue un pseudocódigo, que dejó muchas incógnitas.

Algún código similar a Swift de la presentación.

Si intenta implementar estas ideas, puede notar una brecha entre la inspiración y la implementación.

Mi objetivo es cerrar esta brecha proporcionando ejemplos de código de trabajo de cada tema principal en la presentación.

Las ocho (8) interfaces que crearemos. ¡Botones, resortes, interacciones personalizadas y más!

Aquí hay un resumen de lo que cubriremos:

  1. Un breve resumen de la charla "Diseño de interfaces de fluidos".
  2. Ocho interfaces fluidas, la teoría del diseño detrás de ellas y el código para construirlas.
  3. Aplicaciones para diseñadores y desarrolladores.

¿Qué son las interfaces fluidas?

Una interfaz fluida también podría llamarse "rápida", "suave", "natural" o "mágica". Es una experiencia sin fricciones que simplemente se siente "bien".

La presentación de WWDC habla de interfaces fluidas como "una extensión de su mente" y "una extensión del mundo natural". Una interfaz es fluida cuando se comporta de acuerdo con la forma en que las personas piensan, no con las máquinas.

¿Qué los hace fluidos?

Las interfaces fluidas son receptivas, interrumpibles y redirigibles. Aquí hay un ejemplo del gesto de deslizar para ir a casa en iPhone X:

Las aplicaciones se pueden cerrar durante su animación de lanzamiento.

La interfaz reacciona de inmediato a la entrada del usuario, se puede detener en cualquier punto del proceso e incluso puede cambiar el rumbo a mitad de camino.

¿Por qué nos interesan las interfaces fluidas?

  1. Las interfaces fluidas mejoran la experiencia del usuario, haciendo que cada interacción se sienta rápida, ligera y significativa.
  2. Le dan al usuario una sensación de control, lo que genera confianza con su aplicación y su marca.
  3. Son difíciles de construir. Una interfaz fluida es difícil de copiar y puede ser una ventaja competitiva.

Las interfaces

Para el resto de esta publicación, le mostraré cómo construir ocho (8) interfaces que cubren todos los temas principales de la presentación.

Iconos que representan las ocho (8) interfaces que construiremos.

Interfaz n. ° 1: Botón de la calculadora

Este es un botón que imita el comportamiento de los botones en la aplicación de calculadora iOS.

Características clave

  1. Destaca al instante al tacto.
  2. Se puede tocar rápidamente incluso a mitad de la animación.
  3. El usuario puede tocar hacia abajo y arrastrar fuera del botón para cancelar el toque.
  4. El usuario puede tocar hacia abajo, arrastrar hacia afuera, arrastrar hacia adentro y confirmar el toque.

Teoría del diseño

Queremos botones que se sientan receptivos, reconociendo al usuario que son funcionales. Además, queremos que la acción sea cancelable si el usuario decide en contra de su acción después de tocar tierra. Esto permite a los usuarios tomar decisiones más rápidas ya que pueden realizar acciones en paralelo con el pensamiento.

Diapositivas de la presentación de WWDC que muestran cómo los gestos en paralelo con el pensamiento hacen que las acciones sean más rápidas.

Código crítico

El primer paso para crear este botón es usar una subclase UIControl, no una subclase UIButton. Un UIButton funcionaría bien, pero como estamos personalizando la interacción, no necesitaremos ninguna de sus funciones.

CalculatorButton: UIControl {
    valor var público: Int = 0 {
        didSet {label.text = “\ (value)”}
    }
    etiqueta var privada perezosa: UILabel = {...} ()
}

A continuación, utilizaremos UIControlEvents para asignar funciones a las diversas interacciones táctiles.

addTarget (self, action: #selector (touchDown), para: [.touchDown, .touchDragEnter])
addTarget (self, action: #selector (touchUp), para: [.touchUpInside, .touchDragExit, .touchCancel])

Agrupamos los eventos touchDown y touchDragEnter en un solo "evento" llamado touchDown, y podemos agrupar los eventos touchUpInside, touchDragExit y touchCancel en un solo evento llamado touchUp.

(Para obtener una descripción de todos los eventos UIControlEvents disponibles, consulte la documentación).

Esto nos da dos funciones para manejar las animaciones.

animador var privado = UIViewPropertyAnimator ()
@objc private func touchDown () {
    animator.stopAnimation (verdadero)
    backgroundColor = resaltadoColor
}
@objc private func touchUp () {
    animador = UIViewPropertyAnimator (duración: 0.5, curva: .easeOut, animaciones: {
        self.backgroundColor = self.normalColor
    })
    animator.startAnimation ()
}

En touchDown, cancelamos la animación existente si es necesario, y configuramos instantáneamente el color al color resaltado (en este caso, un gris claro).

En touchUp, creamos un nuevo animador y comenzamos la animación. El uso de un UIViewPropertyAnimator facilita la cancelación de la animación resaltada.

(Nota al margen: este no es el comportamiento exacto de los botones en la aplicación de la calculadora de iOS, que permite que un toque que comenzó en un botón diferente lo active si el toque fue arrastrado dentro del botón. En la mayoría de los casos, un botón como el Creé aquí el comportamiento previsto para los botones de iOS).

Interfaz # 2: Animaciones de primavera

Esta interfaz muestra cómo se puede crear una animación de primavera especificando un "amortiguamiento" (rebote) y una "respuesta" (velocidad).

Características clave

  1. Utiliza parámetros "amigables con el diseño".
  2. No hay concepto de duración de la animación.
  3. Fácilmente interrumpible.

Teoría del diseño

Los resortes son excelentes modelos de animación debido a su velocidad y apariencia natural. Una animación de primavera comienza increíblemente rápido, pasando la mayor parte de su tiempo acercándose gradualmente a su estado final. Esto es perfecto para crear interfaces que se sientan receptivas: ¡cobran vida!

Algunos recordatorios adicionales al diseñar animaciones de primavera:

  1. Los resortes no tienen que ser elásticos. El uso de un valor de amortiguamiento de 1 creará una animación que se detendrá lentamente sin ningún tipo de rebote. La mayoría de las animaciones deberían usar un valor de amortiguamiento de 1.
  2. Intenta evitar pensar en la duración. En teoría, una primavera nunca se detiene por completo, y forzar una duración en la primavera puede hacer que se sienta antinatural. En cambio, juegue con los valores de amortiguación y respuesta hasta que se sienta bien.
  3. La interrupción es crítica. Debido a que los resortes pasan gran parte de su tiempo cerca de su valor final, los usuarios pueden pensar que la animación se ha completado y tratarán de interactuar con ella nuevamente.

Código crítico

En UIKit, podemos crear una animación de primavera con un objeto UIViewPropertyAnimator y un objeto UISpringTimingParameters. Desafortunadamente, no hay un inicializador que solo requiera amortiguación y respuesta. Lo más cerca que podemos llegar es el inicializador UISpringTimingParameters que toma una masa, rigidez, amortiguación y velocidad inicial.

UISpringTimingParameters (masa: CGFloat, rigidez: CGFloat, amortiguación: CGFloat, initialVelocity: CGVector)

Nos gustaría crear un inicializador de conveniencia que tome una amortiguación y una respuesta, y la asigne a la masa, rigidez y amortiguación requeridas.

Con un poco de física, podemos derivar las ecuaciones que necesitamos:

Resolviendo la constante del resorte y el coeficiente de amortiguación.

Con este resultado, podemos crear nuestros propios UISpringTimingParameters con exactamente los parámetros que deseamos.

extensión UISpringTimingParameters {
    conveniencia init (amortiguación: CGFloat, respuesta: CGFloat, initialVelocity: CGVector = .zero) {
        dejar rigidez = pow (2 * .pi / respuesta, 2)
        dejar húmedo = 4 * .pi * amortiguamiento / respuesta
        self.init (masa: 1, rigidez: rigidez, amortiguación: humedad, inicialVelocidad: inicialVelocidad)
    }
}

Así es como especificaremos animaciones de primavera para todas las demás interfaces.

La física detrás de las animaciones de primavera

¿Quieres profundizar en las animaciones de primavera? Echa un vistazo a esta increíble publicación de Christian Schnorr: Desmitificando las animaciones de primavera de UIKit.

Después de leer su publicación, las animaciones de primavera finalmente hicieron clic para mí. Un gran agradecimiento a Christian por ayudarme a comprender las matemáticas detrás de estas animaciones y por enseñarme cómo resolver ecuaciones diferenciales de segundo orden.

Interfaz # 3: Botón de linterna

Otro botón, pero con un comportamiento muy diferente. Esto imita el comportamiento del botón de la linterna en la pantalla de bloqueo del iPhone X.

Características clave

  1. Requiere un gesto intencional con toque 3D.
  2. Bounciness insinúa el gesto requerido.
  3. La retroalimentación háptica confirma la activación.

Teoría del diseño

Apple quería crear un botón que fuera fácil y rápidamente accesible, pero que no pudiera activarse accidentalmente. Requerir presión de fuerza para activar la linterna es una gran opción, pero carece de capacidad y retroalimentación.

Para resolver esos problemas, el botón es elástico y crece a medida que el usuario aplica fuerza, insinuando el gesto requerido. Además, hay dos vibraciones separadas de retroalimentación háptica: una cuando se aplica la cantidad de fuerza requerida y otra cuando el botón se activa a medida que se reduce la fuerza. Estos hápticos imitan el comportamiento de un botón físico.

Código crítico

Para medir la cantidad de fuerza que se aplica al botón, podemos usar el objeto UITouch proporcionado en eventos táctiles.

anular func touchesMoved (_ touch: Set , con evento: UIEvent?) {
    super.touchesMoved (toques, con: evento)
    guardia deja tocar = toques. primero más {return}
    let force = touch.force / touch.maximumPossibleForce
    let scale = 1 + (maxWidth / minWidth - 1) * force
    transform = CGAffineTransform (scaleX: scale, y: scale)
}

Calculamos una transformación de escala basada en la fuerza actual, de modo que el botón crece con una presión creciente.

Dado que el botón se puede presionar pero aún no se ha activado, debemos realizar un seguimiento del estado actual del botón.

enum ForceState {
    reinicio de caso, activado, confirmado
}
privado dejar resetForce: CGFloat = 0.4
Private Let ActivationForce: CGFloat = 0.5
Private Let Confirmación Force: CGFloat = 0.49

El hecho de que la fuerza de confirmación sea ligeramente menor que la fuerza de activación evita que el usuario active y desactive rápidamente el botón al cruzar rápidamente el umbral de fuerza.

Para la retroalimentación háptica, podemos usar los generadores de retroalimentación de UIKit.

private let activaciónFeedbackGenerator = UIImpactFeedbackGenerator (estilo: .light)
privado permite la confirmaciónFeedbackGenerator = UIImpactFeedbackGenerator (estilo: .medium)

Finalmente, para las animaciones hinchables, podemos usar un UIViewPropertyAnimator con los inicializadores personalizados UISpringTimingParameters que creamos antes.

let params = UISpringTimingParameters (amortiguación: 0.4, respuesta: 0.2)
let animator = UIViewPropertyAnimator (duración: 0, timingParameters: params)
animator.addAnimations {
    self.transform = CGAffineTransform (scaleX: 1, y: 1)
    self.backgroundColor = self.isOn? self.onColor: self.offColor
}
animator.startAnimation ()

Interfaz # 4: bandas de goma

Las bandas de goma se producen cuando una vista se resiste al movimiento. Un ejemplo es cuando una vista de desplazamiento llega al final de su contenido.

Características clave

  1. La interfaz siempre responde, incluso cuando una acción no es válida.
  2. El seguimiento táctil desincronizado indica un límite.
  3. La cantidad de movimiento disminuye más lejos del límite.

Teoría del diseño

Rubberbanding es una excelente manera de comunicar acciones no válidas y al mismo tiempo darle al usuario una sensación de control. Indica suavemente un límite, devolviéndolos a un estado válido.

Código crítico

Afortunadamente, la colocación de bandas de goma es fácil de implementar.

offset = pow (offset, 0.7)

Al usar un exponente entre 0 y 1, el desplazamiento de la vista se mueve menos cuanto más se aleja de su posición de reposo. Use un exponente más grande para menos movimiento y un exponente más pequeño para más movimiento.

Para un poco más de contexto, este código generalmente se implementa en una devolución de llamada UIPanGestureRecognizer cada vez que se mueve el toque. El desplazamiento se puede calcular con el delta entre las ubicaciones táctiles actuales y originales, y el desplazamiento se puede aplicar con una transformación de traducción.

var offset = touchPoint.y - originalTouchPoint.y
offset = offset> 0? pow (offset, 0.7): -pow (-offset, 0.7)
view.transform = CGAffineTransform (traducciónX: 0, y: desplazamiento)

Nota: Así no es como Apple realiza bandas de goma con elementos como vistas de desplazamiento. Me gusta este método debido a su simplicidad, pero hay funciones más complejas para diferentes comportamientos.

Interfaz # 5: Pausa de aceleración

Para ver el selector de aplicaciones en el iPhone X, el usuario desliza hacia arriba desde la parte inferior de la pantalla y hace una pausa a mitad de camino. Esta interfaz recrea este comportamiento.

Características clave

  1. La pausa se calcula en función de la aceleración del gesto.
  2. Una detención más rápida da como resultado una respuesta más rápida.
  3. No hay temporizadores.

Teoría del diseño

Las interfaces fluidas deben ser rápidas. Un retraso de un temporizador, incluso si es corto, puede hacer que una interfaz se sienta lenta.

Esta interfaz es particularmente genial porque su tiempo de reacción se basa en el movimiento del usuario. Si se detienen rápidamente, la interfaz responde rápidamente. Si se detienen lentamente, responde lentamente.

Código crítico

Para medir la aceleración, podemos rastrear los valores más recientes de la velocidad del gesto panorámico.

velocidades var privadas = [CGFloat] ()
pista de función privada (velocidad: CGFloat) {
    if velocities.count 

Este código actualiza la matriz de velocidades para tener siempre las últimas siete velocidades, que se utilizan para calcular la aceleración.

Para determinar si la aceleración es lo suficientemente grande, podemos medir la diferencia entre la primera velocidad en nuestra matriz y la velocidad actual.

si abs (velocidad)> 100 || abs (offset) <50 {return}
let ratio = abs (firstRecordedVelocity - velocidad) / abs (firstRecordedVelocity)
si relación> 0.9 {
    pauseLabel.alpha = 1
    feedbackGenerator.impactOccurred ()
    hasPaused = true
}

También verificamos que el movimiento tenga un desplazamiento y una velocidad mínimos. Si el gesto ha perdido más del 90% de su velocidad, consideramos que está en pausa.

Mi implementación no es perfecta. En mis pruebas parece funcionar bastante bien, pero hay una oportunidad para una mejor heurística para medir la aceleración.

Interfaz # 6: Momento gratificante

Un cajón con estados abiertos y cerrados que tiene rebote basado en la velocidad del gesto.

Características clave

  1. Al tocar el cajón, se abre sin rebote.
  2. Mover el cajón lo abre con rebote.
  3. Interactivo, interrumpible y reversible.

Teoría del diseño

Este cajón muestra el concepto de impulso gratificante. Cuando el usuario desliza una vista con velocidad, es mucho más satisfactorio animar la vista con rebote. Esto hace que la interfaz se sienta viva y divertida.

Cuando se toca el cajón, se anima sin rebote, lo que se siente apropiado, ya que un grifo no tiene impulso en una dirección particular.

Al diseñar interacciones personalizadas, es importante recordar que las interfaces pueden tener diferentes animaciones para diferentes interacciones.

Código crítico

Para simplificar la lógica de tapping versus paneo, podemos usar una subclase de reconocimiento de gestos personalizada que ingresa inmediatamente al estado iniciado al tocar tierra.

clase InstantPanGestureRecognizer: UIPanGestureRecognizer {
    anular func touchesBegan (_ touch: Set , con evento: UIEvent) {
        super.touchesBegan (toques, con: evento)
        self.state = .began
    }
}

Esto también permite al usuario tocar el cajón durante su movimiento para pausarlo, de manera similar a tocar en una vista de desplazamiento que se está desplazando actualmente. Para manejar los grifos, podemos verificar si la velocidad es cero cuando finaliza el gesto y continuar la animación.

si yVelocity == 0 {
    animator.continueAnimation (withTimingParameters: nil, DurationFactor: 0)
}

Para manejar un gesto con velocidad, primero necesitamos calcular su velocidad en relación con el desplazamiento restante total.

dejar fracciónRemaining = 1 - animator.fractionComplete
let distanceRemaining = fraccionRemaining * closedTransform.ty
if distanceRemaining == 0 {
    animator.continueAnimation (withTimingParameters: nil, DurationFactor: 0)
    rotura
}
let relativeVelocity = abs (yVelocity) / distanceRemaining

Podemos usar esta velocidad relativa para continuar la animación con los parámetros de tiempo que incluyen un poco de saltos.

let timingParameters = UISpringTimingParameters (amortiguación: 0.8, respuesta: 0.3, initialVelocity: CGVector (dx: relativeVelocity, dy: relativeVelocity))
let newDuration = UIViewPropertyAnimator (duración: 0, timingParameters: timingParameters) .duration
let DurationFactor = CGFloat (newDuration / animator.duration)
animator.continueAnimation (withTimingParameters: timingParameters, DurationFactor: DurationFactor)

Aquí estamos creando un nuevo UIViewPropertyAnimator para calcular el tiempo que debe tomar la animación para que podamos proporcionar la duración correctaFactor al continuar la animación.

Hay más complejidades relacionadas con la inversión de la animación que no voy a cubrir aquí. Si desea obtener más información, escribí un tutorial completo para este componente: Crear mejores animaciones de aplicaciones de iOS.

Interfaz # 7: FaceTime PiP

Una recreación de la interfaz de usuario de imagen en imagen de la aplicación iOS FaceTime.

Características clave

  1. Interacción ligera y aireada.
  2. La posición proyectada se basa en la tasa de desaceleración de UIScrollView.
  3. Animación continua que respeta la velocidad inicial del gesto.

Código crítico

Nuestro objetivo final es escribir algo como esto.

let params = UISpringTimingParameters (amortiguación: 1, respuesta: 0.4, initialVelocity: relativeInitialVelocity)
let animator = UIViewPropertyAnimator (duración: 0, timingParameters: params)
animator.addAnimations {
    self.pipView.center = más cercanoCornerPosition
}
animator.startAnimation ()

Nos gustaría crear una animación con una velocidad inicial que coincida con la velocidad del gesto panorámico y animar el pip a la esquina más cercana.

Primero, calculemos la velocidad inicial.

Para hacer esto, necesitamos calcular una velocidad relativa basada en la velocidad actual, la posición actual y la posición objetivo.

let relativeInitialVelocity = CGVector (
    dx: relativeVelocity (forVelocity: velocity.x, from: pipView.center.x, to :arestCornerPosition.x),
    dy: relativeVelocity (forVelocity: velocity.y, from: pipView.center.y, to :arestCornerPosition.y)
)
func relativeVelocity (forVelocity speed: CGFloat, from currentValue: CGFloat, to targetValue: CGFloat) -> CGFloat {
    guard currentValue - targetValue! = 0 más {return 0}
    velocidad de retorno / (targetValue - currentValue)
}

Podemos dividir la velocidad en sus componentes x e y y determinar la velocidad relativa para cada uno.

A continuación, calculemos la esquina para animar la imagen incrustada.

Para que nuestra interfaz se sienta natural y liviana, proyectaremos la posición final de PiP en función de su movimiento actual. Si el PiP se deslizara y se detuviera, ¿dónde aterrizaría?

let decelerationRate = UIScrollView.DecelerationRate.normal.rawValue
let velocity = Recognizer.velocity (en: vista)
let projectedPosition = CGPoint (
    x: pipView.center.x + project (initialVelocity: velocity.x, decelerationRate: decelerationRate),
    Proyecto y: pipView.center.y + (inicialVelocity: velocity.y, decelerationRate: decelerationRate)
)
dejar más cercanoCornerPosition = más cercanoCorner (a: projectedPosition)

Podemos usar la tasa de desaceleración de un UIScrollView para calcular esta posición de reposo. Esto es importante porque hace referencia a la memoria muscular del usuario para el desplazamiento. Si un usuario sabe qué tan lejos se desplaza una vista, puede usar ese conocimiento previo para adivinar intuitivamente cuánta fuerza se necesita para mover el PiP a su objetivo deseado.

Esta tasa de desaceleración también es bastante generosa, lo que hace que la interacción se sienta liviana: solo se necesita un pequeño movimiento para enviar el PiP volando por toda la pantalla.

Podemos usar la función de proyección provista en la charla “Diseño de interfaces de fluidos” para calcular la posición proyectada final.

/// Distancia recorrida después de desacelerar a velocidad cero a una velocidad constante.
proyecto func (initialVelocity: CGFloat, decelerationRate: CGFloat) -> CGFloat {
    return (initialVelocity / 1000) * decelerationRate / (1 - decelerationRate)
}

La última pieza que falta es la lógica para encontrar la esquina más cercana en función de la posición proyectada. Para hacer esto, podemos recorrer todas las posiciones de las esquinas y encontrar la que tenga la menor distancia a la posición de aterrizaje proyectada.

funcarestCorner (al punto: CGPoint) -> CGPoint {
    var minDistance = CGFloat.greatestFiniteMagnitude
    vararestPosition = CGPoint.zero
    para la posición en pipPositions {
        dejar distancia = punto.distancia (a: posición)
        si la distancia 

Para resumir la implementación final: Usamos la tasa de desaceleración de UIScrollView para proyectar el movimiento de la pepita a su posición de reposo final, y calculamos la velocidad relativa para alimentar todo a los parámetros UISpringTimingParameters.

Interfaz # 8: rotación

Aplicación de los conceptos de la interfaz de imágenes incrustadas a una animación de rotación.

Características clave

  1. Utiliza la proyección para respetar la velocidad del gesto.
  2. Siempre termina en una orientación válida.

Código crítico

El código aquí es muy similar a la interfaz anterior de PiP. Usaremos los mismos bloques de construcción, excepto intercambiar la función de esquina más cercana por una función de ángulo más cercano.

proyecto func (...) {...}
func relativeVelocity (...) {...}
funcarestAngle (...) {...}

Cuando llegue el momento de crear finalmente los parámetros UISpringTimingParameters, debemos utilizar un CGVector para la velocidad inicial, aunque nuestra rotación solo tenga una dimensión. En cualquier caso en el que la propiedad animada tenga solo una dimensión, establezca el valor dx a la velocidad deseada y establezca el valor dy en cero.

let timingParameters = UISpringTimingParameters (
    amortiguación: 0.8,
    respuesta: 0.4,
    initialVelocity: CGVector (dx: relativeInitialVelocity, dy: 0)
)

Internamente, el animador ignorará el valor dy y usará el valor dx para crear la curva de tiempo.

¡Inténtalo tú mismo!

Estas interfaces son mucho más divertidas en un dispositivo real. Para jugar con estas interfaces usted mismo, la aplicación de demostración está disponible en GitHub.

¡La aplicación de demostración de interfaces fluidas, disponible en GitHub!

Aplicaciones prácticas

Para diseñadores

  1. Piense en las interfaces como medios de expresión fluidos, no como colecciones de elementos estáticos.
  2. Considere animaciones y gestos al principio del proceso de diseño. Las herramientas de diseño como Sketch son fantásticas, pero no ofrecen la total expresividad del dispositivo.
  3. Prototipo con desarrolladores. Obtenga desarrolladores con mentalidad de diseño que lo ayuden a crear prototipos de animaciones, gestos y hápticos.

Para desarrolladores

  1. Aplique los consejos de estas interfaces a sus propios componentes personalizados. Piense en cómo podrían combinarse de formas nuevas e interesantes.
  2. Eduque a sus diseñadores sobre nuevas posibilidades. Muchos no son conscientes del poder total del tacto 3D, hápticos, gestos y animaciones de primavera.
  3. Prototipo con diseñadores. Ayúdelos a ver sus diseños en un dispositivo real y cree herramientas para ayudarlos a diseñar de manera más efectiva.

Si te ha gustado esta publicación, deja algunos aplausos.

Puedes aplaudir hasta 50 veces, ¡así que haz clic / toca!

Comparta la publicación con sus amigos del diseñador / desarrollador de iOS en su medio de comunicación social de su elección.

Si te gusta este tipo de cosas, deberías seguirme en Twitter. Solo publico tweets de alta calidad. twitter.com/nathangitter

Gracias a David Okun por revisar los borradores de esta publicación.