Suave dibujo a mano alzada en iOS

Este tutorial le enseñará cómo implementar un algoritmo de dibujo avanzado para dibujar a mano alzada y sin problemas en dispositivos iOS. Sigue leyendo!

Panorama teórico

El tacto es la principal forma en que un usuario interactúa con los dispositivos iOS. Una de las funcionalidades más naturales y obvias que se espera que proporcionen estos dispositivos es permitir al usuario dibujar en la pantalla con el dedo. Hay muchas aplicaciones de dibujo y toma de notas a mano alzada actualmente en la App Store, y muchas compañías incluso les piden a los clientes que firmen un iDevice al realizar compras. ¿Cómo funcionan realmente estas aplicaciones? Paremos y pensemos por un minuto lo que está pasando "bajo el capó".

Cuando un usuario desplaza una vista de tabla, pellizca para ampliar una imagen o dibuja una curva en una aplicación de pintura, la pantalla del dispositivo se está actualizando rápidamente (por ejemplo, 60 veces por segundo) y el ciclo de ejecución de la aplicación está constantemente muestreo la ubicación de los dedos del usuario. Durante este proceso, la entrada "analógica" de un arrastre de dedo a través de la pantalla debe convertirse en un conjunto digital de puntos en la pantalla, y este proceso de conversión puede plantear desafíos importantes. En el contexto de nuestra aplicación de pintura, tenemos un problema de "ajuste de datos" en nuestras manos. A medida que el usuario escribe alegremente en el dispositivo, el programador esencialmente debe interpolar la información analógica faltante ("conectar los puntos") que se ha perdido entre los puntos de contacto muestreados que iOS nos informó. Además, esta interpolación debe ocurrir de tal manera que el resultado sea un trazo que parezca continuo, natural y suave para el usuario final, como si hubiera estado dibujando con un bolígrafo en una libreta hecha de papel.

El propósito de este tutorial es mostrar cómo se puede implementar el dibujo a mano alzada, comenzando desde un algoritmo básico que realiza la interpolación en línea recta y avanzando a un algoritmo más sofisticado que se acerca a la calidad proporcionada por aplicaciones conocidas como Penultimate. Como si no fuera lo suficientemente difícil crear un algoritmo que funcione, también debemos asegurarnos de que el algoritmo funcione bien. Como veremos, una implementación de dibujo ingenua puede llevar a una aplicación con problemas de rendimiento significativos que harán que el dibujo sea engorroso y, finalmente, inutilizable..


Empezando

Supongo que no eres totalmente nuevo en el desarrollo de iOS, así que repasé los pasos de crear un nuevo proyecto, agregar archivos al proyecto, etc. De todos modos, espero que no haya nada demasiado difícil aquí, pero por si acaso, el El código completo del proyecto está disponible para que lo descargues y juegues con él..

Comienza un nuevo proyecto de Xcode iPad basado en el "Solicitud de vista única"Plantilla y nombre"Dibujar a mano alzada". Asegúrese de habilitar el conteo automático de referencias (ARC), pero anule la selección de los guiones gráficos y las pruebas unitarias. Puede hacer que este proyecto sea un iPhone o una aplicación universal, dependiendo de los dispositivos que tenga disponibles para probar..

A continuación, siga adelante y seleccione el proyecto "FreeHandDrawingTut" en Xcode Navigator y asegúrese de que solo se admita la orientación vertical:

Si va a implementar en iOS 5.x o una versión anterior, puede cambiar el soporte de orientación de esta manera:

 - (BOOL) shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation) interfaceOrientation return (interfaceOrientation == UIInterfaceOrientationPortrait); 

Estoy haciendo esto para mantener las cosas simples para poder enfocarnos en el problema principal que nos ocupa.

