introduction

Cette mini-série en deux parties vous apprendra à créer un effet de pliage de page impressionnant avec Core Animation. Dans cet article, vous apprendrez les étapes nécessaires pour peaufiner le pli de page créé précédemment et vous construirez une expérience de pliage plus interactive à l'aide de gestes pincés..


introduction

Dans la première partie de ce didacticiel en deux parties, nous avons pris une application de dessin à main levée existante qui permettait à l'utilisateur de dessiner sur l'écran avec son doigt et nous avons implémenté un effet de repliement en 3D sur la zone de dessin qui donnait l'impression visuelle d'un livre en cours de création. plié le long de son dos. Nous avons réalisé cela en utilisant CALayers et transformations. Nous avons associé l'effet à un geste de tapotement qui entraînait le repliement par incréments, en s'animant en douceur entre les incréments..

Dans cette partie du didacticiel, nous verrons comment améliorer l’aspect visuel de l’effet (en ajoutant des coins courbes à nos pages et en laissant nos pages ombrer en arrière-plan), ainsi que de rendre le pliage-dépliage plus interactif. , en laissant l’utilisateur le contrôler avec un geste pincé. En chemin, nous apprendrons plusieurs choses: a CALayer sous-classe appelée CAShapeLayer, comment masquer un calque pour lui donner une forme non rectangulaire, comment permettre à un calque de projeter une ombre, comment configurer les propriétés de l'ombre, et un peu plus sur l'animation de calque implicite et comment faire en sorte que ces animations soient agréables à l'ajout interaction de l'utilisateur dans le mix.

Le point de départ de cette partie sera le projet Xcode avec lequel nous nous sommes retrouvés à la fin du premier tutoriel. Continuons!


Façonner les coins de la page

