views:

28890

answers:

12

I want to react when somebody shakes the iPhone. I don't particularly care how they shake it, just that it was waved vigorously about for a split second. Does anyone know how to detect this?

+5  A: 

Check out the GLPaint example.

camflan
+7  A: 

You need to check the accelerometer via accelerometer:didAccelerate: method which is part of the UIAccelerometerDelegate protocol and check whether the values go over a threshold for the amount of movement needed for a shake.

There is decent sample code in the accelerometer:didAccelerate: method right at the bottom of AppController.m in the GLPaint example which is available on the iPhone developer site.

Dave Verwer
+52  A: 

From my Diceshaker application:

// 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

The histeresis prevents the shake event from triggering multiple times until the user stops the shake.

millenomi
Great answer. Thank you!
Stephen Darlington
Note I have added an answer below presenting an easier method for 3.0.
Kendall Helmstetter Gelner
What happens if they're shaking it precisely on a particular axis?
jprete
+18  A: 

I came across this post looking for a "shaking" implementation. millenomi's answer worked well for me, although i was looking for something that required a bit more "shaking action" to trigger. I've replaced to Boolean value with an int shakeCount. I also reimplemented the L0AccelerationIsShaking() method in Objective-C. You can tweak the ammount of shaking required by tweaking the ammount added to shakeCount. I'm not sure i've found the optimal values yet, but it seems to be working well so far. Hope this helps someone:

- (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: I've set the update interval to 1/15th of a second.

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

This is the basic delegate code you need:

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

Also set the in the appropriate code in the Interface. i.e:

@interface MyViewController : UIViewController [UIPickerViewDelegate, UIPickerViewDataSource, UIAccelerometerDelegate] (replace my square brakes for < >, stackoverflow thinks they are html markup and is hidding them)

Benjamin Ortuzar
+67  A: 

In 3.0, there's now an easier way - hook into the new motion events.

The main trick is that you need to have some UIView (not view controller) that you want as firstResponder to receive the shake event messages. Here's the code that you can use in any UIView to get shake events:

@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

You can easily transform any UIView (even system views) into a view that can get the shake event simply by subclassing the view with only these methods (and then selecting this new type instead of the base type in IB, or using it when allocating a view).

In the view controller, you want to set this view to become first responder:

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

Don't forget that if you have other views that become first responder from user actions (like a search bar or text entry field) you'll also need to restore the shaking view first responder status when the other view resigns!

This method works even if you set applicationSupportsShakeToEdit to NO.

Kendall Helmstetter Gelner
This didn't quite work for me: I needed to override my controller's `viewDidAppear` instead of `viewWillAppear`. I'm not sure why; maybe the view needs to be visible before it can do whatever it does to start receiving the shake events?
Kristopher Johnson
I noticed in 3.1 something seemed funny, that might have been it - it could be the view will not take first responder status until that point (or it is a bug). I'll research and file a radar or update my code.
Kendall Helmstetter Gelner
FWIW, I was using 3.0.
Kristopher Johnson
Odd, I tested again (under 3.0) and the code works- though in 3.1, in the simulator, it stopped working as I had it (still works on the device though).
Kendall Helmstetter Gelner
This is wonderful - thanks Kendall! Now for the $1M question: How do I detect when another view resigns as first responder? (I'm thinkig I need to set a timer and periodically call canBecomeFirstResponder, but I also wonder if that might be overkill.) What I'd _really_ like to do is detect a shake throughout the app's lifetime, and then send out a notification. Then I can have _any_ part of my app get word of the shake and opt to do something about it. Contrived Example: A table VC could use it as a cue to reload the table.
Joe D'Andrea
Wait a sec, you ARE using canBecomeFirstResponder. :) I'll try this again, only I might move it to UIWindow (if that's even advisable from a performance POV?).
Joe D'Andrea
Got it! I'll post to a new answer.
Joe D'Andrea
Great answer, very helpful to put a C#/MonoTouch sample together http://conceptdev.blogspot.com/2009/10/monotouch-shake-shake-shake.html
CraigD
Can you please elaborate why you advise "not view controller"? It works fine for me if I implement the canBecomeFirstResponder -> return YES method in the view controller. Also, I don't have to create a subclass just to handle the shake motion and I have more information in the view controller to handle the shake than I would have in the view.
0xced
Since the viewController is not a view, I would have thought it could not be a part of the responder chain. I'll have to figure out why that works...
Kendall Helmstetter Gelner
Here's a weird one, when I run a shake gesture in the simulator the motionEnded:withEvent: method gets called twice with UIEventSubtypeMotionShake. Any idea why this would be getting called more than once?
Cory Imdieke
Thought I would comment further, I changed motionEnded:withEvent: to motionBegan:withEvent: and that only receives the call once. Not sure why that one gets the call once and the Ended method gets it twice, but either way this seems to fix the problem.
Cory Imdieke
+2  A: 

Hi, Im a noob in XCode, and I am implementing an app with two views Inputs and TableView, two controllers, and one app delegate.

Been trying to implement the code that is provided in the GLPaint example but nothing happens (uisng Xcode Version 3.1.3)

@Kendall, read your posts... Could you help me with more details on where to implement the code you provided... I'm a bit confused on this part... Should I implement it in the delegate? Thanksss!!

FelipeUY
You should post this as your own question if you want answers.
Jeff Kelley
+2  A: 