Quiero desarrollar nuestro código de forma iterativa, mejorándolo de forma incremental, como lo haría de manera realista si empezara desde cero, en lugar de dejarle la versión final de una sola vez. Espero que este enfoque le permita manejar mejor los diferentes problemas involucrados. Teniendo esto en cuenta, y para evitar tener que eliminar, modificar y agregar el código en el mismo archivo repetidamente, lo que puede causar problemas y errores, seguiré el siguiente enfoque:

  • Para cada iteración, crearemos una nueva subclase UIView. Publicaré todo el código necesario para que pueda simplemente copiar y pegar en el archivo .m de la nueva subclase UIView que cree. No habrá una interfaz pública para ver la funcionalidad de la subclase, lo que significa que no necesitará tocar el archivo .h.
  • Para probar cada nueva versión, tendremos que asignar la subclase UIView que creamos para que sea la vista que ocupa actualmente la pantalla. Le mostraré cómo hacerlo con Interface Builder la primera vez, siguiendo los pasos detalladamente, y luego le recordaré este paso cada vez que codifiquemos una nueva versión..

Primer intento de dibujar

En Xcode, elija Archivo> Nuevo> Archivo ... , elija la clase de Objective-C como la plantilla, y en la siguiente pantalla nombre el archivo LinearInterpView y convertirlo en una subclase de Vista. Guardalo El nombre "LinearInterp" es la abreviatura de "interpolación lineal" aquí. Por el bien del tutorial, nombraré cada subclase UIView que creamos para enfatizar algún concepto o enfoque introducido dentro del código de clase.

Como mencioné anteriormente, puede dejar el archivo de encabezado tal como está. Borrar todos El código presente en el archivo LinearInterpView.m, y reemplazarlo con lo siguiente:

 #import "LinearInterpView.h" @implementation LinearInterpView UIBezierPath * ruta; // (3) - (id) initWithCoder: (NSCoder *) aDecoder // (1) if (self = [super initWithCoder: aDecoder]) [self setMultipleTouchEnabled: NO]; // (2) [self setBackgroundColor: [UIColor whiteColor]]; ruta = [UIBezierPath bezierPath]; [ruta setLineWidth: 2.0];  devuélvete a ti mismo;  - (void) drawRect: (CGRect) rect // (5) [[UIColor blackColor] setStroke]; [trazo de trayectoria];  - (void) touchesBegan: (NSSet *) toca withEvent: (UIEvent *) evento UITouch * touch = [toca cualquier Objeto]; CGPoint p = [toque locationInView: self]; [ruta moveToPoint: p];  - (void) touchesMoved: (NSSet *) toca conEvent: (UIEvent *) evento UITouch * touch = [toca cualquier Objeto]; CGPoint p = [toque locationInView: self]; [ruta addLineToPoint: p]; // (4) [self setNeedsDisplay];  - (void) touchesEnded: (NSSet *) toca conEvent: (UIEvent *) evento [self touchesMoved: toca conEvent: evento];  - (void) touchesCancelled: (NSSet *) toca withEvent: (UIEvent *) event [self touchesEnded: toca withEvent: event];  @final

En este código, trabajamos directamente con los eventos táctiles que la aplicación nos informa cada vez que tenemos una secuencia táctil. es decir, el usuario coloca un dedo en la vista en pantalla, lo mueve y finalmente lo levanta de la pantalla. Para cada evento en esta secuencia, la aplicación nos envía un mensaje correspondiente (en la terminología de iOS, los mensajes se envían al "primer respondedor"; puede consultar la documentación para obtener detalles).

Para lidiar con estos mensajes, implementamos los métodos. -toquesBegan: ConEvento: y compañía, que se declaran en la clase UIResponder de la que UIView hereda. Podemos escribir código para manejar los eventos táctiles de la manera que queramos. En nuestra aplicación, queremos consultar la ubicación en pantalla de los toques, hacer un procesamiento y luego dibujar líneas en la pantalla..

