Dessin à main levée lisse sur iOS

Ce didacticiel vous expliquera comment mettre en œuvre un algorithme de dessin avancé pour un dessin fluide à main levée sur des appareils iOS. Continuer à lire!

Aperçu théorique

Le toucher est le moyen principal utilisé par un utilisateur pour interagir avec les appareils iOS. L’une des fonctionnalités les plus naturelles et les plus évidentes que ces appareils sont censés fournir est de permettre à l’utilisateur de dessiner sur l’écran avec le doigt. De nombreuses applications de dessin à main levée et de prise de notes sont actuellement disponibles sur l'App Store, et de nombreuses entreprises demandent même aux clients de signer un iDevice lors de leurs achats. Comment fonctionnent ces applications? Arrêtons-nous et réfléchissons un instant à ce qui se passe "sous le capot".

Lorsqu'un utilisateur fait défiler une vue tabulaire, pince pour agrandir une image ou trace une courbe dans une application de peinture, l'affichage de l'appareil est mis à jour rapidement (par exemple, 60 fois par seconde) et la boucle d'exécution de l'application est constamment affichée. échantillonnage l'emplacement du ou des doigts de l'utilisateur. Au cours de ce processus, l'entrée "analogique" d'un doigt glissant sur l'écran doit être convertie en un ensemble de points numériques sur l'écran, et ce processus de conversion peut poser des problèmes considérables. Dans le contexte de notre application de peinture, nous avons un problème "d'adaptation des données". Alors que l'utilisateur gribouille joyeusement sur l'appareil, le programmeur doit essentiellement interpoler les informations analogiques manquantes ("connect-the-dots") qui ont été perdues parmi les points de contact échantillonnés qu'iOS nous a signalés. En outre, cette interpolation doit se produire de manière à obtenir un trait continu, naturel et lisse pour l'utilisateur final, comme s'il dessinait avec un stylo sur un bloc-notes en papier..

Ce didacticiel a pour objectif de montrer comment le dessin à main levée peut être implémenté sur iOS, en partant d'un algorithme de base effectuant une interpolation en ligne droite et en passant à un algorithme plus sophistiqué se rapprochant de la qualité fournie par des applications connues telles que Penultimate. Comme si créer un algorithme qui fonctionne ne soit pas assez difficile, nous devons également nous assurer que cet algorithme fonctionne bien. Comme nous le verrons, une implémentation de dessin naïve peut conduire à une application présentant des problèmes de performances importants qui rendrait le dessin fastidieux et inutilisable..


Commencer

Je suppose que le développement sur iOS n’est pas totalement nouveau pour moi, j’ai donc survolé les étapes de création d’un nouveau projet, d’ajout de fichiers au projet, etc. Espérons qu’il n’ya rien de trop difficile ici de toute façon, mais juste au cas où, Le code de projet complet est disponible pour que vous puissiez télécharger et jouer avec.

Démarrer un nouveau projet Xcode iPad basé sur le "Application à vue unique"Modèle et nommez-le"FreehandDrawingTut". Assurez-vous d'activer le comptage automatique des références (ARC), mais désélectionnez Storyboards et tests unitaires. Vous pouvez faire de ce projet un iPhone ou une application universelle, en fonction des périphériques disponibles pour les tests..

Continuez ensuite et sélectionnez le projet "FreeHandDrawingTut" dans le navigateur Xcode et assurez-vous que seule l'orientation portrait est prise en charge:

Si vous souhaitez déployer sur iOS 5.x ou une version antérieure, vous pouvez modifier le support d'orientation de cette manière:

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

Je fais cela pour que les choses restent simples afin que nous puissions nous concentrer sur le principal problème à résoudre.

