¿Cómo detecto cuando alguien sacude un iPhone?

340

Quiero reaccionar cuando alguien sacude el iPhone. No me importa particularmente cómo lo sacuden, solo que se agitó vigorosamente durante una fracción de segundo. ¿Alguien sabe cómo detectar esto?

Josh Gagnon
fuente

Respuestas:

292

En 3.0, ahora hay una manera más fácil: conectarse a los nuevos eventos de movimiento.

El truco principal es que necesita tener algo de UIView (no UIViewController) que desea como primer respondedor para recibir los mensajes de evento de sacudida. Aquí está el código que puede usar en cualquier UIView para obtener eventos de sacudida:

@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

Puede transformar fácilmente cualquier UIView (incluso vistas del sistema) en una vista que pueda obtener el evento shake simplemente subclasificando la vista solo con estos métodos (y luego seleccionando este nuevo tipo en lugar del tipo base en IB, o usándolo cuando se asigna un ver).

En el controlador de vista, desea configurar esta vista para convertirse en el primer respondedor:

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

¡No olvide que si tiene otras vistas que se convierten en el primer respondedor de las acciones del usuario (como una barra de búsqueda o un campo de entrada de texto) también necesitará restaurar el estado del primer respondedor de la vista temblorosa cuando la otra vista renuncie!

Este método funciona incluso si establece applicationSupportsShakeToEdit en NO.