Los puntos se refieren a los números comentados correspondientes del código anterior:

  1. Anulamos -initWithCoder: porque la vista nace de un XIB, como lo configuraremos en breve.
  2. Inhabilitamos varios toques: vamos a tratar solo con una secuencia de toques, lo que significa que el usuario solo puede dibujar con un dedo a la vez; Cualquier otro dedo colocado en la pantalla durante ese tiempo será ignorado. ¡Esto es una simplificación, pero no necesariamente irrazonable, ya que las personas tampoco suelen escribir en papel con dos bolígrafos a la vez! En cualquier caso, evitará que nos desviemos demasiado, ya que tenemos suficiente trabajo por hacer.
  3. los UIBezierPath es una clase de UIKit que nos permite dibujar formas en la pantalla compuestas de líneas rectas o ciertos tipos de curvas.
  4. Ya que estamos haciendo dibujos personalizados, necesitamos anular la vista de -drawRect: método. Hacemos esto acariciando la ruta cada vez que se agrega un nuevo segmento de línea.
  5. Tenga en cuenta también que si bien el ancho de línea es una propiedad de la ruta, el color de la línea en sí es una propiedad del contexto de dibujo. Si no está familiarizado con los contextos gráficos, puede leer sobre ellos en los documentos de Apple. Por ahora, piense en un contexto de gráficos como un "lienzo" que dibuja cuando anula el -drawRect: Método, y el resultado de lo que ve es la vista en pantalla. En breve nos encontraremos con otro tipo de contexto de dibujo..

Antes de poder construir la aplicación, debemos configurar la subclase de vista que acabamos de crear a la vista en pantalla.

  1. En el panel del navegador, haga clic en ViewController.xib (En caso de que haya creado una aplicación Universal, simplemente lleve a cabo este paso tanto para ViewController ~ iPhone.xib y ViewController ~ iPad.xib archivos).
  2. Cuando la vista aparezca en el lienzo del generador de interfaces, haga clic en él para seleccionarlo. En el panel de utilidades, haga clic en "Inspector de identidades" (tercer botón de la derecha en la parte superior del panel). La sección superior dice "Clase personalizada", aquí es donde establecerá la clase de vista en la que hizo clic..
  3. En este momento debería decir "UIView", pero debemos cambiarlo a (lo has adivinado) LinearInterpView. Escriba el nombre de la clase (simplemente escribiendo "L" debería hacer que el autocompletar suene de manera tranquilizadora).
  4. Nuevamente, si va a probar esto como una aplicación universal, repita este paso exacto para los dos archivos XIB que la plantilla creó para usted..

Ahora construye la aplicación. Debería obtener una vista en blanco brillante que puede dibujar con el dedo. ¡Teniendo en cuenta las pocas líneas de código que hemos escrito, los resultados no están tan mal! Por supuesto, tampoco son espectaculares. La apariencia de conectar los puntos es bastante notable (y sí, mi escritura también apesta).

Asegúrese de ejecutar la aplicación no solo en el simulador, sino también en un dispositivo real.

Si juegas con la aplicación por un tiempo en tu dispositivo, estás obligado a notar algo: finalmente, la respuesta de la interfaz de usuario comienza a demorarse, y en lugar de los ~ 60 puntos de contacto que se adquirían por segundo, por alguna razón, la cantidad de puntos La interfaz de usuario es capaz de muestrear gotas más y más. Como los puntos se están separando, la interpolación en línea recta hace que el dibujo sea "más bloqueado" que antes. Esto es ciertamente indeseable. Entonces, ¿qué está pasando??


Mantener el rendimiento y la capacidad de respuesta

Revisemos lo que hemos estado haciendo: a medida que dibujamos, adquirimos puntos, los agregamos a una ruta en constante crecimiento y luego representamos la ruta * completa * en cada ciclo del ciclo principal. Entonces, a medida que el camino se hace más largo, en cada iteración, el sistema de dibujo tiene más que dibujar y, finalmente, se vuelve demasiado, lo que dificulta que la aplicación se mantenga al día. Dado que todo está sucediendo en el hilo principal, nuestro código de dibujo está compitiendo con el código de UI que, entre otras cosas, tiene que probar los toques en la pantalla.

Te perdonarían por pensar que había una forma de dibujar "encima de" lo que ya estaba en la pantalla; desafortunadamente, aquí es donde debemos liberarnos de la analogía del lápiz en el papel, ya que el sistema de gráficos no funciona de esa manera por defecto. Aunque, en virtud del código que vamos a escribir a continuación, indirectamente vamos a implementar el enfoque de "dibujar en la parte superior".

Si bien hay algunas cosas que podríamos intentar arreglar el rendimiento de nuestro código, solo vamos a implementar una idea, ya que resulta suficiente para nuestras necesidades actuales..