Je souhaite développer notre code de manière itérative, en l'améliorant progressivement - comme vous le feriez de manière réaliste si vous partiez de zéro - au lieu de vous laisser tomber la version finale d'un seul coup. J'espère que cette approche vous permettra de mieux maîtriser les différentes questions en jeu. Gardant cela à l'esprit, et pour éviter d'avoir à supprimer, modifier et ajouter à plusieurs reprises du code dans le même fichier, ce qui pourrait devenir salissant et sujet à erreurs, je vais adopter l'approche suivante:

  • Pour chaque itération, nous allons créer une nouvelle sous-classe UIView. Je publierai tout le code nécessaire pour que vous puissiez simplement copier et coller dans le fichier .m de la nouvelle sous-classe UIView que vous créez. Il n'y aura pas d'interface publique pour les fonctionnalités de la sous-classe view, ce qui signifie que vous n'aurez pas besoin de toucher le fichier .h.
  • Pour tester chaque nouvelle version, nous devrons attribuer la sous-classe UIView que nous avons créée à la vue occupant actuellement l'écran. Je vais vous montrer comment faire cela avec Interface Builder pour la première fois, en suivant les étapes en détail, puis je vous le rappellerai chaque fois que nous coderons une nouvelle version..

Première tentative de dessin

Dans Xcode, choisissez Fichier> Nouveau> Fichier… , choisissez la classe Objective-C comme modèle et, dans l'écran suivant, nommez le fichier LinearInterpView et en faire une sous-classe de UIView. Sauvegarde le. Le nom "LinearInterp" est l'abréviation de "interpolation linéaire" ici. Par souci du didacticiel, je vais nommer chaque sous-classe UIView que nous créons pour souligner un concept ou une approche introduite dans le code de classe..

Comme je l'ai déjà mentionné, vous pouvez laisser le fichier d'en-tête tel quel. Effacer tout le code présent dans le fichier LinearInterpView.m et remplacez-le par le texte suivant:

 #import "LinearInterpView.h" @implementation LinearInterpView UIBezierPath * path; // (3) - (id) initWithCoder: (NSCoder *) aDecoder // (1) if (self = [super initWithCoder: aDecoder]) [self setMultipleTouchEnabled: NO]; // (2) [self setBackgroundColor: [UIColor whiteColor]]; chemin = [UIBezierPath bezierPath]; [chemin setLineWidth: 2.0];  retourner soi-même;  - (void) drawRect: (CGRect) rect // (5) [[UIColor blackColor] setStroke]; [accident vasculaire cérébral];  - (void) toucheBegan: (NSSet *) touche withEvent: (UIEvent *) événement UITouch * touch = [touche anyObject]; CGPoint p = [touch locationInView: self]; [path moveToPoint: p];  - (void) toucheMoved: (NSSet *) touche withEvent: (UIEvent *) événement UITouch * touch = [touche anyObject]; CGPoint p = [touch locationInView: self]; [chemin addLineToPoint: p]; // (4) [self setNeedsDisplay];  - (void) touchesEnded: (NSSet *) touche withEvent: (UIEvent *) event [self touchesMoved: touches withEvent: event];  - (void) touchesCancelled: (NSSet *) touche withEvent: (UIEvent *) event [auto touchesEnded: touches withEvent: event];  @fin

Dans ce code, nous travaillons directement avec les événements tactiles que l'application nous signale chaque fois que nous avons une séquence tactile. en d’autres termes, l’utilisateur place un doigt sur la vue à l’écran, le déplace et le lève enfin de l’écran. Pour chaque événement de cette séquence, l'application nous envoie un message correspondant (en terminologie iOS, les messages sont envoyés au "premier répondant"; vous pouvez vous reporter à la documentation pour plus de détails)..

Pour traiter ces messages, nous implémentons les méthodes -touchesBegan: WithEvent: et company, qui sont déclarés dans la classe UIResponder dont UIView hérite. Nous pouvons écrire du code pour gérer les événements tactiles comme bon nous semble. Dans notre application, nous souhaitons interroger l'emplacement des touches sur l'écran, effectuer un traitement, puis tracer des lignes à l'écran..

