Comment identifier CAAnimation dans le délégué animationDidStop?

102

J'ai eu un problème où j'avais une série de séquences CATransition / CAAnimation qui se chevauchaient, dont j'avais besoin pour effectuer des opérations personnalisées lorsque les animations s'arrêtaient, mais je ne voulais qu'un seul gestionnaire de délégué pour animationDidStop.

Cependant, j'ai eu un problème, il ne semblait pas y avoir de moyen d'identifier de manière unique chaque CATransition / CAAnimation dans le délégué animationDidStop.

J'ai résolu ce problème via le système clé / valeur exposé dans le cadre de CAAnimation.

Lorsque vous démarrez votre animation, utilisez la méthode setValue sur CATransition / CAAnimation pour définir vos identificateurs et valeurs à utiliser lorsque animationDidStop se déclenche:

-(void)volumeControlFadeToOrange
{   
    CATransition* volumeControlAnimation = [CATransition animation];
    [volumeControlAnimation setType:kCATransitionFade];
    [volumeControlAnimation setSubtype:kCATransitionFromTop];
    [volumeControlAnimation setDelegate:self];
    [volumeControlLevel setBackgroundImage:[UIImage imageNamed:@"SpecialVolume1.png"] forState:UIControlStateNormal];
    volumeControlLevel.enabled = true;
    [volumeControlAnimation setDuration:0.7];
    [volumeControlAnimation setValue:@"Special1" forKey:@"MyAnimationType"];
    [[volumeControlLevel layer] addAnimation:volumeControlAnimation forKey:nil];    
}

- (void)throbUp
{
    doThrobUp = true;

    CATransition *animation = [CATransition animation]; 
    [animation setType:kCATransitionFade];
    [animation setSubtype:kCATransitionFromTop];
    [animation setDelegate:self];
    [hearingAidHalo setBackgroundImage:[UIImage imageNamed:@"m13_grayglow.png"] forState:UIControlStateNormal];
    [animation setDuration:2.0];
    [animation setValue:@"Throb" forKey:@"MyAnimationType"];
    [[hearingAidHalo layer] addAnimation:animation forKey:nil];
}

Dans votre délégué animationDidStop:

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag{

    NSString* value = [theAnimation valueForKey:@"MyAnimationType"];
    if ([value isEqualToString:@"Throb"])
    {
       //... Your code here ...
       return;
    }


    if ([value isEqualToString:@"Special1"])
    {
       //... Your code here ...
       return;
    }

    //Add any future keyed animation operations when the animations are stopped.
 }

L'autre aspect de ceci est qu'il vous permet de conserver l'état dans le système d'appariement clé-valeur au lieu d'avoir à le stocker dans votre classe de délégué. Moins il y a de code, mieux c'est.

N'oubliez pas de consulter la référence Apple sur le codage des paires de valeurs clés .

Existe-t-il de meilleures techniques pour l'identification CAAnimation / CATransition dans le délégué animationDidStop?

Merci, --Batgar

Batgar
la source
4
Batgar, quand j'ai cherché sur Google "iphone animationDidStop identifier", le premier hit était votre message, suggérant l'utilisation de la valeur-clé pour identifier l'animation. Juste ce dont j'avais besoin, merci. Rudi
rudifa
1
Sachez que CAAnimationl » delegateest forte, de sorte que vous pourriez avoir besoin de le mettre à niléviter de conserver les cycles!
Iulian Onofrei

Réponses:

92

La technique de Batgar est trop compliquée. Pourquoi ne pas profiter du paramètre forKey dans addAnimation? Il était destiné précisément à cette fin. Prenez simplement l'appel à setValue et déplacez la chaîne de clé vers l'appel addAnimation. Par exemple:

[[hearingAidHalo layer] addAnimation:animation forKey:@"Throb"];

Ensuite, dans votre rappel animationDidStop, vous pouvez faire quelque chose comme:

