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?
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?
Respostas:
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.
motionEnded
antes 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.
[super respondsToSelector:
não fará o que você deseja, pois é equivalente à chamada [self respondsToSelector:
que retornará YES
. O que você precisa é [[ShakingView superclass] instancesRespondToSelector:
.
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.
motionBegan
e os motionEnded
eventos 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.
Finalmente o fiz funcionar usando exemplos de código deste Tutorial do Gerenciador de Desfazer / Refazer .
É exatamente isso que você precisa fazer:
- (void)applicationDidFinishLaunching:(UIApplication *)application {
application.applicationSupportsShakeToEdit = YES;
[window addSubview:viewController.view];
[window makeKeyAndVisible];
}
-(BOOL)canBecomeFirstResponder {
return YES;
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self becomeFirstResponder];
}
- (void)viewWillDisappear:(BOOL)animated {
[self resignFirstResponder];
[super viewWillDisappear:animated];
}
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (motion == UIEventSubtypeMotionShake)
{
// your code
}
}
applicationSupportsShakeToEdit
é YES
.
[self resignFirstResponder];
antes [super viewWillDisappear:animated];
? Isso parece peculiar.
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?
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)];
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.
No iOS 8.3 (talvez anterior) com Swift, é tão simples quanto substituir os métodos motionBegan
ou motionEnded
no 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!")
}
}
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>
Confira o exemplo do GLPaint.
http://developer.apple.com/library/ios/#samplecode/GLPaint/Introduction/Intro.html
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
}
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 viewDidLoad
mé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 becomeFirstResponder
e resignFirstResponder
para, em self
vez da subclasse UIView.
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:
No seu ViewController:
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (motion == UIEventSubtypeMotionShake) {
// Shake detected.
}
}
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.
@implementation
desde Xcode 5.
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 lá
Uma versão swiftease baseada na primeira resposta!
override func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?) {
if ( event?.subtype == .motionShake )
{
print("stop shaking me!")
}
}
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
viewDidAppear
vez deviewWillAppear
. 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?