Crea una nueva subclase UIView como lo hiciste antes, dándole un nombre CachedLIView (El LI es para recordarnos que todavía estamos haciendo Len el oido yonterpolación). Borrar todos los contenidos de CachedLIView.m y reemplazarlo con lo siguiente:

 #import "CachedLIView.h" @implementation CachedLIView UIBezierPath * ruta; UIImage * incrementalImage; // (1) - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) [self setMultipleTouchEnabled: NO]; [self setBackgroundColor: [UIColor whiteColor]]; ruta = [UIBezierPath bezierPath]; [ruta setLineWidth: 2.0];  devuélvete a ti mismo;  - (void) drawRect: (CGRect) rect [incrementalImage drawInRect: rect]; // (3) [trazo de trayectoria];  - (void) touchesBegan: (NSSet *) toca withEvent: (UIEvent *) evento UITouch * touch = [toca cualquier Objeto]; CGPoint p = [toque locationInView: self]; [ruta moveToPoint: p];  - (void) touchesMoved: (NSSet *) toca conEvent: (UIEvent *) evento UITouch * touch = [toca cualquier Objeto]; CGPoint p = [toque locationInView: self]; [ruta addLineToPoint: p]; [auto setNeedsDisplay];  - (void) touchesEnded: (NSSet *) toca conEvent: (UIEvent *) evento // (2) UITouch * touch = [toca cualquier Objeto]; CGPoint p = [toque locationInView: self]; [ruta addLineToPoint: p]; [auto drawBitmap]; // (3) [self setNeedsDisplay]; [ruta removeAllPoints]; // (4) - (void) touchesCancelled: (NSSet *) toca conEvent: (UIEvent *) event [self touchesEnded: toca conEvent: event];  - (void) drawBitmap // (3) UIGraphicsBeginImageContextWithOptions (self.bounds.size, YES, 0.0); [[UIColor blackColor] setStroke]; if (! incrementalImage) // primer sorteo; pinta el fondo blanco por ... UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; // encerrar el mapa de bits con un rectángulo definido por otro objeto UIBezierPath [[UIColor whiteColor] setFill]; [rectpath fill]; // llenándolo con blanco [incrementalImage drawAtPoint: CGPointZero]; [trazo de trayectoria]; incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext ();  @final

Después de guardar, recuerde cambiar la clase del objeto de vista en su XIB (s) a CachedLIView!

Cuando el usuario coloca su dedo en la pantalla para dibujar, comenzamos con un camino nuevo sin puntos o líneas, y le agregamos segmentos de línea tal como lo hicimos antes..

De nuevo, refiriéndose a los números en los comentarios:

  1. Además, mantenemos en la memoria una imagen de mapa de bits (fuera de pantalla) del mismo tamaño que nuestro lienzo (es decir, en la vista de pantalla), en la que podemos almacenar lo que hemos dibujado hasta ahora..
  2. Dibujamos el contenido en pantalla en este búfer cada vez que el usuario levanta su dedo (señalado por -touchesEnded: WithEvent).
  3. El método drawBitmap crea un contexto de mapa de bits: los métodos UIKit necesitan un "contexto actual" (un lienzo) para dibujar. Cuando estamos dentro -drawRect: este contexto se pone automáticamente a nuestra disposición y refleja lo que dibujamos en nuestra vista en pantalla. En contraste, el contexto del mapa de bits debe crearse y destruirse explícitamente, y los contenidos dibujados residen en la memoria.
  4. Al almacenar en caché el dibujo anterior de esta manera, podemos deshacernos de los contenidos anteriores de la ruta, y de esta manera evitar que la ruta se vuelva demasiado larga.
  5. Ahora cada vez drawRect: se llama, primero dibujamos los contenidos del búfer de memoria en nuestra vista que (por diseño) tiene exactamente el mismo tamaño, y para el usuario mantenemos la ilusión de dibujo continuo, solo de una manera diferente que antes.