if (theAnimation == [[hearingAidHalo layer] animationForKey:@"Throb"]) ...
vocaro
la source
Je tiens à mentionner qu'en utilisant les INCREMENTS ci-dessus, LE COMPTE DE RETENUE! Être averti. Autrement dit, animationForKey: incrémente le nombre de rétention de votre objet CAAnimation.
mmilo
1
@mmilo Ce n'est pas très surprenant, n'est-ce pas? En ajoutant une animation à un calque, le calque est propriétaire de l'animation, de sorte que le nombre de rétention de l'animation est bien sûr incrémenté.
GorillaPatch
16
Ne fonctionne pas - au moment où le sélecteur d'arrêt est appelé, l'animation n'existe plus. Vous obtenez une référence nulle.
Adam le
4
C'est une mauvaise utilisation du paramètre forKey:, et ce n'est pas nécessaire. Ce que Batgar faisait est tout à fait juste - le codage clé-valeur vous permet d'attacher des données arbitraires à votre animation, afin que vous puissiez facilement l'identifier.
mat
7
Adam, voyez la réponse de Jimt ci-dessous - vous devez régler anim.removedOnCompletion = NO;pour qu'il existe toujours quand il -animationDidStop:finished:est appelé.
Yang Meyer
46

Je viens de trouver un moyen encore meilleur de créer un code de complétion pour CAAnimations:

J'ai créé un typedef pour un bloc:

typedef void (^animationCompletionBlock)(void);

Et une clé que j'utilise pour ajouter un bloc à une animation:

#define kAnimationCompletionBlock @"animationCompletionBlock"

Ensuite, si je veux exécuter un code de fin d'animation après la fin d'une CAAnimation, je me définis comme délégué de l'animation et j'ajoute un bloc de code à l'animation en utilisant setValue: forKey:

animationCompletionBlock theBlock = ^void(void)
{
  //Code to execute after the animation completes goes here    
};
[theAnimation setValue: theBlock forKey: kAnimationCompletionBlock];

Ensuite, j'implémente une méthode animationDidStop: done:, qui vérifie un bloc à la clé spécifiée et l'exécute si elle est trouvée:

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
  animationCompletionBlock theBlock = [theAnimation valueForKey: kAnimationCompletionBlock];
  if (theBlock)
    theBlock();
}

La beauté de cette approche est que vous pouvez écrire le code de nettoyage au même endroit où vous créez l'objet d'animation. Mieux encore, puisque le code est un bloc, il a accès aux variables locales dans la portée englobante dans laquelle il est défini. Vous n'avez pas à vous soucier de la configuration des dictionnaires userInfo ou d'autres absurdités du genre, et vous n'avez pas à écrire une animationDidStop: terminée: une méthode qui devient de plus en plus complexe à mesure que vous ajoutez différents types d'animations.

À vrai dire, CAAnimation devrait avoir une propriété de bloc de complétion intégrée et une prise en charge système pour l'appeler automatiquement si elle est spécifiée. Cependant, le code ci-dessus vous offre la même fonctionnalité avec seulement quelques lignes de code supplémentaire.

Duncan C
la source
7
Quelqu'un a également créé une catégorie sur CAAnimation pour ceci: github.com/xissburg/CAAnimationBlocks
Jay Peyer
Cela ne semble pas correct. Assez souvent, j'obtiens un EXEC_Err juste après avoir theBlock();été invoqué, et je pense que cela est dû au fait que la portée du bloc a été détruite.
mahboudz
J'utilise le bloc depuis un certain temps, et il fonctionne BEAUCOUP mieux que la terrible approche "officielle" d'Apple.
Adam
3
Je suis assez sûr que vous auriez besoin d'un pour [bloquer la copie] ce bloc avant de le définir comme valeur pour une propriété.
Fiona Hopkins
1
Non, vous n'avez pas besoin de copier le bloc.
Duncan C
33

