Comment détecter quand quelqu'un secoue un iPhone?

340

Je veux réagir quand quelqu'un secoue l'iPhone. Je ne me soucie pas particulièrement de la façon dont ils le secouent, juste qu'il a été agité vigoureusement pendant une fraction de seconde. Quelqu'un sait-il comment détecter cela?

Josh Gagnon
la source

Réponses:

292

Dans 3.0, il existe désormais un moyen plus simple: connectez-vous aux nouveaux événements de mouvement.

L'astuce principale est que vous devez avoir certains UIView (pas UIViewController) que vous souhaitez que firstResponder reçoive les messages d'événement shake. Voici le code que vous pouvez utiliser dans n'importe quelle UIView pour obtenir des événements de secousse:

@implementation ShakingView

- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
    if ( event.subtype == UIEventSubtypeMotionShake )
    {
        // Put in code here to handle shake
    }

    if ( [super respondsToSelector:@selector(motionEnded:withEvent:)] )
        [super motionEnded:motion withEvent:event];
}

- (BOOL)canBecomeFirstResponder
{ return YES; }

@end

Vous pouvez facilement transformer n'importe quelle UIView (même les vues système) en une vue qui peut obtenir l'événement shake simplement en sous-classant la vue avec uniquement ces méthodes (puis en sélectionnant ce nouveau type au lieu du type de base dans IB, ou en l'utilisant lors de l'allocation d'un vue).

Dans le contrôleur de vue, vous souhaitez définir cette vue pour devenir le premier répondant:

- (void) viewWillAppear:(BOOL)animated
{
    [shakeView becomeFirstResponder];
    [super viewWillAppear:animated];
}
- (void) viewWillDisappear:(BOOL)animated
{
    [shakeView resignFirstResponder];
    [super viewWillDisappear:animated];
}

N'oubliez pas que si vous avez d'autres vues qui deviennent le premier répondant des actions de l'utilisateur (comme une barre de recherche ou un champ de saisie de texte), vous devrez également restaurer l'état de premier répondant de la vue tremblante lorsque l'autre vue démissionne!

Cette méthode fonctionne même si vous définissez applicationSupportsShakeToEdit sur NO.

Kendall Helmstetter Gelner
la source
5
Cela ne fonctionnait pas tout à fait pour moi: j'avais besoin de remplacer mon contrôleur au viewDidAppearlieu de viewWillAppear. Je ne sais pas pourquoi; peut-être que la vue doit être visible avant de pouvoir faire ce qu'elle fait pour commencer à recevoir les événements de tremblement?
Kristopher Johnson
1
C'est plus facile mais pas nécessairement meilleur. Si vous souhaitez détecter un tremblement long et continu, cette approche n'est pas utile car l'iPhone aime tirer motionEndedavant que le tremblement ne se soit réellement arrêté. Donc, en utilisant cette approche, vous obtenez une série décousue de secousses courtes au lieu d'une longue. L'autre réponse fonctionne beaucoup mieux dans ce cas.
2011
3
@Kendall - UIViewControllers implémente UIResponder et fait partie de la chaîne de répondeurs. La fenêtre UIWindow la plus élevée l'est également. developer.apple.com/library/ios/DOCUMENTATION/EventHandling/…
DougW
1
[super respondsToSelector:ne fera pas ce que vous voulez, car c'est équivalent à appeler [self respondsToSelector:qui reviendra YES. Ce dont vous avez besoin, c'est [[ShakingView superclass] instancesRespondToSelector:.
Vadim Yelagin
2
Au lieu d'utiliser: if (event.subtype == UIEventSubtypeMotionShake), vous pouvez utiliser: if (motion == UIEventSubtypeMotionShake)
Hashim Akhtar
179

Depuis mon application Diceshaker :

// Ensures the shake is strong enough on at least two axes before declaring it a shake.
// "Strong enough" means "greater than a client-supplied threshold" in G's.
static BOOL L0AccelerationIsShaking(UIAcceleration* last, UIAcceleration* current, double threshold) {
    double
        deltaX = fabs(last.x - current.x),
        deltaY = fabs(last.y - current.y),
        deltaZ = fabs(last.z - current.z);

    return
        (deltaX > threshold && deltaY > threshold) ||
        (deltaX > threshold && deltaZ > threshold) ||
        (deltaY > threshold && deltaZ > threshold);
}

@interface L0AppDelegate : NSObject <UIApplicationDelegate> {
    BOOL histeresisExcited;
    UIAcceleration* lastAcceleration;
}

@property(retain) UIAcceleration* lastAcceleration;

@end

@implementation L0AppDelegate

- (void)applicationDidFinishLaunching:(UIApplication *)application {
    [UIAccelerometer sharedAccelerometer].delegate = self;
}

- (void) accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {

    if (self.lastAcceleration) {
        if (!histeresisExcited && L0AccelerationIsShaking(self.lastAcceleration, acceleration, 0.7)) {
            histeresisExcited = YES;

            /* SHAKE DETECTED. DO HERE WHAT YOU WANT. */

        } else if (histeresisExcited && !L0AccelerationIsShaking(self.lastAcceleration, acceleration, 0.2)) {
            histeresisExcited = NO;
        }
    }

    self.lastAcceleration = acceleration;
}

// and proper @synthesize and -dealloc boilerplate code

@end

L'histérèse empêche l'événement de tremblement de se déclencher plusieurs fois jusqu'à ce que l'utilisateur arrête le tremblement.

millenomi
la source
7
Remarque J'ai ajouté une réponse ci-dessous présentant une méthode plus simple pour 3.0.
Kendall Helmstetter Gelner le
2
Que se passe-t-il s'ils le secouent précisément sur un axe particulier?
jprete
5
La meilleure réponse, car l'iOS 3.0 motionBeganet les motionEndedévénements ne sont pas très précis ni précis en termes de détection du début et de la fin exacts d'un tremblement. Cette approche vous permet d'être aussi précis que vous le souhaitez.
2011
2
UIAccelerometerDelegate accéléromètre: didAccelerate: est déconseillé dans iOS5.
Yantao Xie
Cette autre réponse est meilleure et plus rapide à implémenter stackoverflow.com/a/2405692/396133
Abramodj
154

Je l'ai finalement fait fonctionner en utilisant des exemples de code de ce didacticiel Undo / Redo Manager .
C'est exactement ce que vous devez faire:

  • Définissez la propriété applicationSupportsShakeToEdit dans le délégué de l'application:
  • 
        - (void)applicationDidFinishLaunching:(UIApplication *)application {
    
            application.applicationSupportsShakeToEdit = YES;
    
            [window addSubview:viewController.view];
            [window makeKeyAndVisible];
    }
    

  • Ajoutez / remplacez les méthodes canBecomeFirstResponder , viewDidAppear: et viewWillDisappear: dans votre View Controller:
  • 
    -(BOOL)canBecomeFirstResponder {
        return YES;
    }
    
    -(void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
        [self becomeFirstResponder];
    }
    
    - (void)viewWillDisappear:(BOOL)animated {
        [self resignFirstResponder];
        [super viewWillDisappear:animated];
    }
    

  • Ajoutez la méthode motionEnded à votre contrôleur de vue:
  • 
    - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
    {
        if (motion == UIEventSubtypeMotionShake)
        {
            // your code
        }
    }
    
    Eran Talmor
    la source
    Juste pour ajouter à la solution Eran Talmor: vous devez utiliser un contrôleur de navigation au lieu d'un contrôleur de vue si vous avez choisi une telle application dans le sélecteur de nouveau projet.
    Rob
    J'ai essayé d'utiliser cela, mais parfois de fortes secousses ou des tremblements légers, cela s'est simplement arrêté
    vinothp
    L'étape 1 est désormais redondante, car la valeur par défaut de applicationSupportsShakeToEditest YES.
    DonVaughn
    1
    Pourquoi appeler à l' [self resignFirstResponder];avance [super viewWillDisappear:animated];? Cela semble particulier.
    JaredH
    94

    Tout d'abord, la réponse de Kendall du 10 juillet est parfaite.

    Maintenant ... Je voulais faire quelque chose de similaire (dans l'iPhone OS 3.0+), seulement dans mon cas, je le voulais à l'échelle de l'application afin de pouvoir alerter différentes parties de l'application en cas de tremblement. Voici ce que j'ai fini par faire.

    Tout d'abord, j'ai sous- classé UIWindow . C'est facile. Créez un nouveau fichier de classe avec une interface telle que MotionWindow : UIWindow(n'hésitez pas à choisir le vôtre, natch). Ajoutez une méthode comme ceci:

    - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
        if (event.type == UIEventTypeMotion && event.subtype == UIEventSubtypeMotionShake) {
            [[NSNotificationCenter defaultCenter] postNotificationName:@"DeviceShaken" object:self];
        }
    }
    

    Modifiez @"DeviceShaken"le nom de notification de votre choix. Enregistrez le fichier.

    Maintenant, si vous utilisez un MainWindow.xib (élément de modèle Xcode stock), allez-y et changez la classe de votre objet Window de UIWindow en MotionWindow ou tout ce que vous avez appelé. Enregistrez le xib. Si vous configurez UIWindow par programme, utilisez plutôt votre nouvelle classe Window.

    Maintenant, votre application utilise la classe spécialisée UIWindow . Partout où vous voulez être informé d'une secousse, inscrivez-vous pour les notifications! Comme ça:

    [[NSNotificationCenter defaultCenter] addObserver:self
    selector:@selector(deviceShaken) name:@"DeviceShaken" object:nil];
    

    Pour vous retirer en tant qu'observateur:

    [[NSNotificationCenter defaultCenter] removeObserver:self];

    J'ai mis le mien dans viewWillAppear: et viewWillDisappear: en ce qui concerne les contrôleurs de vue. Assurez-vous que votre réponse à l'événement shake sait s'il est "déjà en cours" ou non. Sinon, si l'appareil est secoué deux fois de suite, vous aurez un embouteillage limité. De cette façon, vous pouvez ignorer les autres notifications jusqu'à ce que vous ayez vraiment fini de répondre à la notification d'origine.

    Aussi: Vous pouvez choisir de repérer motionBegan vs motionEnded . C'est à vous. Dans mon cas, l'effet doit toujours avoir lieu après que l'appareil est au repos (vs quand il commence à trembler), donc j'utilise motionEnded . Essayez les deux et voyez lequel a le plus de sens ... ou détectez / notifiez les deux!

    Une autre observation (curieuse?) Ici: Remarquez qu'il n'y a aucun signe de gestion des premiers intervenants dans ce code. Je n'ai essayé cela qu'avec les contrôleurs de vue de table jusqu'à présent et tout semble fonctionner très bien ensemble! Je ne peux cependant pas garantir d'autres scénarios.

    Kendall, et. al - quelqu'un peut-il expliquer pourquoi cela pourrait être le cas pour les sous-classes UIWindow ? Est-ce parce que la fenêtre est au sommet de la chaîne alimentaire?

    Joe D'Andrea
    la source
    1
    Voilà ce qui est si bizarre. Cela fonctionne tel quel. Espérons que ce ne soit pas un "travail malgré lui"! Veuillez me faire savoir ce que vous découvrez dans vos propres tests.
    Joe D'Andrea
    Je préfère cela à la sous-classe d'une UIView régulière, d'autant plus que vous n'avez besoin que de cela pour exister en un seul endroit, donc mettre dans une sous-classe de fenêtre semble un choix naturel.
    Shaggy Frog
    Merci jdandrea, j'utilise ta méthode mais en secouant plusieurs fois !!! je veux juste que les utilisateurs puissent secouer une fois (sur une vue que la secousse fait là) ...! comment puis-je le gérer? j'implémente qc comme ça. j'implémente mon code smht comme ceci: i-phone.ir/forums/uploadedpictures/… ---- mais le problème est que l'application ne secoue qu'une seule fois pour la durée de vie de l'application! non seulement voir
    Momi
    1
    Momeks: Si vous avez besoin que la notification se produise une fois que l'appareil est au repos (post-shake), utilisez motionEnded au lieu de motionBegan. Cela devrait faire l'affaire!
    Joe D'Andrea du
    1
    @Joe D'Andrea - Cela fonctionne tel quel car UIWindow fait partie de la chaîne de répondeurs. Si quelque chose plus haut dans la chaîne interceptait et consommait ces événements, il ne les recevrait pas. developer.apple.com/library/ios/DOCUMENTATION/EventHandling/…
    DougW
    34

    Je suis tombé sur ce post à la recherche d'une implémentation "tremblante". La réponse de millenomi a bien fonctionné pour moi, même si je cherchais quelque chose qui nécessitait un peu plus d '"action tremblante" pour se déclencher. J'ai remplacé la valeur booléenne par un entier shakeCount. J'ai également réimplémenté la méthode L0AccelerationIsShaking () dans Objective-C. Vous pouvez modifier la quantité de secousses requise en modifiant la quantité ajoutée à shakeCount. Je ne suis pas sûr d'avoir encore trouvé les valeurs optimales, mais cela semble bien fonctionner jusqu'à présent. J'espère que cela aide quelqu'un:

    - (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
        if (self.lastAcceleration) {
            if ([self AccelerationIsShakingLast:self.lastAcceleration current:acceleration threshold:0.7] && shakeCount >= 9) {
                //Shaking here, DO stuff.
                shakeCount = 0;
            } else if ([self AccelerationIsShakingLast:self.lastAcceleration current:acceleration threshold:0.7]) {
                shakeCount = shakeCount + 5;
            }else if (![self AccelerationIsShakingLast:self.lastAcceleration current:acceleration threshold:0.2]) {
                if (shakeCount > 0) {
                    shakeCount--;
                }
            }
        }
        self.lastAcceleration = acceleration;
    }
    
    - (BOOL) AccelerationIsShakingLast:(UIAcceleration *)last current:(UIAcceleration *)current threshold:(double)threshold {
        double
        deltaX = fabs(last.x - current.x),
        deltaY = fabs(last.y - current.y),
        deltaZ = fabs(last.z - current.z);
    
        return
        (deltaX > threshold && deltaY > threshold) ||
        (deltaX > threshold && deltaZ > threshold) ||
        (deltaY > threshold && deltaZ > threshold);
    }
    

    PS: J'ai défini l'intervalle de mise à jour au 1 / 15e de seconde.

    [[UIAccelerometer sharedAccelerometer] setUpdateInterval:(1.0 / 15)];

    la source
    Comment puis-je détecter le mouvement de l'appareil comme un panorama?
    user2526811
    Comment identifier le tremblement si l'iPhone est déplacé uniquement dans la direction X? Je ne veux pas détecter de tremblement si dans la direction Y ou Z.
    Manthan
    12

    Vous devez vérifier l'accéléromètre via accelerometer: didAccelerate: méthode qui fait partie du protocole UIAccelerometerDelegate et vérifier si les valeurs dépassent un seuil pour la quantité de mouvement nécessaire pour une secousse.

    Il y a un exemple de code décent dans l'accéléromètre: didAccelerate: méthode tout en bas d'AppController.m dans l'exemple GLPaint qui est disponible sur le site des développeurs iPhone.

    Dave Verwer
    la source
    12

    Dans iOS 8.3 (peut-être plus tôt) avec Swift, c'est aussi simple que de remplacer les méthodes motionBeganou motionEndeddans votre contrôleur de vue:

    class ViewController: UIViewController {
        override func motionBegan(motion: UIEventSubtype, withEvent event: UIEvent) {
            println("started shaking!")
        }
    
        override func motionEnded(motion: UIEventSubtype, withEvent event: UIEvent) {
            println("ended shaking!")
        }
    }
    nhgrif
    la source
    Est-ce que tu l'as essayé? Je suppose que pour que cela fonctionne lorsque l'application est en arrière-plan, il faudrait un peu de travail supplémentaire.
    nhgrif
    Non .. sera bientôt .. mais si vous avez remarqué l'iPhone 6s, il a cette fonction "ramasser pour réveiller l'écran", où l'écran s'éclaire pendant un certain temps, lorsque vous prenez l'appareil. Je dois capturer ce comportement
    nr5
    9

    Voici le code délégué de base dont vous avez besoin:

    #define kAccelerationThreshold      2.2
    
    #pragma mark -
    #pragma mark UIAccelerometerDelegate Methods
        - (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration 
        {   
            if (fabsf(acceleration.x) > kAccelerationThreshold || fabsf(acceleration.y) > kAccelerationThreshold || fabsf(acceleration.z) > kAccelerationThreshold) 
                [self myShakeMethodGoesHere];   
        }

    Définissez également le dans le code approprié dans l'interface. c'est à dire:

    @interface MyViewController: UIViewController <UIPickerViewDelegate, UIPickerViewDataSource, UIAccelerometerDelegate>

    Benjamin Ortuzar
    la source
    Comment puis-je détecter le mouvement de l'appareil comme un panorama?
    user2526811
    Comment identifier le tremblement si l'iPhone est déplacé uniquement dans la direction X? Je ne veux pas détecter de tremblement si dans la direction Y ou Z.
    Manthan
    7

    Ajouter les méthodes suivantes dans le fichier ViewController.m, son fonctionnement correctement

        -(BOOL) canBecomeFirstResponder
        {
             /* Here, We want our view (not viewcontroller) as first responder 
             to receive shake event message  */
    
             return YES;
        }
    
        -(void) motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
        {
                if(event.subtype==UIEventSubtypeMotionShake)
                {
                        // Code at shake event
    
                        UIAlertView *alert=[[UIAlertView alloc] initWithTitle:@"Motion" message:@"Phone Vibrate"delegate:self cancelButtonTitle:@"OK" otherButtonTitles: nil];
                        [alert show];
                        [alert release];
    
                        [self.view setBackgroundColor:[UIColor redColor]];
                 }
        }
        - (void)viewDidAppear:(BOOL)animated
        {
                 [super viewDidAppear:animated];
                 [self becomeFirstResponder];  // View as first responder 
         }
    Himanshu Mahajan
    la source
    5

    Désolé de poster ceci comme réponse plutôt que comme commentaire mais comme vous pouvez le voir, je suis nouveau sur Stack Overflow et je ne suis donc pas encore assez réputé pour poster des commentaires!

    Quoi qu'il en soit, j'appuie @cire à m'assurer de définir le statut du premier répondant une fois que la vue fait partie de la hiérarchie des vues. Ainsi, la définition du statut de premier répondant dans votre viewDidLoadméthode de contrôleur de vue ne fonctionnera pas par exemple. Et si vous ne savez pas si cela fonctionne, [view becomeFirstResponder]vous obtenez un booléen que vous pouvez tester.

    Autre point: vous pouvez utiliser un contrôleur de vue pour capturer l'événement shake si vous ne voulez pas créer inutilement une sous-classe UIView. Je sais que ce n'est pas très compliqué, mais l'option est toujours là. Déplacez simplement les extraits de code que Kendall a mis dans la sous-classe UIView dans votre contrôleur et envoyez les messages becomeFirstResponderet resignFirstResponderà la selfplace de la sous-classe UIView.

    Newtz
    la source
    Cela ne fournit pas de réponse à la question. Pour critiquer ou demander des éclaircissements à un auteur, laissez un commentaire sous son article.
    Alex Salauyou
    @SashaSalauyou J'ai clairement déclaré dans la toute première phrase ici que je n'avais pas assez de réputation pour poster un commentaire à l'époque
    Newtz
    Désolé. Il est possible ici
    qu'une
    5

    Tout d'abord, je sais que c'est un ancien poste, mais il est toujours pertinent, et j'ai trouvé que les deux réponses les plus votées n'ont pas détecté la secousse le plus tôt possible . Voici comment faire:

    1. Liez CoreMotion à votre projet dans les phases de construction de la cible: CoreMotion
    2. Dans votre ViewController:

      - (BOOL)canBecomeFirstResponder {
          return YES;
      }
      
      - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
      {
          if (motion == UIEventSubtypeMotionShake) {
              // Shake detected.
          }
      }
    Dennis
    la source
    4

    La solution la plus simple consiste à dériver une nouvelle fenêtre racine pour votre application:

    @implementation OMGWindow : UIWindow
    
    - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
        if (event.type == UIEventTypeMotion && motion == UIEventSubtypeMotionShake) {
            // via notification or something   
        }
    }
    @end

    Ensuite, dans votre délégué d'application:

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        self.window = [[OMGWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
        //…
    }

    Si vous utilisez un Storyboard, cela peut être plus délicat, je ne connais pas précisément le code dont vous aurez besoin dans le délégué d'application.

    mxcl
    la source
    Et oui, vous pouvez dériver votre classe de base @implementationdepuis le Xcode 5.
    mxcl
    Cela fonctionne, je l'utilise dans plusieurs applications. Je ne sais donc pas pourquoi il a été rejeté.
    mxcl
    travaillé comme un charme. Au cas où vous vous poseriez la question, vous pouvez remplacer la classe de fenêtre comme ceci: stackoverflow.com/a/10580083/709975
    Edu
    Est-ce que cela fonctionnera lorsque l'application est en arrière-plan? Si ce n'est pas une autre idée comment détecter en arrière-plan?
    n ° 5
    Cela fonctionnera probablement si vous disposez d'un mode en arrière-plan.
    mxcl
    1

    Utilisez simplement ces trois méthodes pour le faire

    - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event{
    - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event{
    - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event{

    pour plus de détails, vous pouvez vérifier un exemple de code complet là-bas

    Mashhadi
    la source
    1

    Une version swiftease basée sur la toute première réponse!

    override func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?) {
        if ( event?.subtype == .motionShake )
        {
            print("stop shaking me!")
        }
    }
    user3069232
    la source
    -1

    Pour activer cette application à l'échelle, j'ai créé une catégorie sur UIWindow:

    @implementation UIWindow (Utils)
    
    - (BOOL)canBecomeFirstResponder
    {
        return YES;
    }
    
    - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
    {
        if (motion == UIEventSubtypeMotionShake) {
            // Do whatever you want here...
        }
    }
    
    @end
    Mike
    la source