Si bien esto no es perfecto (¿qué pasaría si nuestro usuario sigue dibujando sin levantar el dedo, alguna vez?), Será lo suficientemente bueno para el alcance de este tutorial. Le recomendamos que experimente por su cuenta para encontrar un método mejor. Por ejemplo, podría intentar guardar el dibujo periódicamente en lugar de cuando el usuario levanta el dedo. A medida que sucede, este procedimiento de almacenamiento en caché fuera de la pantalla nos brinda la oportunidad de procesar en segundo plano, en caso de que decidamos implementarlo. Pero no vamos a hacer eso en este tutorial. Sin embargo, estás invitado a probar por tu cuenta.!


Mejorar la calidad del trazo visual

Ahora vamos a centrar nuestra atención en hacer que el dibujo se vea mejor. Hasta ahora, hemos estado uniendo puntos de contacto adyacentes con segmentos de línea recta. Pero normalmente cuando dibujamos a mano alzada, nuestro golpe natural tiene una apariencia fluida y curvilínea (en lugar de rígida y con bloques). Tiene sentido que intentemos interpolar nuestros puntos con curvas en lugar de segmentos de línea. Afortunadamente, la clase UIBezierPath nos permite dibujar su homónimo: curvas de Bezier.

¿Qué son las curvas de Bezier? Sin invocar la definición matemática, una curva Bezier se define por cuatro puntos: dos puntos finales a través de los cuales pasa una curva y dos "puntos de control" que ayudan a definir las tangentes que la curva debe tocar en sus puntos finales (esto es técnicamente una curva Bezier cúbica, pero por simplicidad me referiré a esto simplemente como una "curva Bezier").

Las curvas de Bézier nos permiten dibujar todo tipo de formas interesantes..

Lo que intentaremos ahora es agrupar secuencias de cuatro puntos de contacto adyacentes e interpolar la secuencia de puntos dentro de un segmento de curva Bezier. Cada par adyacente de segmentos Bezier compartirá un punto final en común para mantener la continuidad del golpe.

Ya conoces el ejercicio. Crea una nueva subclase UIView y nómbrela BezierInterpView. Pegue el siguiente código en el archivo .m:

 #import "BezierInterpView.h" @implementation BezierInterpView UIBezierPath * ruta; UIImage * incrementalImage; Pts CGPoint [4]; // para realizar un seguimiento de los cuatro puntos de nuestro segmento Bezier uint ctr; // una variable de contador para realizar un seguimiento del índice de puntos - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) [self setMultipleTouchEnabled: NO]; [self setBackgroundColor: [UIColor whiteColor]]; ruta = [UIBezierPath bezierPath]; [ruta setLineWidth: 2.0];  devuélvete a ti mismo;  - (void) drawRect: (CGRect) rect [incrementalImage drawInRect: rect]; [trazo de trayectoria];  - (void) touchesBegan: (NSSet *) toca withEvent: (UIEvent *) event ctr = 0; UITouch * touch = [toca cualquier objeto]; pts [0] = [toque locationInView: self];  - (void) touchesMoved: (NSSet *) toca conEvent: (UIEvent *) evento UITouch * touch = [toca cualquier Objeto]; CGPoint p = [toque locationInView: self]; ctr ++; pts [ctr] = p; if (ctr == 3) // 4th point [path moveToPoint: pts [0]]; [ruta addCurveToPoint: pts [3] controlPoint1: pts [1] controlPoint2: pts [2]]; // Así es como se anexa una curva Bezier a una ruta. Estamos agregando un Bezier cúbico de pt [0] a pt [3], con los puntos de control pt [1] y pt [2] [auto setNeedsDisplay]; pts [0] = [ruta currentPoint]; ctr = 0;  - (void) touchesEnded: (NSSet *) toca withEvent: (UIEvent *) event [self drawBitmap]; [auto setNeedsDisplay]; pts [0] = [ruta currentPoint]; // deje que el segundo punto final del segmento Bezier actual sea el primero para el siguiente segmento Bezier [ruta removeAllPoints]; ctr = 0;  - (void) touchesCancelled: (NSSet *) toca withEvent: (UIEvent *) event [self touchesEnded: toca withEvent: event];  - (void) drawBitmap UIGraphicsBeginImageContextWithOptions (self.bounds.size, YES, 0.0); [[UIColor blackColor] setStroke]; if (! incrementalImage) // primera vez; pintar fondo blanco UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; [[UIColor whiteColor] setFill]; [rectpath fill];  [incrementalImage drawAtPoint: CGPointZero]; [trazo de trayectoria]; incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext ();  @final