La deuxième approche ne fonctionnera que si vous définissez explicitement votre animation pour ne pas être supprimée à la fin avant de l'exécuter:

CAAnimation *anim = ...
anim.removedOnCompletion = NO;

Si vous ne le faites pas, votre animation sera supprimée avant la fin, et le rappel ne la trouvera pas dans le dictionnaire.

Jimt
la source
10
Cela devrait être un commentaire, pas une réponse.
Jusqu'au
2
Je me demande s'il est nécessaire de le supprimer explicitement par la suite avec removeAnimationForKey?
bompf
Cela dépend vraiment de ce que vous voulez faire. Vous pouvez le supprimer si nécessaire ou le laisser parce que vous voulez faire autre chose en tandem.
applejack42
31

Toutes les autres réponses sont bien trop compliquées! Pourquoi n'ajoutez-vous pas simplement votre propre clé pour identifier l'animation?

Cette solution est très simple, il vous suffit d' ajouter votre propre clé à l'animation (animationID dans cet exemple)

Insérez cette ligne pour identifier l' animation1 :

[myAnimation1 setValue:@"animation1" forKey:@"animationID"];

et ceci pour identifier l' animation2 :

[myAnimation2 setValue:@"animation2" forKey:@"animationID"];

Testez-le comme ceci:

- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)flag
{
    if([[animation valueForKey:@"animationID"] isEqual:@"animation1"]) {
    //animation is animation1

    } else if([[animation valueForKey:@"animationID"] isEqual:@"animation2"]) {
    //animation is animation2

    } else {
    //something else
    }
}

Il ne nécessite aucune variable d'instance :

Tibidabo
la source
Je reçois une valeur int (int (0)) dans animationDidStop as[animation valueForKey:@"animationID"]
abhimuralidharan
14

Pour rendre explicite ce qui est sous-entendu d'en haut (et ce qui m'a amené ici après quelques heures perdues): ne vous attendez pas à voir l'objet d'animation original que vous avez alloué vous être renvoyé par

 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)flag 

lorsque l'animation se termine, car [CALayer addAnimation:forKey:]fait une copie de votre animation.

Ce sur quoi vous pouvez compter, c'est que les valeurs clés que vous avez données à votre objet d'animation sont toujours là avec une valeur équivalente (mais pas nécessairement une équivalence de pointeur) dans l'objet d'animation de réplique transmis avec le animationDidStop:finished:message. Comme mentionné ci-dessus, utilisez KVC et vous obtenez une grande latitude pour stocker et récupérer l'état.

t0rst
la source
1
+1 C'est la meilleure solution! Vous pouvez définir le «nom» de l'animation avec [animation setValue:@"myanim" forKey:@"name"]et vous pouvez même définir le calque à animer à l'aide de [animation setValue:layer forKey:@"layer"]. Ces valeurs peuvent ensuite être récupérées dans les méthodes déléguées.
trojanfoe
valueForKey:revient nilpour moi, une idée pourquoi?
Iulian Onofrei
@IulianOnofrei vérifie que votre animation n'a pas été déplacée par une autre animation pour la même propriété - peut se produire comme un effet secondaire inattendu.
t0rst
@ t0rst, Désolé, ayant plusieurs animations et utilisant le copier-coller, je définissais des valeurs différentes sur la même variable d'animation.
Iulian Onofrei
2

Je peux voir la plupart des réponses objc, je vais en faire une pour swift 2.3 basé sur la meilleure réponse ci-dessus.

Pour commencer, il sera bon de stocker toutes ces clés sur une structure privée afin que le type soit sûr et que le changer à l'avenir ne vous apportera pas de bugs ennuyeux simplement parce que vous avez oublié de le changer partout dans le code:

private struct AnimationKeys {
    static let animationType = "animationType"
    static let volumeControl = "volumeControl"
    static let throbUp = "throbUp"
}