Just to add to Kendall's solution, setting the "shake" view's first reponder status should be done when the "shake" view is ALREADY part of the view hierarchy.. preferably after a call to [view addSubview:shakeView]

+2  A: 

Sorry to post this as an answer rather than a comment but as you can see I'm new to Stack Overflow and so I'm not yet reputable enough to post comments!

Anyway I second cire about making sure to set the first responder status once the view is part of the view hierarchy. So setting first responder status in your view controllers viewDidLoad method won't work for example. And if you're unsure as to whether it is working [view becomeFirstResponder] returns you a boolean that you can test.

Another point: you can use a view controller to capture the shake event if you don't want to create a UIView subclass unnecessarily. I know it's not that much hassle but still the option is there. Just move the code snippets that Kendall put into the UIView subclass into your controller and send the becomeFirstResponder and resignFirstResponder messages to self instead of the UIView subclass.

Newtz
+28  A: 

First, Kendall's July 10th answer is spot-on.

Now ... I wanted to do something similar (in iPhone OS 3.0+), only in my case I wanted it app-wide so I could alert various parts of the app when a shake occurred. Here's what I ended up doing.

First, I subclassed UIWindow. This is easy peasy. Create a new class file with an interface such as MotionWindow : UIWindow (feel free to pick your own, natch). Add a method like so:

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

Change @"DeviceShaken" to the notification name of your choice. Save the file.

Now, if you use a MainWindow.xib (stock Xcode template stuff), go in there and change the class of your Window object from UIWindow to MotionWindow or whatever you called it. Save the xib. If you set up UIWindow programmatically, use your new Window class there instead.

Now your app is using the specialized UIWindow class. Wherever you want to be told about a shake, sign up for them notifications! Like this:

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

To remove yourself as an observer:

[[NSNotificationCenter defaultCenter] removeObserver:self];

I put mine in viewWillAppear: and viewWillDisappear: where View Controllers are concerned. Be sure your response to the shake event knows if it is "already in progress" or not. Otherwise, if the device is shaken twice in succession, you'll have a li'l traffic jam. This way you can ignore other notifications until you're truly done responding to the original notification.

Also: You may choose to cue off of motionBegan vs. motionEnded. It's up to you. In my case, the effect always needs to take place after the device is at rest (vs. when it starts shaking), so I use motionEnded. Try both and see which one makes more sense ... or detect/notify for both!

One more (curious?) observation here: Notice there's no sign of first responder management in this code. I've only tried this with Table View Controllers so far and everything seems to work quite nicely together! I can't vouch for other scenarios though.

Kendall, et. al - can anyone speak to why this might be so for UIWindow subclasses? Is it because the window is at the top of the food chain?

Joe D'Andrea
It seems like you might still have to re-set your status as first responder after anything else grabbed it (like editing a text field) but if it works as-is - possibly not. I think you are basically right that by default the main UIWindow gets first responder status, or events always pass through that being the primary container. I'll have to test out this technique a bit myself.
Kendall Helmstetter Gelner
That's what's so weird. It works as-is. Hopefully it's not a case of "working in spite of itself" though! Please let me know what you find out in your own testing.
Joe D'Andrea
I prefer this to subclassing a regular UIView, especially since you only need this to exist in one place, so putting in a window subclass seems a natural fit.
Shaggy Frog
This answer is so awesome and clean. Notifications are a *perfect* fit here. Thank you.
Squeegy
Thank you jdandrea , i use your method but shaking getting multiple times !!! i just want users can shake one time (on a view that shaking does there ) ... ! how can i handle it ? i implement sth like this. i implement my code smht like this : http://www.i-phone.ir/forums/uploadedpictures/1a53f7d39d2d711ae73e332d48d5be21.png ---- but the problem is app does shake only 1 time for life of application ! not only view
Momeks
Momeks: If you need the notification to occur once the device is at rest (post-shake), use motionEnded instead of motionBegan. That ought to do it!
Joe D'Andrea
This is for sure the right way to do it. See Apples code example CLPaint.
Thomas
I had not looked at GLPaint - brilliant. Thanks Thomas!
Joe D'Andrea
Bravo!! Joe D'Andrea
Asad Khan
+8  A: 

Finally I got it working using code examples from this Undo/Redo Manager Tutorial.
This is exactly what you need to do:

  • Set the applicationSupportsShakeToEdit property in the App's Delegate:
  • 
        - (void)applicationDidFinishLaunching:(UIApplication *)application {
    
            application.applicationSupportsShakeToEdit = YES;
    
            [window addSubview:viewController.view];
            [window makeKeyAndVisible];
    }
    

  • Add/Override canBecomeFirstResponder, viewDidAppear: and viewWillDisappear: methods in your View Controller:
  • 
    -(BOOL)canBecomeFirstResponder {
        return YES;
    }
    
    -(void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
        [self becomeFirstResponder];
    }
    
    - (void)viewWillDisappear:(BOOL)animated {
        [self resignFirstResponder];
        [super viewWillDisappear:animated];
    }
    

  • Add the motionEnded method to your View Controller:
  • 
    - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
    {
        if (motion == UIEventSubtypeMotionShake)
        {
            // your code
        }
    }
    
    Eran Talmor
    Worked perfect for me in 3.1.3, thanks!
    Rob S.
    excellent, worked for me too. (easiest on this page so far).
    norskben
    A: 

    Just to add to Eran Talmor solution : you should use a navigationcontroller instead of a viewcontroller in case of you've choose such an application in new project chooser.

    Rob