344

当有人摇晃 iPhone 时,我想做出反应。我并不特别在乎他们如何摇晃它,只是它被大力挥动了一瞬间。有谁知道如何检测这个?

4

17 回答 17

293

在 3.0 中,现在有一种更简单的方法 - 挂钩新的运动事件。

主要技巧是您需要有一些 UIView(不是 UIViewController)作为 firstResponder 来接收震动事件消息。以下是您可以在任何 UIView 中用于获取抖动事件的代码:

@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

您可以轻松地将任何 UIView(甚至系统视图)转换为可以获取抖动事件的视图,只需使用这些方法对视图进行子类化(然后在 IB 中选择这种新类型而不是基本类型,或者在分配看法)。

在视图控制器中,您希望将此视图设置为第一响应者:

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

不要忘记,如果您有其他视图成为用户操作的第一响应者(例如搜索栏或文本输入字段),您还需要在其他视图辞职时恢复摇晃视图的第一响应者状态!

即使您将 applicationSupportsShakeToEdit 设置为 NO,此方法也有效。

于 2009-07-10T21:06:33.907 回答
180

从我的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

滞后性可防止多次触发摇晃事件,直到用户停止摇晃。

于 2008-10-01T20:43:58.170 回答
154

我终于使用此Undo/Redo Manager Tutorial中的代码示例使其工作。
这正是您需要做的:

  • 在 App 的 Delegate 中设置applicationSupportsShakeToEdit属性:
  • 
        - (void)applicationDidFinishLaunching:(UIApplication *)application {
    
            application.applicationSupportsShakeToEdit = YES;
    
            [window addSubview:viewController.view];
            [window makeKeyAndVisible];
    }
    

  • 在视图控制器中添加/覆盖canBecomeFirstResponderviewDidAppear:viewWillDisappear:方法:
  • 
    -(BOOL)canBecomeFirstResponder {
        return YES;
    }
    
    -(void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
        [self becomeFirstResponder];
    }
    
    - (void)viewWillDisappear:(BOOL)animated {
        [self resignFirstResponder];
        [super viewWillDisappear:animated];
    }
    

  • motionEnded方法添加到您的视图控制器:
  • 
    - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
    {
        if (motion == UIEventSubtypeMotionShake)
        {
            // your code
        }
    }
    
    于 2010-03-08T23:58:19.530 回答
    94

    首先,肯德尔 7 月 10 日的回答是准确的。

    现在......我想做类似的事情(在 iPhone OS 3.0+ 中),只是在我的情况下,我希望它适用于整个应用程序,以便在发生震动时提醒应用程序的各个部分。这就是我最终做的事情。

    首先,我继承了 UIWindow。这很容易。创建一个新的类文件,其接口如MotionWindow : UIWindow(feel free to pick your own, natch)。添加这样的方法:

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

    更改@"DeviceShaken"为您选择的通知名称。保存文件。

    现在,如果你使用 MainWindow.xib(股票 Xcode 模板的东西),进入那里并将你的 Window 对象的类从UIWindow更改为MotionWindow或任何你称之为它的东西。保存xib。如果您以编程方式设置UIWindow,请在此处使用新的 Window 类。

    现在您的应用程序正在使用专门的UIWindow类。无论您想在哪里收到关于摇晃的消息,都可以注册他们的通知!像这样:

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

    将自己作为观察者移除:

    [[NSNotificationCenter defaultCenter] removeObserver:self];
    

    我将我的放在viewWillAppear:viewWillDisappear:中,其中涉及视图控制器。确保您对抖动事件的响应知道它是否“已经在进行中”。否则,如果设备连续摇晃两次,您将遇到交通堵塞。这样,您可以忽略其他通知,直到您真正完成对原始通知的响应。

    另外:您可以选择提示motionBeganmotionEnded。由你决定。在我的情况下,效果总是需要在设备静止发生(相对于它开始摇晃时),所以我使用motionEnded。尝试两者,看看哪一个更有意义......或检测/通知两者!

    这里还有一个(好奇的?)观察:请注意,此代码中没有第一响应者管理的迹象。到目前为止,我只用 Table View Controllers 尝试过这个,一切似乎都很好地协同工作!不过,我不能保证其他情况。

    肯德尔等。al - 谁能谈谈为什么UIWindow子类会这样?是因为窗户位于食物链的顶端吗?

    于 2009-08-29T13:47:43.933 回答
    35

    我发现这篇文章正在寻找一个“摇晃”的实现。millenomi 的回答对我来说效果很好,尽管我正在寻找需要更多“摇晃动作”才能触发的东西。我已经用一个 int shakeCount 替换为布尔值。我还在 Objective-C 中重新实现了 L0AccelerationIsShaking() 方法。您可以通过调整添加到shakeCount 的量来调整所需的摇动量。我不确定我是否找到了最佳值,但到目前为止它似乎运作良好。希望这可以帮助某人:

    - (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:我已将更新间隔设置为 1/15 秒。

    [[UIAccelerometer sharedAccelerometer] setUpdateInterval:(1.0 / 15)];
    
    于 2008-11-10T17:14:52.160 回答
    13

    在带有 Swift 的 iOS 8.3(可能更早)中,它就像覆盖视图控制器中的motionBeganor方法一样简单:motionEnded

    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!")
        }
    }
    
    于 2015-04-20T22:39:56.957 回答
    12

    您需要通过 accelerometer:didAccelerate: 方法检查加速度计,该方法是 UIAccelerometerDelegate 协议的一部分,并检查值是否超过摇动所需移动量的阈值。

    GLPaint 示例中 AppController.m 底部的 accelerometer:didAccelerate: 方法中有不错的示例代码,该示例可在 iPhone 开发者网站上找到。

    于 2008-09-29T21:17:44.377 回答
    10

    这是您需要的基本委托代码:

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

    还要在界面中的相应代码中设置。IE:

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

    于 2009-03-22T21:15:52.407 回答
    7

    查看 GLPaint 示例。

    http://developer.apple.com/library/ios/#samplecode/GLPaint/Introduction/Intro.html

    于 2008-09-29T20:16:00.327 回答
    7

    在 ViewController.m 文件中添加以下方法,它可以正常工作

        -(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 
         }
    
    于 2012-10-15T12:11:16.310 回答
    5

    很抱歉将其发布为答案而不是评论,但正如您所见,我是 Stack Overflow 的新手,所以我还没有足够的声誉来发表评论!

    无论如何,一旦视图成为视图层次结构的一部分,我第二次@cire 确保设置第一响应者状态。因此,例如,在您的视图控制器viewDidLoad方法中设置第一响应者状态将不起作用。如果您不确定它是否有效,[view becomeFirstResponder]则会返回一个可以测试的布尔值。

    还有一点:如果您不想不必要地创建 UIView 子类,可以使用视图控制器来捕获抖动事件。我知道这不是那么麻烦,但仍然有选择。只需将 Kendall 放入 UIView 子类的代码片段移动到您的控制器中,然后将becomeFirstResponderandresignFirstResponder消息发送到self而不是 UIView 子类。

    于 2009-08-18T15:05:41.763 回答
    5

    首先,我知道这是一个旧帖子,但它仍然是相关的,我发现两个最高投票的答案并没有尽早检测到抖动。这是如何做到的:

    1. 在目标的构建阶段将 CoreMotion 链接到您的项目: 核心运动
    2. 在您的视图控制器中:

      - (BOOL)canBecomeFirstResponder {
          return YES;
      }
      
      - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
      {
          if (motion == UIEventSubtypeMotionShake) {
              // Shake detected.
          }
      }
      
    于 2015-03-13T14:59:13.347 回答
    4

    最简单的解决方案是为您的应用程序派生一个新的根窗口:

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

    然后在您的应用程序委托中:

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

    如果您使用的是 Storyboard,这可能会比较棘手,我不知道您在应用程序委托中需要的代码。

    于 2014-07-12T12:46:34.273 回答
    1

    只需使用这三种方法即可

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

    有关详细信息,您可以在那里查看完整的示例代码

    于 2013-02-21T09:30:47.530 回答
    1

    基于第一个答案的 swiftease 版本!

    override func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?) {
        if ( event?.subtype == .motionShake )
        {
            print("stop shaking me!")
        }
    }
    
    于 2017-09-10T07:44:21.960 回答
    0

    在 swift 5 中,您可以通过以下方式捕捉动作并检查

    override func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?) {
       if motion == .motionShake 
       {
          print("shaking")
       }
    }
    
    于 2021-01-25T05:53:39.217 回答
    -1

    为了在应用范围内启用这个应用程序,我在 UIWindow 上创建了一个类别:

    @implementation UIWindow (Utils)
    
    - (BOOL)canBecomeFirstResponder
    {
        return YES;
    }
    
    - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
    {
        if (motion == UIEventSubtypeMotionShake) {
            // Do whatever you want here...
        }
    }
    
    @end
    
    于 2017-09-15T19:45:23.737 回答