Comme vous pouvez le voir, j'ai changé les noms des variables / animations pour que ce soit plus clair. Maintenant, définissez ces clés lorsque l'animation est créée.

volumeControlAnimation.setValue(AnimationKeys.volumeControl, forKey: AnimationKeys.animationType)

(...)

throbUpAnimation.setValue(AnimationKeys.throbUp, forKey: AnimationKeys.animationType)

Puis enfin gérer le délégué pour quand l'animation s'arrête

override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
    if let value = anim.valueForKey(AnimationKeys.animationType) as? String {
        if value == AnimationKeys.volumeControl {
            //Do volumeControl handling
        } else if value == AnimationKeys.throbUp {
            //Do throbUp handling
        }
    }
}
Apinho
la source
0

À mon humble avis, l'utilisation de la valeur-clé d'Apple est la manière élégante de le faire: elle est spécifiquement destinée à permettre l'ajout de données spécifiques à une application à des objets.

Une autre possibilité beaucoup moins élégante est de stocker des références à vos objets d'animation et de faire une comparaison de pointeurs pour les identifier.

Teemu Kurppa
la source
Cela ne fonctionnera jamais - vous ne pouvez pas faire d'équivalence de pointeur, car Apple modifie le pointeur.
Adam le
0

Pour que je vérifie si 2 objets CABasicAnimation sont la même animation, j'utilise la fonction keyPath pour faire exactement comme ça.

if ([animationA keyPath] == [animationB keyPath])

  • Il n'est pas nécessaire de définir KeyPath pour CABasicAnimation car il ne s'animera plus
Sirisilp Kongsilp
la source
la question concerne les rappels délégués, et keyPath n'est pas une méthode sur CAAnimation
Max MacLeod
0

J'aime utiliser setValue:forKey: pour garder une référence de la vue que j'anime, c'est plus sûr que d'essayer d'identifier de manière unique l'animation en fonction de l'ID car le même type d'animation peut être ajouté à différentes couches.

Ces deux sont équivalents:

[UIView animateWithDuration: 0.35
                 animations: ^{
                     myLabel.alpha = 0;
                 } completion: ^(BOOL finished) {
                     [myLabel removeFromSuperview];
                 }];

avec celui-ci:

CABasicAnimation *fadeOut = [CABasicAnimation animationWithKeyPath:@"opacity"];
fadeOut.fromValue = @([myLabel.layer opacity]);
fadeOut.toValue = @(0.0);
fadeOut.duration = 0.35;
fadeOut.fillMode = kCAFillModeForwards;
[fadeOut setValue:myLabel forKey:@"item"]; // Keep a reference to myLabel
fadeOut.delegate = self;
[myLabel.layer addAnimation:fadeOut forKey:@"fadeOut"];
myLabel.layer.opacity = 0;

et dans la méthode déléguée:

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
    id item = [anim valueForKey:@"item"];

    if ([item isKindOfClass:[UIView class]])
    {
        // Here you can identify the view by tag, class type 
        // or simply compare it with a member object

        [(UIView *)item removeFromSuperview];
    }
}
Andrei Marincas
la source
0

Xcode 9 Swift 4.0

Vous pouvez utiliser des valeurs clés pour associer une animation que vous avez ajoutée à l'animation renvoyée dans la méthode déléguée animationDidStop.

Déclarez un dictionnaire contenant toutes les animations actives et les complétions associées:

 var animationId: Int = 1
 var animating: [Int : () -> Void] = [:]

Lorsque vous ajoutez votre animation, définissez-lui une clé:

moveAndResizeAnimation.setValue(animationId, forKey: "CompletionId")
animating[animationId] = {
    print("completion of moveAndResize animation")
}
animationId += 1    

Dans animationDidStop, la magie opère:

    let animObject = anim as NSObject
    if let keyValue = animObject.value(forKey: "CompletionId") as? Int {
        if let completion = animating.removeValue(forKey: keyValue) {
            completion()
        }
    }
Eng If
la source