Como detecto quando alguém sacode um iPhone?

340

Quero reagir quando alguém sacode o iPhone. Eu particularmente não me importo com como eles agitam, apenas que foi acenado vigorosamente por uma fração de segundo. Alguém sabe como detectar isso?

Josh Gagnon
fonte

Respostas:

292

Na versão 3.0, agora existe uma maneira mais fácil de se conectar aos novos eventos de movimento.

O truque principal é que você precisa ter algum UIView (não o UIViewController) que deseja que o firstResponder receba as mensagens do evento shake. Aqui está o código que você pode usar em qualquer UIView para obter eventos de agitação:

@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

Você pode transformar facilmente qualquer UIView (até mesmo visualizações do sistema) em uma visualização que possa obter o evento shake simplesmente subclassificando a visualização apenas com esses métodos (e selecionando esse novo tipo em vez do tipo base no IB, ou usando-o ao alocar um Visão).

No controlador de exibição, você deseja definir essa exibição para se tornar o primeiro respondedor:

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

Não se esqueça de que, se você tiver outras visualizações que se tornam a primeira resposta a partir de ações do usuário (como uma barra de pesquisa ou um campo de entrada de texto), também será necessário restaurar o status de resposta trémula da primeira exibição quando a outra exibição for renegada!

Este método funciona mesmo se você definir applicationSupportsShakeToEdit como NO.