Les points font référence aux nombres commentés correspondants du code ci-dessus:

  1. Nous annulons -initWithCoder: parce que la vue est née d'un XIB, comme nous allons le mettre en place sous peu.
  2. Nous avons désactivé les touches multiples: nous ne traiterons qu’une séquence de touches, ce qui signifie que l’utilisateur ne peut dessiner qu’avec un doigt à la fois; tout autre doigt placé sur l'écran pendant ce temps sera ignoré. Ceci est une simplification, mais pas forcément une personne déraisonnable - les gens n'écrivent pas non plus sur papier avec deux stylos à la fois! En tout cas, ça nous empêchera de trop nous éloigner, car nous avons déjà assez de travail à faire..
  3. le UIBezierPath est une classe UIKit qui nous permet de dessiner des formes sur l'écran composées de lignes droites ou de certains types de courbes.
  4. Puisque nous faisons un dessin personnalisé, nous devons remplacer la vue -drawRect: méthode. Nous faisons cela en dessinant le chemin chaque fois qu'un nouveau segment de ligne est ajouté..
  5. Notez également que si la largeur de ligne est une propriété du chemin, la couleur de la ligne elle-même est une propriété du contexte de dessin. Si vous n'êtes pas familier avec les contextes graphiques, vous pouvez en savoir plus dans les documents Apple. Pour l’instant, imaginez un contexte graphique comme un "canevas" dans lequel vous dessinez lorsque vous remplacez le -drawRect: méthode, et le résultat de ce que vous voyez est la vue à l'écran. Nous rencontrerons bientôt un autre type de contexte de dessin.