Como lo indican los comentarios en línea, el cambio principal es la introducción de un par de nuevas variables para realizar un seguimiento de los puntos en nuestros segmentos Bezier, y una modificación de la -(void) touchesmoved: withEvent: método para dibujar un segmento Bezier por cada cuatro puntos (en realidad, cada tres puntos, en términos de los toques que nos informa la aplicación, porque compartimos un punto final por cada par de segmentos Bezier adyacentes).

Podría indicar aquí que hemos descuidado el caso de que el usuario levante su dedo y termine la secuencia táctil antes de que tengamos suficientes puntos para completar nuestro último segmento Bezier. Si es así, tendrías razón! Mientras que visualmente esto no hace mucha diferencia, en ciertos casos importantes lo hace. Por ejemplo, trata de dibujar un círculo pequeño. Puede que no se cierre completamente, y en una aplicación real querría manejar esto de manera apropiada en el -toquesEnded: WithEvent método. Mientras estamos en ello, tampoco hemos prestado ninguna atención especial al caso de la cancelación de toque. los toquesCancelado: ConEvento El método de instancia maneja esto. Eche un vistazo a la documentación oficial y vea si hay casos especiales que deba manejar aquí..

Entonces, ¿cómo se ven los resultados? Una vez más, les recuerdo que establezcan la clase correcta en el XIB antes de construir.

Huh No parece una gran mejora, ¿verdad? Creo que podría ser ligeramente mejor que la interpolación en línea recta, o tal vez eso es solo una ilusión En cualquier caso, no vale la pena jactarse..


Seguir mejorando la calidad del trazo

Esto es lo que creo que está sucediendo: mientras nos tomamos la molestia de interpolar cada secuencia de cuatro puntos con un segmento de curva suave, no estamos haciendo ningún esfuerzo para hacer que un segmento de curva pase sin problemas al siguiente, Tan efectivamente todavía tenemos un problema con el resultado final..

Entonces, ¿qué podemos hacer al respecto? Si vamos a ceñirnos al enfoque que comenzamos en la última versión (es decir, usando curvas Bezier), debemos cuidar la continuidad y la suavidad en el "punto de unión" de dos segmentos Bezier adyacentes. Las dos tangentes en el punto final con los puntos de control correspondientes (segundo punto de control del primer segmento y primer punto de control del segundo segmento) parecen ser la clave; Si ambas tangentes tuvieran la misma dirección, la curva sería más suave en la unión..

¿Qué sucede si movemos el punto final común en algún lugar de la línea que une los dos puntos de control? Sin utilizar datos adicionales sobre los puntos de contacto, el mejor punto parece ser el punto medio de la línea que une los dos puntos de control en consideración, y nuestro requisito impuesto sobre la dirección de las dos tangentes se cumpliría. Intentemos esto!