Kendall Helmstetter Gelner
fuente
55
Esto no funcionó para mí: necesitaba reemplazar mi controlador en viewDidAppearlugar de viewWillAppear. No estoy seguro de por qué; ¿Quizás la vista debe ser visible antes de poder hacer lo que sea para comenzar a recibir los eventos de sacudida?
Kristopher Johnson
1
Esto es más fácil pero no necesariamente mejor. Si desea detectar una sacudida larga y continua, este enfoque no es útil ya que al iPhone le gusta disparar motionEndedantes de que la sacudida se haya detenido. Entonces, usando este enfoque, obtienes una serie desarticulada de batidos cortos en lugar de uno largo. La otra respuesta funciona mucho mejor en este caso.
Aroth
3
@Kendall - UIViewControllers implementan UIResponder y están en la cadena de respuesta. El UIWindow superior también lo es. developer.apple.com/library/ios/DOCUMENTATION/EventHandling/…
DougW
1
[super respondsToSelector:no hará lo que quieras, ya que es equivalente a llamar [self respondsToSelector:y volverá YES. Lo que necesita es [[ShakingView superclass] instancesRespondToSelector:.
Vadim Yelagin
2
En lugar de usar: if (event.subtype == UIEventSubtypeMotionShake) puede usar: if (motion == UIEventSubtypeMotionShake)
Hashim Akhtar
179

Desde mi aplicación 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

La histéresis evita que el evento de sacudida se active varias veces hasta que el usuario detiene la sacudida.

millenomi
fuente
77
Tenga en cuenta que he agregado una respuesta a continuación que presenta un método más fácil para 3.0.
Kendall Helmstetter Gelner
2
¿Qué sucede si lo están sacudiendo precisamente en un eje en particular?
jprete
55
La mejor respuesta, porque el iOS 3.0 motionBegany los motionEndedeventos no son muy precisos o precisos en términos de detectar el inicio y el final exactos de un batido. Este enfoque le permite ser tan preciso como desee.
Aroth
2
UIAccelerometer Acelerador delegado : didAccelerate: ha quedado en desuso en iOS5.
Yantao Xie
Esta otra respuesta es mejor y más rápida de implementar stackoverflow.com/a/2405692/396133
Abramodj
154

Finalmente lo hice funcionar usando ejemplos de código de este Tutorial del Administrador de Deshacer / Rehacer .
Esto es exactamente lo que debes hacer:

  • Establezca la propiedad applicationSupportsShakeToEdit en Delegado de la aplicación:
  • 
        - (void)applicationDidFinishLaunching:(UIApplication *)application {
    
            application.applicationSupportsShakeToEdit = YES;
    
            [window addSubview:viewController.view];
            [window makeKeyAndVisible];
    }

  • Agregar / Anular canBecomeFirstResponder , viewDidAppear: y viewWillDisappear: métodos en su controlador de vista:
  • 
    -(BOOL)canBecomeFirstResponder {
        return YES;
    }
    
    -(void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
        [self becomeFirstResponder];
    }
    
    - (void)viewWillDisappear:(BOOL)animated {
        [self resignFirstResponder];
        [super viewWillDisappear:animated];
    }

  • Agregue el método motionEnded a su controlador de vista:
  • 
    - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
    {
        if (motion == UIEventSubtypeMotionShake)
        {
            // your code
        }
    }
    Eran Talmor
    fuente
    Solo para agregar a la solución Eran Talmor: debe usar un controlador de navegación en lugar de un controlador de vista en caso de que haya elegido dicha aplicación en el nuevo selector de proyectos.
    Rob
    Intenté usar esto, pero a veces el batido fuerte o el batido ligero simplemente se detuvo
    vinothp
    El paso 1 ahora es redundante, ya que el valor predeterminado de applicationSupportsShakeToEdites YES.
    DonVaughn
    1
    ¿Por qué llamar [self resignFirstResponder];antes [super viewWillDisappear:animated];? Esto parece peculiar.
    JaredH
    94

    Primero, la respuesta del 10 de julio de Kendall es acertada.

    Ahora ... quería hacer algo similar (en iPhone OS 3.0+), solo en mi caso lo quería en toda la aplicación para poder alertar a varias partes de la aplicación cuando ocurriera una sacudida. Esto es lo que terminé haciendo.

    Primero, subclasifiqué UIWindow . Esto es fácil, fácil. Cree un nuevo archivo de clase con una interfaz como MotionWindow : UIWindow(siéntase libre de elegir el suyo, natch). Agregue un método como este:

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

    Cambie @"DeviceShaken"al nombre de notificación de su elección. Guarda el archivo.

    Ahora, si usa un MainWindow.xib (material de plantilla Xcode de stock), entre allí y cambie la clase de su objeto Window de UIWindow a MotionWindow o como lo llame. Guarda el xib. Si configura UIWindow mediante programación, use su nueva clase Window allí.

    Ahora su aplicación está utilizando la clase especializada UIWindow . Donde quiera que te informen sobre un batido, ¡regístrate para recibir notificaciones! Me gusta esto:

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

    Para eliminarse como observador:

    [[NSNotificationCenter defaultCenter] removeObserver:self];

    Puse el mío en viewWillAppear: y viewWillDisappear: en lo que respecta a los controladores de vista. Asegúrese de que su respuesta al evento shake sepa si está "en progreso" o no. De lo contrario, si el dispositivo se agita dos veces seguidas, tendrá un pequeño atasco de tráfico. De esta manera, puede ignorar otras notificaciones hasta que haya terminado de responder a la notificación original.

    Además: puede elegir salir de motionBegan vs. motionEnded . Tu decides. En mi caso, el efecto siempre debe tener lugar después de que el dispositivo esté en reposo (frente a cuando comienza a temblar), por lo que utilizo motionEnded . Pruebe ambos y vea cuál tiene más sentido ... ¡o detecte / notifique a ambos!

    Una observación más (¿curiosa?) Aquí: observe que no hay signos de administración de primeros respondedores en este código. ¡Hasta ahora solo he probado esto con Table View Controllers y todo parece funcionar muy bien juntos! Sin embargo, no puedo responder por otros escenarios.

    Kendall, et. ¿Alguien puede hablar de por qué esto podría ser así para las subclases de UIWindow ? ¿Es porque la ventana está en la parte superior de la cadena alimentaria?

    Joe D'Andrea
    fuente
    1
    Eso es lo que es tan raro. Funciona tal como está. ¡Espero que no se trate de "trabajar a pesar de sí mismo"! Por favor, hágame saber lo que descubra en sus propias pruebas.
    Joe D'Andrea
    Prefiero esto a subclasificar una UIView normal, especialmente porque solo necesitas que exista en un lugar, por lo que poner una subclase de ventana parece un ajuste natural.
    Shaggy Frog
    ¡Gracias jdandrea, uso tu método pero me tiemblan varias veces! ¡Solo quiero que los usuarios puedan sacudirse una vez (en una vista que el temblor hace allí) ...! ¿Cómo puedo manejarlo? Implemento algo así. Implemento mi código de la siguiente manera: i-phone.ir/forums/uploadedpictures/… ---- pero el problema es que la aplicación solo se agita 1 vez durante toda la vida de la aplicación. no solo ver
    Momi
    1
    Momeks: si necesita que la notificación se produzca una vez que el dispositivo está en reposo (después de la sacudida), use motionEnded en lugar de motionBegan. ¡Eso debería hacerlo!
    Joe D'Andrea
    1
    @ Joe D'Andrea: funciona tal cual, porque UIWindow está en la cadena de respuesta. Si algo más arriba en la cadena interceptara y consumiera estos eventos, no los recibiría. developer.apple.com/library/ios/DOCUMENTATION/EventHandling/…
    DougW
    34

    Encontré esta publicación buscando una implementación "temblorosa". La respuesta de millenomi funcionó bien para mí, aunque estaba buscando algo que requiriera un poco más de "acción de sacudida" para desencadenarse. He reemplazado el valor booleano con un int shakeCount. También reimplementé el método L0AccelerationIsShaking () en Objective-C. Puede ajustar la cantidad de temblor requerida ajustando la cantidad agregada a shakeCount. No estoy seguro de haber encontrado los valores óptimos todavía, pero parece estar funcionando bien hasta ahora. Espero que esto ayude a alguien:

    - (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);
    }

    PD: he configurado el intervalo de actualización a 1/15 de segundo.

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

    fuente
    ¿Cómo puedo detectar el movimiento del dispositivo como si fuera panorama?
    user2526811
    ¿Cómo identificar el movimiento si el iPhone se mueve solo en dirección X? No quiero detectar temblores en dirección Y o Z.
    Manthan
    12

    Debe verificar el acelerómetro a través del acelerómetro: didAccelerate: método que forma parte del protocolo UIAccelerometerDelegate y verificar si los valores superan un umbral para la cantidad de movimiento necesaria para una sacudida.

    Hay un código de muestra decente en el acelerómetro: didAccelerate: método justo en la parte inferior de AppController.m en el ejemplo de GLPaint que está disponible en el sitio para desarrolladores de iPhone.

    Dave Verwer
    fuente
    12

    En iOS 8.3 (quizás antes) con Swift, es tan simple como anular los métodos motionBegano motionEndeden su controlador de vista:

    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
    fuente
    ¿Lo intentaste? Supongo que hacer que esto funcione cuando la aplicación está en segundo plano requeriría un poco de trabajo extra.
    nhgrif
    No ... pronto ... pero si ha notado que el iPhone 6s tiene esta función de "levantar para activar la pantalla", donde la pantalla se ilumina por algún tiempo, cuando levanta el dispositivo. Necesito capturar este comportamiento
    nr5
    9

    Este es el código de delegado básico que necesita:

    #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];   
        }

    Establezca también en el código apropiado en la interfaz. es decir:

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

    Benjamin Ortuzar
    fuente
    ¿Cómo puedo detectar el movimiento del dispositivo como si fuera panorama?
    user2526811
    ¿Cómo identificar el movimiento si el iPhone se mueve solo en dirección X? No quiero detectar temblores en dirección Y o Z.
    Manthan
    7

    Agregue los siguientes métodos en el archivo ViewController.m, funciona correctamente

        -(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
    fuente
    5

    Lamento publicar esto como una respuesta en lugar de un comentario, pero como puede ver, soy nuevo en Stack Overflow y, por lo tanto, todavía no tengo la reputación suficiente para publicar comentarios.

    De todos modos, secundo a @cire para asegurarme de establecer el primer estado de respuesta una vez que la vista es parte de la jerarquía de vistas. Por lo tanto, establecer el estado del primer respondedor en su viewDidLoadmétodo de controladores de vista no funcionará, por ejemplo. Y si no está seguro de si está funcionando, le [view becomeFirstResponder]devuelve un valor booleano que puede probar.

    Otro punto: puede usar un controlador de vista para capturar el evento de sacudida si no desea crear una subclase UIView innecesariamente. Sé que no es tanta molestia, pero aún así la opción está ahí. Simplemente mueva los fragmentos de código que Kendall puso en la subclase UIView a su controlador y envíe los mensajes becomeFirstRespondery resignFirstRespondera en selflugar de la subclase UIView.

    Newtz
    fuente
    Esto no proporciona una respuesta a la pregunta. Para criticar o solicitar una aclaración de un autor, deje un comentario debajo de su publicación.
    Alex Salauyou
    @SashaSalauyou Claramente dije en la primera oración aquí que no tenía suficiente reputación para publicar un comentario en ese momento
    Newtz
    lo siento. Aquí es posible que la respuesta de 5 años llegue a la cola de revisión))
    Alex Salauyou
    5

    En primer lugar, sé que esta es una publicación antigua, pero aún es relevante, y descubrí que las dos respuestas mejor votadas no detectaron la sacudida lo antes posible . Asi es como se hace:

    1. Enlace CoreMotion a su proyecto en las fases de compilación del objetivo: CoreMotion
    2. En su ViewController:

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

    La solución más fácil es derivar una nueva ventana raíz para su aplicación:

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

    Luego, en su aplicación delegado:

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

    Si está utilizando un Storyboard, esto puede ser más complicado, no sé con precisión el código que necesitará en el delegado de la aplicación.

    mxcl
    fuente
    Y sí, se puede concluir cuál es su clase base en el @implementationpuesto Xcode 5.
    mxcl
    Esto funciona, lo uso en varias aplicaciones. Así que no estoy seguro de por qué se ha rechazado.
    mxcl
    trabajado como un encanto. En caso de que se lo pregunte, puede anular la clase de ventana de esta manera: stackoverflow.com/a/10580083/709975
    Edu
    ¿Funcionará cuando la aplicación esté en segundo plano? Si no alguna otra idea, ¿cómo detectarla en segundo plano?
    nr5
    Esto probablemente funcionará si tiene un modo de fondo establecido.
    mxcl
    1

    Solo usa estos tres métodos para hacerlo

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

    para más detalles, puede consultar un código de ejemplo completo allí

    Mashhadi
    fuente
    1

    ¡Una versión rápida basada en la primera respuesta!

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

    Para habilitar esta aplicación, creé una categoría en UIWindow:

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