Avant de pouvoir créer l'application, nous devons définir la sous-classe de vue que nous venons de créer sur la vue à l'écran..

  1. Dans le volet de navigation, cliquez sur ViewController.xib (au cas où vous auriez créé une application universelle, effectuez simplement cette étape à la fois ViewController ~ iPhone.xib et ViewController ~ iPad.xib des dossiers).
  2. Lorsque la vue apparaît sur le canevas du générateur d’interface, cliquez dessus pour le sélectionner. Dans le volet Utilitaires, cliquez sur "Inspecteur des identités" (troisième bouton en partant de la droite en haut du volet). La section la plus en haut dit "Classe personnalisée", c'est ici que vous allez définir la classe de la vue sur laquelle vous avez cliqué.
  3. À l'heure actuelle, il devrait indiquer "UIView", mais nous devons le changer en (vous l'avez deviné) LinearInterpView. Tapez le nom de la classe (il suffit de taper "L" pour que le remplissage automatique s'abaisse de manière rassurante).
  4. Encore une fois, si vous envisagez de le tester en tant qu'application universelle, répétez cette étape pour les deux fichiers XIB créés par le modèle..

Maintenant, construisez l'application. Vous devriez obtenir une vue blanche brillante que vous pouvez dessiner avec votre doigt. Compte tenu des quelques lignes de code que nous avons écrites, les résultats ne sont pas trop médiocres! Bien sûr, ils ne sont pas spectaculaires non plus. L’aspect relier les points est plutôt perceptible (et oui, mon écriture craint aussi).

Assurez-vous que vous exécutez l'application non seulement sur le simulateur, mais également sur un appareil réel..

Si vous jouez avec l'application pendant un certain temps sur votre appareil, vous remarquerez sûrement quelque chose: éventuellement, la réponse de l'interface utilisateur commence à ralentir et au lieu des ~ 60 points de contact acquis par seconde, pour une raison quelconque, le nombre de points l'interface utilisateur est capable d'échantillonner des gouttes de plus en plus loin. Puisque les points sont de plus en plus éloignés, l'interpolation en ligne droite rend le dessin encore plus "bloc" qu'auparavant. Ceci est certainement indésirable. Alors que se passe-t-il?


Maintenir la performance et la réactivité

Passons en revue ce que nous avons fait: au fur et à mesure que nous dessinons, nous acquérons des points, nous les ajoutons à un chemin toujours croissant, puis nous rendons le chemin * complet * à chaque cycle de la boucle principale. Ainsi, au fur et à mesure que le chemin s'allonge, le système de dessin a plus à dessiner à chaque itération et devient finalement trop, rendant l'application difficile à suivre. Puisque tout se passe sur le thread principal, notre code de dessin est en concurrence avec le code de l'interface utilisateur qui doit, entre autres, échantillonner les retouches à l'écran..

Vous seriez pardonné de penser qu'il y avait un moyen de dessiner "au-dessus" de ce qui était déjà à l'écran; malheureusement, c’est là que nous devons nous libérer de l’analogie stylo sur papier car le système graphique ne fonctionne pas vraiment de cette façon par défaut. Bien que, en vertu du code que nous allons écrire prochainement, nous allons indirectement mettre en œuvre l’approche «tirage au sort».

Bien que nous puissions essayer de corriger les performances de notre code, nous n'allons mettre en œuvre qu'une idée, car elle s'avère suffisante pour nos besoins actuels..

Créez une nouvelle sous-classe UIView comme vous le faisiez auparavant, en la nommant CachedLIView (le LI est pour nous rappeler que nous faisons encore Ldans l'oreille jenterpolation). Supprimer tout le contenu de CachedLIView.m et le remplacer par ce qui suit:

 #import "CachedLIView.h" @implementation CachedLIView UIBezierPath * path; UIImage * incrementalImage; // (1) - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) self setMultipleTouchEnabled: NO]; [self setBackgroundColor: [UIColor whiteColor]]; chemin = [UIBezierPath bezierPath]; [chemin setLineWidth: 2.0];  retourner soi-même;  - (void) drawRect: (CGRect) rect [incrementalImage drawInRect: rect]; // (3) [tracé de chemin];  - (void) toucheBegan: (NSSet *) touche withEvent: (UIEvent *) événement UITouch * touch = [touche anyObject]; CGPoint p = [touch locationInView: self]; [path moveToPoint: p];  - (void) toucheMoved: (NSSet *) touche withEvent: (UIEvent *) événement UITouch * touch = [touche anyObject]; CGPoint p = [touch locationInView: self]; [chemin addLineToPoint: p]; [self setNeedsDisplay];  - (void) toucheEnded: (NSSet *) touche withEvent: (UIEvent *) event // (2) UITouch * touch = [touche anyObject]; CGPoint p = [touch locationInView: self]; [chemin addLineToPoint: p]; [self drawBitmap]; // (3) [self setNeedsDisplay]; [chemin removeAllPoints]; // (4) - (void) touchesCancelled: (NSSet *) touches withEvent: (UIEvent *) event [self touchesEnded: touches withEvent: event];  - (void) drawBitmap // (3) UIGraphicsBeginImageContextWithOptions (self.bounds.size, YES, 0.0); [[UIColor blackColor] setStroke]; if (! incrementalImage) // premier tirage au sort; peindre en fond blanc avec… UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; // encadrant un bitmap par un rectangle défini par un autre objet UIBezierPath [[UIColor whiteColor] setFill]; [remplissage rectpath]; // le remplissant de blanc [incrementalImage drawAtPoint: CGPointZero]; [accident vasculaire cérébral]; incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext ();  @fin

Après la sauvegarde, n'oubliez pas de changer la classe de l'objet vue dans votre / vos XIB en CachedLIView!

Lorsque l'utilisateur place son doigt sur l'écran pour dessiner, nous commençons avec un nouveau chemin, sans points ni lignes, et nous y ajoutons des segments de ligne, comme nous le faisions auparavant..

Encore une fois, en référence aux chiffres dans les commentaires:

  1. Nous conservons en outre en mémoire une image bitmap (hors écran) de la même taille que notre canevas (c'est-à-dire à l'écran), dans laquelle nous pouvons stocker ce que nous avons dessiné jusqu'à présent..
  2. Nous dessinons le contenu à l'écran dans cette mémoire tampon chaque fois que l'utilisateur lève son doigt (signalé par -touchesEnded: WithEvent).
  3. La méthode drawBitmap crée un contexte bitmap - les méthodes UIKit ont besoin d'un "contexte actuel" (un canevas) pour y dessiner. Quand on est dedans -drawRect: ce contexte est automatiquement mis à notre disposition et reflète ce que nous attirons dans notre vue à l'écran. En revanche, le contexte bitmap doit être créé et détruit explicitement, et le contenu dessiné réside en mémoire..
  4. En mettant en cache le dessin précédent de cette manière, nous pouvons supprimer le contenu précédent du chemin et ainsi éviter que le chemin ne devienne trop long..
  5. Maintenant chaque fois drawRect: s’appelle, nous dessinons d’abord le contenu de la mémoire tampon dans notre vue qui (par conception) a exactement la même taille, et nous maintenons donc pour l’utilisateur l’illusion de dessiner en continu, mais d’une manière différente de celle d’avant.

Bien que ce ne soit pas parfait (et si notre utilisateur continue à dessiner sans lever le doigt, jamais?), Ce sera suffisant pour la portée de ce tutoriel. Nous vous encourageons à expérimenter vous-même pour trouver une meilleure méthode. Par exemple, vous pouvez essayer de mettre le dessin en cache périodiquement plutôt que lorsque l'utilisateur lève le doigt. En l'occurrence, cette procédure de mise en cache hors écran nous offre l'opportunité d'un traitement en arrière-plan, si nous choisissions de l'implémenter. Mais nous ne ferons pas cela dans ce tutoriel. Vous êtes invités à essayer vous-même, bien que!


Améliorer la qualité visuelle des accidents vasculaires cérébraux

Nous allons maintenant concentrer notre attention sur l'amélioration du dessin. Jusqu'à présent, nous avons joint des points de contact adjacents avec des segments de droite. Mais normalement, lorsque nous dessinons à main levée, notre trait naturel a une apparence fluide et courbée (plutôt que bloc et rigide). Il est logique d'essayer d'interpoler nos points avec des courbes plutôt que des segments. Heureusement, la classe UIBezierPath nous permet de dessiner son nom: courbes de Bézier.

Quelles sont les courbes de Bézier? Sans invoquer la définition mathématique, une courbe de Bézier est définie par quatre points: deux extrémités parcourues par une courbe et deux "points de contrôle" qui aident à définir les tangentes que la courbe doit toucher à ses extrémités (techniquement une courbe de Bézier cubique, mais pour plus de simplicité, je parlerai simplement de "courbe de Bézier").

Les courbes de Bézier nous permettent de dessiner toutes sortes de formes intéressantes.

Nous allons maintenant essayer de regrouper des séquences de quatre points de contact adjacents et d’interpoler la séquence de points dans un segment de courbe de Bézier. Chaque paire adjacente de segments de Bézier partagera un point final en commun afin de maintenir la continuité du trait.

Vous connaissez la foret maintenant. Créez une nouvelle sous-classe UIView et nommez-la BezierInterpView. Collez le code suivant dans le fichier .m:

 #import "BezierInterpView.h" @implementation BezierInterpView UIBezierPath * path; UIImage * incrementalImage; Points CGPoint [4]; // suivre les quatre points de notre segment de Bézier uint ctr; // une variable de compteur permettant de suivre l'indice de point - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) self setMultipleTouchEnabled: NO]; [self setBackgroundColor: [UIColor whiteColor]]; chemin = [UIBezierPath bezierPath]; [chemin setLineWidth: 2.0];  retourner soi-même;  - (void) drawRect: (CGRect) rect [incrementalImage drawInRect: rect]; [accident vasculaire cérébral];  - (void) touchBegan: (NSSet *) touche withEvent: (UIEvent *) événement ctr = 0; UITouch * touch = [touche anyObject]; pts [0] = [touch locationInView: self];  - (void) toucheMoved: (NSSet *) touche withEvent: (UIEvent *) événement UITouch * touch = [touche anyObject]; CGPoint p = [touch locationInView: self]; ctr ++; pts [ctr] = p; if (ctr == 3) // 4ème point [path moveToPoint: pts [0]]; [chemin addCurveToPoint: pts [3] controlPoint1: pts [1] controlPoint2: pts [2]]; // voici comment une courbe de Bézier est ajoutée à un chemin. Nous ajoutons un Bézier cubique de pt [0] à pt [3], avec les points de contrôle pt [1] et pt [2] [self setNeedsDisplay]; pts [0] = [chemin currentPoint]; ctr = 0;  - (void) touchesEnded: (NSSet *) touche withEvent: (UIEvent *) event [self drawBitmap]; [self setNeedsDisplay]; pts [0] = [chemin currentPoint]; // que le deuxième point d'extrémité du segment de Bézier actuel soit le premier du prochain segment de Bézier [path removeAllPoints]; ctr = 0;  - (void) touchesCancelled: (NSSet *) touche withEvent: (UIEvent *) event [auto touchesEnded: touches withEvent: event];  - (void) drawBitmap UIGraphicsBeginImageContextWithOptions (self.bounds.size, YES, 0.0); [[UIColor blackColor] setStroke]; if (! incrementalImage) // première fois; peinture de fond blanc UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; [[UIColor whiteColor] setFill]; [remplissage rectpath];  [incrementalImage drawAtPoint: CGPointZero]; [accident vasculaire cérébral]; incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext ();  @fin

Comme l'indiquent les commentaires en ligne, le principal changement est l'introduction de deux nouvelles variables permettant de garder une trace des points de nos segments de Bézier, ainsi qu'une modification de la -(void) touchesMoved: withEvent: méthode permettant de dessiner un segment de Bézier pour quatre points (en fait, tous les trois points, en termes de touches que l'application nous signale, car nous partageons un point de terminaison pour chaque paire de segments de Bézier adjacents).

Vous remarquerez peut-être ici que nous avons négligé le cas où l'utilisateur a levé son doigt et mis fin à la séquence tactile avant de disposer de suffisamment de points pour terminer notre dernier segment de Bézier. Si oui, vous auriez raison! Bien que visuellement, cela ne fasse pas beaucoup de différence, dans certains cas importants, cependant. Par exemple, essayez de dessiner un petit cercle. Il se peut qu’elle ne ferme pas complètement et, dans une application réelle, vous souhaitez gérer cela de manière appropriée dans le -touchesEnded: WithEvent méthode. Pendant que nous y sommes, nous n’avons également accordé aucune attention particulière au cas de l’annulation tactile. le touchesCancelled: WithEvent méthode d'instance gère cela. Consultez la documentation officielle pour voir s’il ya des cas particuliers que vous devrez peut-être gérer ici..

Alors, à quoi ressemblent les résultats? Une fois de plus, je vous rappelle de définir la classe correcte dans le XIB avant de construire.

Huh. Cela ne semble pas être beaucoup d'amélioration, n'est-ce pas? Je crois que ça pourrait l'être légèrement mieux que l'interpolation en ligne droite, ou peut-être que c'est juste un vœu pieux. En tout cas, rien ne vaut la peine de se vanter.


Améliorer davantage la qualité de l'AVC

Voici ce que je pense qui se passe: pendant que nous prenons la peine d'interpoler chaque séquence de quatre points avec un segment de courbe lisse, nous ne faisons aucun effort pour faire un segment de courbe pour une transition en douceur dans le prochain, si bien que nous avons toujours un problème avec le résultat final.

Alors, que pouvons-nous faire à ce sujet? Si nous allons nous en tenir à l’approche que nous avons commencée dans la dernière version (c’est-à-dire en utilisant les courbes de Bézier), nous devons veiller à la continuité et au lissage au "point de jonction" de deux segments de Bézier adjacents. Les deux tangentes au point final avec les points de contrôle correspondants (deuxième point de contrôle du premier segment et premier point de contrôle du deuxième segment) semblent être la clé; si ces deux tangentes avaient la même direction, la courbe serait plus lisse à la jonction.

Et si nous déplacions le point de terminaison commun quelque part sur la ligne joignant les deux points de contrôle? Sans utiliser des données supplémentaires sur les points de contact, le meilleur point semblerait être le point médian de la ligne joignant les deux points de contrôle considérés et notre exigence imposée concernant la direction des deux tangentes serait satisfaite. Essayons ça!

Créez (encore une fois encore) une sous-classe UIView et nommez-la SmoothedBIView. Remplacez le code complet du fichier .m par le texte suivant:

 #import "SmoothedBIView.h" @implementation SmoothedBIView UIBezierPath * path; UIImage * incrementalImage; Points CGPoint [5]; // nous devons maintenant suivre les quatre points d'un segment de Bézier et le premier point de contrôle du segment suivant uint ctr;  - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) self setMultipleTouchEnabled: NO]; [self setBackgroundColor: [UIColor whiteColor]]; chemin = [UIBezierPath bezierPath]; [chemin setLineWidth: 2.0];  retourner soi-même;  - (id) initWithFrame: (CGRect) frame self = [super initWithFrame: frame]; if (self) [self setMultipleTouchEnabled: NO]; chemin = [UIBezierPath bezierPath]; [chemin setLineWidth: 2.0];  retourner soi-même;  // substitue uniquement drawRect: si vous effectuez un dessin personnalisé. // Une implémentation vide affecte négativement les performances lors de l'animation. - (void) drawRect: (CGRect) rect [incrementalImage drawInRect: rect]; [accident vasculaire cérébral];  - (void) touchBegan: (NSSet *) touche withEvent: (UIEvent *) événement ctr = 0; UITouch * touch = [touche anyObject]; pts [0] = [touch locationInView: self];  - (void) toucheMoved: (NSSet *) touche withEvent: (UIEvent *) événement UITouch * touch = [touche anyObject]; CGPoint p = [touch locationInView: self]; ctr ++; pts [ctr] = p; si (ctr == 4) pts [3] = CGPointMake ((pts [2] .x + pts [4] .x) /2.0, (pts [2] .y + pts [4] .y) /2.0 ) // déplace le point final au milieu de la ligne joignant le deuxième point de contrôle du premier segment de Bézier et le premier point de contrôle du deuxième segment de Bézier [path moveToPoint: pts [0]]; [chemin addCurveToPoint: pts [3] controlPoint1: pts [1] controlPoint2: pts [2]]; // ajoute un Bezier cubique de pt [0] à pt [3], avec les points de contrôle pt [1] et pt [2] [self setNeedsDisplay]; // remplace les points et prépare le prochain segment pts [0] = pts [3]; pts [1] = pts [4]; ctr = 1;  - (void) touchesEnded: (NSSet *) touche withEvent: (UIEvent *) event [self drawBitmap]; [self setNeedsDisplay]; [chemin removeAllPoints]; ctr = 0;  - (void) touchesCancelled: (NSSet *) touche withEvent: (UIEvent *) event [auto touchesEnded: touches withEvent: event];  - (void) drawBitmap UIGraphicsBeginImageContextWithOptions (self.bounds.size, YES, 0.0); if (! incrementalImage) // première fois; peinture de fond blanc UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; [[UIColor whiteColor] setFill]; [remplissage rectpath];  [incrementalImage drawAtPoint: CGPointZero]; [[UIColor blackColor] setStroke]; [accident vasculaire cérébral]; incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext ();  @fin

Le noeud de l’algorithme que nous avons discuté ci-dessus est implémenté dans le -touchesMoved: WithEvent: méthode. Les commentaires en ligne devraient vous aider à relier la discussion au code..

Alors, comment sont les résultats, visuellement? N'oubliez pas de faire la chose avec le XIB.

Heureusement, il y a eu une amélioration substantielle cette fois-ci. Compte tenu de la simplicité de notre modification, cela semble plutôt bon (si je le dis moi-même!). Notre analyse du problème avec l'itération précédente et la solution proposée ont également été validées..


Où aller en partant d'ici

J'espère que vous avez trouvé ce tutoriel utile. J'espère que vous développerez vos propres idées sur la façon d'améliorer le code. L'une des améliorations les plus importantes (mais faciles) que vous pouvez intégrer est la gestion plus harmonieuse de la fin des séquences tactiles, comme indiqué précédemment..

Un autre cas que j'ai négligé concerne le traitement d'une séquence tactile consistant pour l'utilisateur à toucher la vue avec son doigt, puis à la soulever sans l'avoir déplacée, ce qui se traduit par un tapotement sur l'écran. L'utilisateur s'attendrait probablement à dessiner un point ou un petit gribouillis sur la vue de cette façon, mais avec notre implémentation actuelle, rien ne se produit car notre code de dessin ne se déclenche que si notre vue reçoit le message. -touchesMoved: WithEvent: message. Vous voudrez peut-être jeter un oeil à la UIBezierPath documentation de classe pour voir quels autres types de chemins vous pouvez construire.

Si votre application fait plus de travail que ce que nous avons fait ici (et dans une application de dessin méritant d'être livrée, ce serait le cas!), Concevez-la de sorte que le code non UI (en particulier la mise en cache hors écran) s'exécute dans un fil d'arrière-plan. faire une différence significative sur un périphérique multicœur (iPad 2 et ultérieur). Même sur un périphérique à processeur unique, tel que l'iPhone 4, les performances devraient être améliorées, car je m'attends à ce que le processeur divise le travail de mise en cache qui, après tout, ne se produit qu'une fois tous les quelques cycles de la boucle principale..

Je vous encourage à faire preuve de souplesse et à jouer avec l'API UIKit afin de développer et d'améliorer certaines des idées mises en œuvre dans ce didacticiel. Amusez-vous et merci d'avoir lu!