Cree una subclase UIView (una vez más) y asígnele el nombre SmoothedBIView. Reemplace el código completo en el archivo .m con lo siguiente:

 #import "SmoothedBIView.h" @implementation SmoothedBIView UIBezierPath * path; UIImage * incrementalImage; Pts CGPoint [5]; // ahora necesitamos hacer un seguimiento de los cuatro puntos de un segmento Bezier y el primer punto de control del siguiente segmento uint ctr;  - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) [self setMultipleTouchEnabled: NO]; [self setBackgroundColor: [UIColor whiteColor]]; ruta = [UIBezierPath bezierPath]; [ruta setLineWidth: 2.0];  devuélvete a ti mismo;  - (id) initWithFrame: (CGRect) frame self = [super initWithFrame: frame]; if (self) [self setMultipleTouchEnabled: NO]; ruta = [UIBezierPath bezierPath]; [ruta setLineWidth: 2.0];  devuélvete a ti mismo;  // Solo anular drawRect: si realiza un dibujo personalizado. // Una implementación vacía afecta negativamente el rendimiento durante la animación. - (void) drawRect: (CGRect) rect [incrementalImage drawInRect: rect]; [trazo de trayectoria];  - (void) touchesBegan: (NSSet *) toca withEvent: (UIEvent *) event ctr = 0; UITouch * touch = [toca cualquier objeto]; pts [0] = [toque locationInView: self];  - (void) touchesMoved: (NSSet *) toca conEvent: (UIEvent *) evento UITouch * touch = [toca cualquier Objeto]; CGPoint p = [toque locationInView: self]; ctr ++; pts [ctr] = p; if (ctr == 4) pts [3] = CGPointMake ((pts [2] .x + pts [4] .x) /2.0, (pts [2] .y + pts [4] .y) /2.0 ); // mover el punto final al centro de la línea que une el segundo punto de control del primer segmento Bezier y el primer punto de control del segundo segmento Bezier [ruta moveToPoint: pts [0]]; [ruta addCurveToPoint: pts [3] controlPoint1: pts [1] controlPoint2: pts [2]]; // agrega un Bezier cúbico de pt [0] a pt [3], con los puntos de control pt [1] y pt [2] [auto setNeedsDisplay]; // Reemplace los puntos y prepárese para manejar el siguiente segmento pts [0] = pts [3]; pts [1] = pts [4]; ctr = 1;  - (void) touchesEnded: (NSSet *) toca withEvent: (UIEvent *) event [self drawBitmap]; [auto setNeedsDisplay]; [ruta removeAllPoints]; ctr = 0;  - (void) touchesCancelled: (NSSet *) toca withEvent: (UIEvent *) event [self touchesEnded: toca withEvent: event];  - (void) drawBitmap UIGraphicsBeginImageContextWithOptions (self.bounds.size, YES, 0.0); if (! incrementalImage) // primera vez; pintar fondo blanco UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; [[UIColor whiteColor] setFill]; [rectpath fill];  [incrementalImage drawAtPoint: CGPointZero]; [[UIColor blackColor] setStroke]; [trazo de trayectoria]; incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext ();  @final

El quid del algoritmo que discutimos anteriormente se implementa en el -toquesMovidos: ConEvento: método. Los comentarios en línea le ayudarán a vincular la discusión con el código..

Entonces, ¿cómo están los resultados, visualmente hablando? Recuerda hacer la cosa con el XIB..

Afortunadamente, hay mejoras sustanciales en esta ocasión. Teniendo en cuenta la simplicidad de nuestra modificación, se ve bastante bien (¡si lo digo yo mismo!). Nuestro análisis del problema con la iteración anterior y nuestra solución propuesta también se han validado..


A dónde ir desde aquí

Espero que hayas encontrado este tutorial beneficioso. Esperemos que desarrolles tus propias ideas sobre cómo mejorar el código. Una de las mejoras más importantes (pero fáciles) que puede incorporar es manejar el final de las secuencias táctiles con más gracia, como se explicó anteriormente..

Otro caso que descuidé es el manejo de una secuencia táctil que consiste en que el usuario toque la vista con el dedo y luego la levante sin haberla movido, con un toque efectivo en la pantalla. El usuario probablemente esperaría dibujar un punto o un pequeño garabato en la vista de esta manera, pero con nuestra implementación actual no pasa nada porque nuestro código de dibujo no se activa a menos que nuestra vista reciba la -toquesMovidos: ConEvento: mensaje. Es posible que desee echar un vistazo a la UIBezierPath Documentación de clase para ver qué otros tipos de rutas puedes construir..

Si su aplicación hace más trabajo que el que hicimos aquí (¡y en una aplicación de dibujo que vale la pena enviar, lo haría!), Diseñándola de tal manera que el código de no UI (en particular, el almacenamiento en caché fuera de la pantalla) se ejecute en un hilo de fondo Hacer una diferencia significativa en un dispositivo multinúcleo (iPad 2 en adelante). Incluso en un dispositivo de un solo procesador, como el iPhone 4, el rendimiento debería mejorar, ya que creo que el procesador dividirá el trabajo de almacenamiento en caché que, después de todo, ocurre solo una vez cada pocos ciclos del ciclo principal.

Le animo a que flexione sus músculos de codificación y juegue con UIKit API para desarrollar y mejorar algunas de las ideas implementadas en este tutorial. Diviértete, y gracias por leer.!