Rappelons que chacune des pages gauche et droite était une instance de CALayer. Ils étaient de forme rectangulaire, placés sur les moitiés gauche et droite de la toile, aboutant le long de la ligne verticale qui traverse le centre. Nous avons dessiné le contenu (c'est-à-dire l'esquisse à main levée de l'utilisateur) des moitiés gauche et droite de la toile dans ces deux pages..

Même si un CAlayer lors de la création commence avec des limites rectangulaires (comme UIViews), l’une des choses intéressantes que nous pouvons faire avec les calques est de couper leur forme selon un "masque", afin qu’ils ne soient plus limités à un rectangle! Comment ce masque est-il défini?? CALayers ont un masque propriété qui est un CALayer dont le canal alpha du contenu décrit le masque à utiliser. Si nous utilisons un masque "souple" (le canal alpha a des valeurs fractionnaires), nous pouvons rendre le calque partiellement transparent. Si nous utilisons un masque "dur" (c'est-à-dire avec des valeurs alpha nulles ou égales à un), nous pouvons "découper" le calque pour qu'il atteigne une forme bien définie de notre choix..

Nous pourrions utiliser une image externe pour définir notre masque. Cependant, comme notre masque a une forme spécifique (un rectangle avec quelques coins arrondis), il existe une meilleure façon de le faire en code. Pour spécifier une forme pour notre masque, nous utilisons une sous-classe de CALayer appelé CAShapeLayer. CAShapeLayers sont des calques pouvant avoir n'importe quelle forme définie par un chemin vectoriel du type opaque Core Graphics CGPathRef. Nous pouvons soit créer directement ce chemin à l’aide de l’API Core Graphics basée sur C, soit, plus simplement, créer un fichier. UIBezierPath objet avec le framework Objective-C UIKit. UIBezierPath expose le sous-jacent CGPathRef objet via son CGPath propriété qui peut être attribuée à notre CAShapeLayerde chemin propriété, et cette couche de forme peut à son tour être assignée à être notre CALayerle masque. Heureusement pour nous, UIBezierPath peut être initialisé avec de nombreuses formes prédéfinies intéressantes, y compris un rectangle avec des coins arrondis (où nous choisissons le ou les coins à arrondir).

Ajoutez le code suivant, après, disons, la ligne rightPage.transform = makePerspectiveTransform (); dans le ViewController.m viewDidAppear: méthode:

 // coins arrondis UIBezierPath * leftPageRoundedCornersPath = [UIBezierPath bezierPathWithRoundedRect: leftPage.bounds byRoundingCorners: UIRectCornerTopLeft | UIRectCornerBottomLeft cornerRadii: CGSize. UIBezierPath * rightPageRoundedCornersPath = [UIBezierPath bezierPathWithRoundedRect: rightPage.bounds byRoundingCorners: UIRectCornerTopRight | UIRectCornerBottomDroit cornerRadii: CGSizeMake (25.0, 25.0); CAShapeLayer * leftPageRoundedCornersMask = [couche CAShapeLayer]; CAShapeLayer * rightPageRoundedCornersMask = [couche CAShapeLayer]; leftPageRoundedCornersMask.frame = leftPage.bounds; rightPageRoundedCornersMask.frame = rightPage.bounds; leftPageRoundedCornersMask.path = leftPageRoundedCornersPath.CGPath; rightPageRoundedCornersMask.path = rightPageRoundedCornersPath.CGPath; leftPage.mask = leftPageRoundedCornersMask; rightPage.mask = rightPageRoundedCornersMask;

Le code devrait être explicite. Le tracé de Bézier a la forme d'un rectangle de la même taille que le calque masqué et les coins appropriés sont arrondis (haut gauche et bas gauche pour la page de gauche, haut droite et bas droite pour la page de droite)..

Construisez le projet et exécutez-le. Les coins de la page doivent être arrondis maintenant… cool!


Appliquer une ombre

Appliquer une ombre est également facile, mais il y a un problème lorsque nous voulons une ombre après avoir appliqué un masque (comme nous venons de le faire). Nous verrons cela en temps voulu!

Un CALayer a un shadowPath qui est un (vous l'avez deviné) CGPathRef et définit la forme de l'ombre. Une ombre a plusieurs propriétés que nous pouvons définir: sa couleur, son décalage (de quelle manière et à quelle distance elle tombe du calque), son rayon (précisant son étendue et son flou) et son opacité..

En fait, il convient de mentionner qu’il n’est pas absolument essentiel de définir le shadowPath car le système de dessin déterminera l’ombre de la couche alpha composée de la couche. Cependant, ceci est moins efficace et nuit généralement aux performances, il est donc toujours recommandé de définir le paramètre shadowPath propriété lorsque possible.

Insérez le bloc de code suivant immédiatement après celui que nous venons d'ajouter:

 leftPage.shadowPath = [UIBezierPath bezierPathWithRect: leftPage.bounds] .CGPath; rightPage.shadowPath = [UIBezierPath bezierPathWithRect: rightPage.bounds] .CGPath; leftPage.shadowRadius = 100.0; leftPage.shadowColor = [UIColor blackColor] .CGColor; leftPage.shadowOpacity = 0,9; rightPage.shadowRadius = 100.0; rightPage.shadowColor = [UIColor blackColor] .CGColor; rightPage.shadowOpacity = 0,9;

Générez et exécutez le code. Malheureusement, aucune ombre ne sera projetée et le résultat sera exactement comme avant. Quoi de neuf avec ça?!

Pour comprendre ce qui se passe, commentez le premier bloc de code que nous avons écrit dans ce tutoriel, mais laissez le code fantôme en place. Maintenant, construisez et courez. OK, nous avons perdu les coins arrondis, mais maintenant nos couches jettent une ombre nébuleuse sur la vue derrière elles, renforçant le sens de la profondeur.

Qu'est-ce qui se passe est que lorsque nous définissons un CALayerLa propriété masque de la propriété, la région de rendu de la couche est découpée dans la région du masque. Par conséquent, les ombres (qui sont naturellement éloignées du calque) ne sont pas rendues et par conséquent n'apparaissent pas..

Avant de tenter de résoudre ce problème, notez que l'ombre de la page de droite a été projetée en haut de la page de gauche. C'est parce que le leftPage a été ajouté à la vue avant rightPage, par conséquent, le premier est effectivement "derrière" le dernier dans l'ordre d'affichage (même s'il s'agit de calques frères). En plus de changer l’ordre dans lequel les deux couches ont été ajoutées à la super couche, nous pouvons changer la zPosition propriété des calques de spécifier explicitement l’ordre de dessin, en attribuant une plus petite valeur flottante au calque que nous voulions dessiner en premier. Nous voudrions une implémentation plus complexe si nous voulions éviter complètement cet effet, mais comme cela donne (fortuitement) un bel effet ombré à notre page, nous sommes heureux de voir les choses telles qu'elles sont!


Obtenir les deux ombres et une forme masquée

Pour résoudre ce problème, nous allons utiliser deux couches pour représenter chaque page, une pour générer des ombres et l'autre pour afficher le contenu dessiné dans une région de forme. En termes de hiérarchie, nous allons ajouter les calques d'ombre en tant que sous-calque direct au calque de la vue d'arrière-plan. Ensuite, nous ajouterons les calques de contenu aux calques d'ombre. Par conséquent, les calques d'ombre se transformeront en "conteneurs" pour le calque de contenu. Toutes nos transformations géométriques (relatives à l'effet de changement de page) seront appliquées aux couches d'ombres. Puisque les couches de contenu seront rendues par rapport à leurs conteneurs, nous n’aurons pas à leur appliquer de transformation..

Une fois que vous avez compris cela, l'écriture du code est relativement simple. Mais à cause de tous les changements que nous apportons, il sera compliqué de modifier le code précédent. Je vous suggère donc de remplacer tout le code dans viewDidAppear: avec ce qui suit:

 - (void) viewDidAppear: (BOOL) animated [super viewDidAppear: animated]; self.view.backgroundColor = [UIColor whiteColor]; leftPageShadowLayer = [Couche CAShapeLayer]; rightPageShadowLayer = [Couche CAShapeLayer]; leftPageShadowLayer.anchorPoint = CGPointMake (1.0, 0.5); rightPageShadowLayer.anchorPoint = CGPointMake (0.0, 0.5); leftPageShadowLayer.position = CGPointMake (self.view.bounds.size.width / 2, self.view.bounds.size.height / 2); rightPageShadowLayer.position = CGPointMake (self.view.bounds.size.width / 2, self.view.bounds.size.height / 2); leftPageShadowLayer.bounds = CGRectMake (0, 0, self.view.bounds.size.width / 2, self.view.bounds.size.height); rightPageShadowLayer.bounds = CGRectMake (0, 0, self.view.bounds.size.width / 2, self.view.bounds.size.height); UIBezierPath * leftPageRoundedCornersPath = [UIBezierPath bezierPathWithRoundedRect: leftPageShadowLayer.bounds byRoundingCorners: UIRectCornerTopLeft | UIRectCornerBottomLeft cornerRadi: CGSize. UIBezierPath * rightPageRoundedCornersPath = [UIBezierPath bezierPathWithRoundedRect: rightPageShadowLayer.bounds byRoundingCorners: UIRectCornerTopRight | UIRectCornerBottomRight cornerRadii: CGSize. 25, leftPageShadowLayer.shadowPath = leftPageRoundedCornersPath.CGPath; rightPageShadowLayer.shadowPath = rightPageRoundedCornersPath.CGPath; leftPageShadowLayer.shadowColor = [UIColor blackColor] .CGColor; leftPageShadowLayer.shadowRadius = 100.0; leftPageShadowLayer.shadowOpacity = 0.9; rightPageShadowLayer.shadowColor = [UIColor blackColor] .CGColor; rightPageShadowLayer.shadowRadius = 100; rightPageShadowLayer.shadowOpacity = 0.9; leftPage = [couche calque]; rightPage = [couche calque]; leftPage.frame = leftPageShadowLayer.bounds; rightPage.frame = rightPageShadowLayer.bounds; leftPage.backgroundColor = [Couleur blanche UIColor] .CGColor; rightPage.backgroundColor = [UIColor whiteColor] .CGColor; leftPage.borderColor = [UIColor darkGrayColor] .CGColor; rightPage.borderColor = [UIColor darkGrayColor] .CGColor; leftPage.transform = makePerspectiveTransform (); rightPage.transform = makePerspectiveTransform (); CAShapeLayer * leftPageRoundedCornersMask = [couche CAShapeLayer]; CAShapeLayer * rightPageRoundedCornersMask = [couche CAShapeLayer]; leftPageRoundedCornersMask.frame = leftPage.bounds; rightPageRoundedCornersMask.frame = rightPage.bounds; leftPageRoundedCornersMask.path = leftPageRoundedCornersPath.CGPath; rightPageRoundedCornersMask.path = rightPageRoundedCornersPath.CGPath; leftPage.mask = leftPageRoundedCornersMask; rightPage.mask = rightPageRoundedCornersMask; leftPageShadowLayer.transform = makePerspectiveTransform (); rightPageShadowLayer.transform = makePerspectiveTransform (); curtainView = [[UIView alloc]] initWithFrame: self.view.bounds]; curtainView.backgroundColor = [UIColor scrollViewTexturedBackgroundColor]; [curtainView.layer addSublayer: leftPageShadowLayer]; [curtainView.layer addSublayer: rightPageShadowLayer]; [leftPageShadowLayer addSublayer: leftPage]; [rightPageShadowLayer addSublayer: rightPage]; UITapGestureRecognizer * foldTap = [[UITapGestureRecognizer alloc] initWithTarget: action autonome: @selector (fold :)]; [auto.view addGestureRecognizer: foldTap]; UITapGestureRecognizer * unfoldTap = [[UITapGestureRecognizer alloc] initWithTarget: action autonome: @selector (unfold :)]; unfoldTap.numberOfTouchesRequired = 2; [auto.view addGestureRecognizer: unfoldTap]; 

Vous devez également ajouter les deux variables d'instance correspondant aux deux couches d'ombre. Modifier le code au début de la @la mise en oeuvre section à lire:

 @implementation ViewController CALayer * leftPage; CALayer * rightPage; UIView * curtainView; CAShapeLayer * leftPageShadowLayer; CAShapeLayer * rightPageShadowLayer; 

Sur la base de notre discussion précédente, vous devriez trouver le code simple à suivre.

Rappelez-vous que j'ai déjà mentionné que les couches contenant le contenu dessiné seraient contenues en tant que sous-couches de la couche génératrice d'ombres. Par conséquent, nous devons modifier notre plier: et se dérouler: méthodes pour effectuer la transformation requise sur les couches d'ombre.

 - (void) fold: (UITapGestureRecognizer *) gr // dessinant le bitmap "incrementalImage" dans nos calques CGImageRef imgRef = ((CanvasView *) self.view) .incrementalImage.CGImage; leftPage.contents = (__bridge id) imgRef; rightPage.contents = (__bridge id) imgRef; leftPage.contentsRect = CGRectMake (0,0, 0,0, 0,5, 1,0); // ce rectangle représente la moitié gauche de l'image rightPage.contentsRect = CGRectMake (0.5, 0.0, 0.5, 1.0); // ce rectangle représente la moitié droite de l'image leftPageShadowLayer.transform = CATransform3DScale (leftPageShadowLayer.transform, 0.95, 0.95, 0.95); rightPageShadowLayer.transform = CATransform3DScale (rightPageShadowLayer.transform, 0.95, 0.95, 0.95); leftPageShadowLayer.transform = CATransform3DRotate (leftPageShadowLayer.transform, D2R (7.5), 0,0, 1,0, 0,0); rightPageShadowLayer.transform = CATransform3DRotate (rightPageShadowLayer.transform, D2R (-7,5), 0,0, 1,0, 0,0); [auto.view addSubview: curtainView];  - (void) unfold: (UITapGestureRecognizer *) gr leftPageShadowLayer.transform = CATransform3DIdentity; rightPageShadowLayer.transform = CATransform3DIdentity; leftPageShadowLayer.transform = makePerspectiveTransform (); // commente plus tard rightPageShadowLayer.transform = makePerspectiveTransform (); // commenter plus tard [curtainView removeFromSuperview]; 

Construisez et exécutez l'application pour consulter nos pages avec des coins arrondis et des ombres!

Comme auparavant, des tapotements à un doigt entraînent le pliage du livre par incréments, tandis qu'un tapotement à deux doigts supprime l'effet et restaure l'application dans son mode de dessin normal..


Incorporer le pliage par pincement

Visuellement, les choses s'annoncent bien, mais le robinet n'est pas vraiment un geste réaliste pour une métaphore de pliage de livre. Si nous pensons aux applications iPad comme Papier, le pliage et le déploiement se font par un geste de pincement. Appliquons cela maintenant!

Implémentez la méthode suivante dans ViewController.m:

 - (void) foldWithPinch: (UIPinchGestureRecognizer *) p if (p.state == UIGestureRecognizerStateBegan) //… (A) self.view.userInteractionEnabled = NO; CGImageRef imgRef = ((CanvasView *) self.view) .incrementalImage.CGImage; leftPage.contents = (__bridge id) imgRef; rightPage.contents = (__bridge id) imgRef; leftPage.contentsRect = CGRectMake (0,0, 0,0, 0,5, 1,0); rightPage.contentsRect = CGRectMake (0,5, 0,0, 0,5, 1,0); leftPageShadowLayer.transform = CATransform3DIdentity; rightPageShadowLayer.transform = CATransform3DIdentity; leftPageShadowLayer.transform = makePerspectiveTransform (); rightPageShadowLayer.transform = makePerspectiveTransform (); [auto.view addSubview: curtainView];  float scale = p.scale> 0.48? échelle de grandeur: 0,48; //… (B) scale = scale < 1.0 ? scale : 1.0; // SOME CODE WILL GO HERE (1) leftPageShadowLayer.transform = CATransform3DIdentity; rightPageShadowLayer.transform = CATransform3DIdentity; leftPageShadowLayer.transform = makePerspectiveTransform(); rightPageShadowLayer.transform = makePerspectiveTransform(); leftPageShadowLayer.transform = CATransform3DScale(leftPageShadowLayer.transform, scale, scale, scale); // (C) rightPageShadowLayer.transform = CATransform3DScale(rightPageShadowLayer.transform, scale, scale, scale); leftPageShadowLayer.transform = CATransform3DRotate(leftPageShadowLayer.transform, (1.0 - scale) * M_PI, 0.0, 1.0, 0.0); rightPageShadowLayer.transform = CATransform3DRotate(rightPageShadowLayer.transform, -(1.0 - scale) * M_PI, 0.0, 1.0, 0.0); // SOME CODE WILL GO HERE (2) if (p.state == UIGestureRecognizerStateEnded) //… (C)  // SOME CODE CHANGES HERE LATER (3) self.view.userInteractionEnabled = YES; [curtainView removeFromSuperview];  

Une brève explication concernant le code, en ce qui concerne les étiquettes A, B et C mentionnées dans le code:

  1. Lorsque le geste de pincement est reconnu (indiqué par son Etat propriété prenant la valeur UIGestureRecognizerStateBegan) nous commençons à préparer l’animation du pli. La déclaration self.view.userInteractionEnabled = NO; garantit que les éventuelles touches supplémentaires effectuées pendant le geste de pincement ne feront pas que le dessin se produise sur la vue de la toile. Le code restant devrait vous être familier. Nous sommes en train de réinitialiser la couche transforme.
  2. le échelle propriété du pincement détermine le rapport entre la distance entre les doigts et le début du pincement. J'ai décidé de fixer la valeur que nous utiliserons pour calculer les transformations d'échelle et de rotation de nos pages entre 0,48. La condition L’échelle est telle qu’un "pincement inversé" (l’utilisateur écarte ses doigts plus loin que le début du pincement, correspondant à échelle> 1,0) n'a aucun effet. La condition échelle> 0.48 Ainsi, lorsque la distance entre les doigts devient environ la moitié de ce qu’elle était au début du pincement, notre animation de pliage s’achève et aucun pincement supplémentaire n’a d’effet. J'ai choisi 0,48 au lieu de 0,50 en raison de la manière dont je calcule l'angle de rotation de la transformation en rotation du calque. Avec une valeur de 0,48, l'angle de rotation sera légèrement inférieur à 90 degrés. Le livre ne se pliera donc pas complètement et ne deviendra donc pas invisible..
  3. Une fois que l'utilisateur a terminé le pincement, nous supprimons la vue présentant nos couches animées de la vue de la toile (comme auparavant) et nous restaurons l'interactivité de la toile..

Ajoutez le code pour ajouter un identifiant de pincement à la fin de viewDidAppear:

 UIPinchGestureRecognizer * pinch = [[UIPinchGestureRecognizer alloc]] initWithTarget: action autonome: @selector (foldWithPinch :)]; [auto.view addGestureRecognizer: pinch];

Vous pouvez vous débarrasser de tout le code lié aux deux dispositifs de reconnaissance du robinet de ViewController.m parce que nous n'en aurons plus besoin.

Construire et exécuter. Si vous testez sur le simulateur plutôt que sur le périphérique, rappelez-vous que vous devez maintenir la touche Option enfoncée pour simuler les gestes à deux doigts, tels que le pincement..

En principe, le dépliage par pliage à base de pincement fonctionne, mais vous remarquerez certainement (en particulier si vous testez sur un périphérique physique) que l'animation est lente et traîne derrière le pincement, affaiblissant ainsi l'illusion que le pincement anime l’animation de pliage-dépliage. Alors que se passe-t-il?


Obtenir les animations correctement

Rappelez-vous l'animation implicite de la transformation que nous appréciions "gratuitement" lorsque l'animation était contrôlée par le tap? Eh bien, il s'avère que la même animation implicite est devenue un obstacle maintenant! En règle générale, lorsque nous souhaitons contrôler une animation par un geste, par exemple, en déplaçant une vue sur l'écran avec un geste de déplacement (ou de la casse avec notre application ici), nous voulons annuler les animations implicites. Faisons en sorte de comprendre pourquoi, dans le cas d'un utilisateur qui fait glisser une vue sur l'écran avec son doigt:

  1. le vue est en position p0 pour commencer (i.e. view.position = p0 où p0 = (x0, y0) et est un CGPoint)
  2. Lorsque UIKit échantillonne ensuite le contact de l'utilisateur sur l'écran, il déplace son doigt vers une autre position, par exemple p1.
  3. L’animation implicite démarre et le système d’animation commence à animer le changement de position de vue de p0 à p1 avec la durée "détendue" de 0,25 seconde. Cependant, l'animation a à peine commencé et l'utilisateur a déjà fait glisser son doigt vers une nouvelle position., p2. L'animation doit être annulée et une nouvelle commencée vers la position p2.
  4. … et ainsi de suite!

Comme le geste panoramique de l'utilisateur (dans notre exemple hypothétique) est en réalité un geste continu, il suffit de changer la position de la vue au fur et à mesure du geste de l'utilisateur pour conserver l'illusion d'une animation de réponse! C'est exactement le même argument qui s'applique à notre page avec pli ici. J'ai seulement mentionné l'exemple traînant parce qu'il semblait plus simple d'expliquer ce qui se passait.

Il existe différentes manières de désactiver les animations implicites. Nous choisirons le plus simple, qui consiste à envelopper notre code dans un CATransaction bloquer et invoquer la méthode de classe [CATransaction setDisableActions: OUI]. Alors, quel est un CATransaction en tous cas? En termes simples, CATransaction "regroupe" les modifications de propriétés qui doivent être animées et met à jour leurs valeurs à l'écran, en prenant en charge tous les aspects temporels. En effet, il fait tout le travail difficile lié au rendu de nos animations à l'écran pour nous! Même si nous n'avons pas encore utilisé explicitement une transaction d'animation, une transaction implicite est toujours présente lorsqu'un code lié à une animation est exécuté. Ce que nous devons faire maintenant, c’est de terminer l’animation avec un programme personnalisé. CATransaction bloc.

dans le pinchToFold: méthode, ajoutez ces lignes:

 [CATransaction commence]; [CATransaction setDisableActions: YES];

Sur le site du commentaire // Certains codes iront ici (1),
ajouter la ligne:

 [Validation CATransaction];

Si vous construisez et exécutez l'application maintenant, vous remarquerez que le dépliage est beaucoup plus fluide et réactif.!

Un autre problème lié à l'animation auquel nous devons nous attaquer est que notre déploiement: méthode n'anime pas la restauration du livre à son état aplati lorsque le geste de pincement se termine. Vous pouvez vous plaindre que dans notre code, nous n'avons pas pris la peine de revenir en arrière. transformer dans le bloc "then" de notre if (p.state == UIGestureRecognizerStateEnded) déclaration. Mais vous pouvez essayer d'insérer les instructions qui réinitialisent la transformation de la couche avant l'instruction. [canvasView removeFromSuperview]; pour voir si les couches animent ce changement de propriété (spoiler: elles ne le feront pas!).

La raison pour laquelle les couches n'animeront pas le changement de propriété est que tout changement de propriété que nous pourrions effectuer dans ce bloc de code serait regroupé dans le même (implicite) CATransaction en tant que code permettant de supprimer la vue d'hébergement de couche (canvasView) de l'écran. La suppression de la vue se produirait immédiatement - ce n'est pas un changement animé, après tout - et aucune animation dans les sous-vues (ou les sous-couches ajoutées à son calque) ne se produirait..

Encore une fois, un explicite CATransaction bloc vient à notre secours! UNE CATransaction a un bloc d'achèvement qui n'est exécuté qu'après les modifications de propriétés qui apparaissent après l'animation.

Changer le code après le if (p.state == UIGestureRecognizerStateEnded) clause, de sorte que la déclaration if se lit comme suit:

 if (p.state == UIGestureRecognizerStateEnded) [CATransaction begin]; [CATransaction setCompletionBlock: ^ self.view.userInteractionEnabled = YES; [curtainView removeFromSuperview]; ]; [CATransaction setAnimationDuration: 0.5 / scale]; leftPageShadowLayer.transform = CATransform3DIdentity; rightPageShadowLayer.transform = CATransform3DIdentity; [Validation CATransaction]; 

Notez que j’ai décidé de modifier la durée de l’animation inversement par rapport à la échelle de sorte que plus le degré de pli est grand au moment où le geste se termine, plus l'animation de retour doit prendre du temps.

La chose cruciale à comprendre ici est que seulement après la transformer les changements ont animé le code dans le achèvementBloc exécuter. le CATransaction La classe a d’autres propriétés que vous pouvez utiliser pour configurer une animation exactement comme vous le souhaitez. Je vous suggère de consulter la documentation pour plus d'informations..

Construire et exécuter. Enfin, notre animation est non seulement esthétique mais répond également aux interactions de l'utilisateur.!


Conclusion

J'espère que ce didacticiel vous a convaincu que les couches d'animation principales sont un choix réaliste pour obtenir des effets et des animations 3D assez sophistiqués avec un minimum d'effort. Avec quelques optimisations, vous devriez pouvoir animer quelques centaines de couches en même temps sur l’écran en cas de besoin. Un bon exemple d'utilisation de Core Animation consiste à incorporer une transition 3D cool lors du passage d'une vue à une autre dans votre application. Je pense aussi que Core Animation pourrait être une option viable pour créer des jeux simples à base de mots ou de cartes..

Bien que notre tutoriel couvre deux parties, nous avons à peine rayé la surface des calques et des animations (mais j'espère que c'est une bonne chose!). Il y a d'autres sortes d'intéressant CALayer sous-classes que nous n'avons pas eu la chance de regarder. L'animation est un sujet énorme en soi. Je recommande de regarder les conférences de la WWDC sur ces sujets, telles que "Animation de base en pratique" (sessions 424 et 424, WWDC 2010), "Essentiels de base pour l'animation" (Session 421, WWDC 2011), "Performances des applications iOS - Graphiques et animations". (Session 238, WWDC 2012) et "Optimisation des performances graphiques et d'animation 2D" (Session 506, WWDC 2012), puis dans la documentation. Heureux apprentissage et rédaction d'applications!