Kendall Helmstetter Gelner
fonte
5
Isso não funcionou muito para mim: eu precisava substituir meu controlador em viewDidAppearvez de viewWillAppear. Não sei por que; talvez a visualização precise estar visível antes de poder fazer o que for necessário para começar a receber os eventos de agitação?
9139 Kristopher Johnson
11
Isso é mais fácil, mas não necessariamente melhor. Se você deseja detectar um movimento longo e contínuo, essa abordagem não é útil, pois o iPhone gosta de disparar motionEndedantes que o movimento realmente pare. Portanto, usando essa abordagem, você obtém uma série desconexa de batidas curtas, em vez de uma longa. A outra resposta funciona muito melhor neste caso.
aroth
3
@Kendall - O UIViewControllers implementa o UIResponder e está na cadeia de respostas. A UIWindow mais alta também é. developer.apple.com/library/ios/DOCUMENTATION/EventHandling/…
DougW
11
[super respondsToSelector:não fará o que você deseja, pois é equivalente à chamada [self respondsToSelector:que retornará YES. O que você precisa é [[ShakingView superclass] instancesRespondToSelector:.
Vadim Yelagin
2
Em vez de usar: if (event.subtype == UIEventSubtypeMotionShake) você pode usar: if (movimento == UIEventSubtypeMotionShake)
Hashim Akhtar
179

Do meu aplicativo 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

A histerese impede que o evento de agitação seja acionado várias vezes até que o usuário interrompa a agitação.

millenomi
fonte
7
Observe que adicionei uma resposta abaixo, apresentando um método mais fácil para o 3.0.
Kendall Helmstetter Gelner
2
O que acontece se eles estiverem agitando precisamente em um eixo específico?
jprete
5
A melhor resposta, porque o iOS 3.0 motionBegane os motionEndedeventos não são muito precisos ou precisos em termos de detecção do início e do fim exatos de um abalo. Essa abordagem permite que você seja tão preciso quanto desejar.
Aroth 13/04
2
UIAccelerometerDelegate accelerometer : didAccelerate: foi descontinuado no iOS5.
Yantao Xie
Esta outra resposta é melhor e mais rápido para implementar stackoverflow.com/a/2405692/396133
Abramodj
154

Finalmente o fiz funcionar usando exemplos de código deste Tutorial do Gerenciador de Desfazer / Refazer .
É exatamente isso que você precisa fazer:

  • Defina a propriedade applicationSupportsShakeToEdit no Delegado do aplicativo:
  • 
        - (void)applicationDidFinishLaunching:(UIApplication *)application {
    
            application.applicationSupportsShakeToEdit = YES;
    
            [window addSubview:viewController.view];
            [window makeKeyAndVisible];
    }
    

  • Adicionar / Substituir os métodos canBecomeFirstResponder , viewDidAppear: e viewWillDisappear: no seu View Controller:
  • 
    -(BOOL)canBecomeFirstResponder {
        return YES;
    }
    
    -(void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
        [self becomeFirstResponder];
    }
    
    - (void)viewWillDisappear:(BOOL)animated {
        [self resignFirstResponder];
        [super viewWillDisappear:animated];
    }
    

  • Adicione o método motionEnded ao seu View Controller:
  • 
    - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
    {
        if (motion == UIEventSubtypeMotionShake)
        {
            // your code
        }
    }
    
    Eran Talmor
    fonte
    Apenas para adicionar à solução Eran Talmor: você deve usar um controlador de navegação em vez de um controlador de exibição, caso tenha escolhido esse aplicativo no novo seletor de projetos.
    Rob
    Eu tentei usar isso, mas às vezes heavyshake ou shake de luz que só parou
    vinothp
    A etapa 1 agora é redundante, como o valor padrão de applicationSupportsShakeToEdité YES.
    DonVaughn
    11
    Por que ligar [self resignFirstResponder];antes [super viewWillDisappear:animated];? Isso parece peculiar.
    JaredH
    94

    Primeiro, a resposta de 10 de julho de Kendall é direta.

    Agora ... eu queria fazer algo semelhante (no iPhone OS 3.0+), mas no meu caso, eu queria que fosse em todo o aplicativo para que eu pudesse alertar várias partes do aplicativo quando um tremor ocorreu. Aqui está o que eu acabei fazendo.

    Primeiro, subclassifiquei o UIWindow . Isso é fácil. Crie um novo arquivo de classe com uma interface como MotionWindow : UIWindow(fique à vontade para escolher o seu, natch). Adicione um método como este:

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

    Mude @"DeviceShaken"para o nome da notificação de sua escolha. Salve o arquivo.

    Agora, se você usar um MainWindow.xib (material de modelo Xcode padrão), vá lá e altere a classe do seu objeto Window de UIWindow para MotionWindow ou o que você chamou. Salve o xib. Se você configurar o UIWindow programaticamente, use sua nova classe Window lá.

    Agora seu aplicativo está usando a classe UIWindow especializada . Onde você quiser saber sobre um shake, inscreva-se para receber as notificações! Como isso:

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

    Para se remover como um observador:

    [[NSNotificationCenter defaultCenter] removeObserver:self];

    Coloquei o meu em viewWillAppear: e viewWillDisappear: no que diz respeito aos controladores de exibição. Verifique se a sua resposta ao evento de agitação sabe se "já está em andamento" ou não. Caso contrário, se o dispositivo for sacudido duas vezes seguidas, você terá um congestionamento de trânsito lento. Dessa forma, você pode ignorar outras notificações até responder de verdade à notificação original.

    Além disso: você pode optar por deixar o motionBegan vs. motionEnded . Você decide. No meu caso, o efeito sempre precisa ocorrer depois que o dispositivo está em repouso (vs. quando começa a tremer), então uso motionEnded . Experimente os dois e veja qual deles faz mais sentido ... ou detecte / notifique os dois!

    Mais uma observação (curiosa?) Aqui: Observe que não há sinal de gerenciamento de socorristas neste código. Eu só tentei isso com os controladores de exibição de tabela até agora e tudo parece funcionar muito bem juntos! Mas não posso garantir outros cenários.

    Kendall, et. al - alguém pode falar por que isso pode acontecer com as subclasses de UIWindow ? É porque a janela está no topo da cadeia alimentar?

    Joe D'Andrea
    fonte
    11
    Isso é tão estranho. Funciona como está. Espero que não seja um caso de "trabalhar apesar de tudo"! Deixe-me saber o que você descobriu em seus próprios testes.
    Joe D'Andrea
    Eu prefiro isso a subclassificar uma UIView regular, especialmente porque você só precisa que ela exista em um só lugar, então colocar uma subclasse de janela parece um ajuste natural.
    Shaggy Frog
    Obrigado jdandrea, eu uso o seu método, mas tremendo ficando várias vezes !!! Eu só quero que os usuários possam tremer uma vez (em uma visão de que tremer faz lá) ...! como posso lidar com isso? eu implemento sth assim. Eu implemento meu código smht assim: i-phone.ir/forums/uploadedpictures/… ---- mas o problema é que o aplicativo agita apenas 1 vez pela vida do aplicativo! não só vista
    Momi
    11
    Momeks: se você precisar que a notificação ocorra quando o dispositivo estiver em repouso (pós-vibração), use motionEnded em vez de motionBegan. Isso deve fazer!
    Joe D'Andrea
    11
    @ Joe D'Andrea - Funciona como está porque o UIWindow está na cadeia de respostas. Se algo mais alto na cadeia interceptasse e consumisse esses eventos, não os receberia. developer.apple.com/library/ios/DOCUMENTATION/EventHandling/…
    DougW
    34

    Me deparei com este post procurando uma implementação "tremendo". A resposta de millenomi funcionou bem para mim, embora eu estivesse procurando por algo que exigisse um pouco mais de "ação trêmula" para disparar. Substituí o valor booleano por um int shakeCount. Também reimplementei o método L0AccelerationIsShaking () no Objective-C. Você pode ajustar a quantidade de agitação necessária ajustando a quantidade adicionada a shakeCount. Não tenho certeza de que encontrei os valores ideais ainda, mas parece estar funcionando bem até agora. Espero que isso ajude alguém:

    - (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: Eu configurei o intervalo de atualização para 1/15 de segundo.

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

    fonte
    Como posso detectar o movimento do dispositivo como em um panorama?
    user2526811
    Como identificar o shake se o iphone for movido apenas na direção X? Não quero detectar trepidações na direção Y ou Z.
    Manthan
    12

    Você precisa verificar o acelerômetro via accelerometer: didAccelerate: método que faz parte do protocolo UIAccelerometerDelegate e verificar se os valores ultrapassam um limite para a quantidade de movimento necessária para uma trepidação.

    Há um código de amostra decente no acelerômetro: didAccelerate: método na parte inferior do AppController.m no exemplo GLPaint, disponível no site do desenvolvedor do iPhone.

    Dave Verwer
    fonte
    12

    No iOS 8.3 (talvez anterior) com Swift, é tão simples quanto substituir os métodos motionBeganou motionEndedno seu controlador de exibição:

    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
    fonte
    Você tentou? Suponho que, para que isso funcione quando o aplicativo estiver em segundo plano, seria necessário um pouco de trabalho extra.
    Nhgrif 11/0518
    Não .. em breve .. mas Se você notou o iPhone 6s, ele possui o recurso "pegar para ativar a tela", onde a tela fica iluminada por algum tempo quando você pega o dispositivo. Preciso capturar esse comportamento #
    nr5
    9

    Este é o código delegado básico que você precisa:

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

    Defina também o código apropriado na interface. ou seja:

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

    Benjamin Ortuzar
    fonte
    Como posso detectar o movimento do dispositivo como em um panorama?
    user2526811
    Como identificar o shake se o iphone for movido apenas na direção X? Não quero detectar trepidações na direção Y ou Z.
    Manthan
    7

    Adicionar métodos a seguir no arquivo ViewController.m, funcionando corretamente

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

    Desculpe postar isso como uma resposta, e não como um comentário, mas como você pode ver, eu sou novo no Stack Overflow e ainda não tenho reputação o suficiente para postar comentários!

    De qualquer forma, eu digo à @cire sobre a garantia de definir o status de primeiro respondedor quando a exibição fizer parte da hierarquia da exibição. Portanto, definir o status de primeiro respondedor no viewDidLoadmétodo de controladores de exibição não funcionará, por exemplo. E se você não tiver certeza se está funcionando, [view becomeFirstResponder]retorna um booleano que você pode testar.

    Outro ponto: você pode usar um controlador de exibição para capturar o evento de agitação, se não desejar criar uma subclasse UIView desnecessariamente. Eu sei que não é tanto aborrecimento, mas ainda existe a opção. Basta mover os trechos de código que o Kendall colocou na subclasse UIView para o seu controlador e enviar as mensagens becomeFirstRespondere resignFirstResponderpara, em selfvez da subclasse UIView.

    Newtz
    fonte
    Isso não fornece uma resposta para a pergunta. Para criticar ou solicitar esclarecimentos a um autor, deixe um comentário abaixo da postagem.
    Alex Salauyou
    @SashaSalauyou I claramente indicado na primeira frase aqui eu não tenho reputação suficiente para postar um comentário no momento
    Newtz
    desculpa. É possível aqui que a resposta de 5 anos entre na fila de revisão))
    Alex Salauyou
    5

    Primeiro, sei que esse é um post antigo, mas ainda é relevante, e descobri que as duas respostas mais votadas não detectaram o tremor o mais cedo possível . É assim que se faz:

    1. Vincule o CoreMotion ao seu projeto nas fases de construção do destino: CoreMotion
    2. No seu ViewController:

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

    A solução mais fácil é obter uma nova janela raiz para seu aplicativo:

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

    Em seguida, no seu delegado do aplicativo:

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

    Se você estiver usando um Storyboard, isso pode ser mais complicado, não sei exatamente o código que você precisará no delegado do aplicativo.

    mxcl
    fonte
    E sim, você pode derivar sua classe base na @implementationdesde Xcode 5.
    mxcl
    Isso funciona, eu uso em vários aplicativos. Portanto, não sei por que foi votado para baixo.
    Mxcl
    funcionou como um encanto. Caso você esteja se perguntando, você pode substituir a classe da janela assim: stackoverflow.com/a/10580083/709975
    Edu
    Isso funcionará quando o aplicativo estiver em segundo plano? Se não houver outra idéia de como detectar quando em segundo plano?
    Nr5
    Provavelmente isso funcionará se você tiver um conjunto de modos em segundo plano.
    Mxcl 15/05
    1

    Basta usar esses três métodos para fazer isso

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

    para detalhes, você pode verificar um código de exemplo completo por

    Mashhadi
    fonte
    1

    Uma versão swiftease baseada na primeira resposta!

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

    Para habilitar esse aplicativo, criei uma categoria no 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
    fonte