如何检测有人摇动iPhone的时间?


Answers:


292

在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中选择此新类型而不是基本类型,或者在分配a时使用它)来获得震动事件的视图。视图)。

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

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

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

即使将applicationSupportsShakeToEdit设置为NO,此方法也可以使用。


5
这对我来说不太奏效:我需要覆盖控制器的viewDidAppear而不是viewWillAppear。我不知道为什么;可能需要先使视图可见,然后视图才能开始接收震动事件?
克里斯托弗·约翰逊,

1
这比较容易,但不一定更好。如果您想检测到长时间的连续晃动,则此方法没有用,因为iPhone喜欢motionEnded在晃动实际停止之前触发。因此,使用这种方法,您会得到一系列不连贯的短摇,而不是一个长摇。在这种情况下,其他答案会更好。
aroth 2011年

3
@Kendall-UIViewControllers实现UIResponder并位于响应者链中。最顶部的UIWindow也是如此。 developer.apple.com/library/ios/DOCUMENTATION/EventHandling/...
DougW

1
[super respondsToSelector:不会执行您想要的操作,因为这等效于[self respondsToSelector:将返回的调用YES。您需要的是[[ShakingView superclass] instancesRespondToSelector:
Vadim Yelagin 2014年

2
代替使用:if(event.subtype == UIEventSubtypeMotionShake),可以使用:if(motion == UIEventSubtypeMotionShake)
Hashim Akhtar

179

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

历史记录阻止了多次震动事件触发,直到用户停止震动为止。


7
请注意,我在下面添加了一个答案,为3.0提供了一种更简单的方法。
肯德尔·赫尔姆斯特·盖尔纳

2
如果他们精确地在特定轴上摇动会发生什么?
jprete

5
最佳答案,因为iOS 3.0 motionBeganmotionEnded事件在检测摇动的确切开始和结束方面不是非常精确或不准确。这种方法可以使您尽可能精确。
aroth 2011年

2
UIAccelerometerDelegate accelerometer:didAccelerate:在iOS5中已弃用。
谢艳涛2012年

这个另外的答案是更好,更快地实现stackoverflow.com/a/2405692/396133
Abramodj 2012年

154

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

  • 应用程序的委托中设置applicationSupportsShakeToEdit属性:
  • 
        - (void)applicationDidFinishLaunching:(UIApplication *)application {
    
            application.applicationSupportsShakeToEdit = YES;
    
            [window addSubview:viewController.view];
            [window makeKeyAndVisible];
    }

  • 在View Controller中添加/覆盖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方法添加到您的View Controller中:
  • 
    - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
    {
        if (motion == UIEventSubtypeMotionShake)
        {
            // your code
        }
    }

    只是添加到Eran Talmor解决方案中:如果您已在新项目选择器中选择了这样的应用程序,则应使用Navigationcontroller而不是Viewcontroller。
    罗布

    我尝试使用此功能,但有时剧烈晃动或轻微晃动就停止了
    vinothp

    步骤1是现在的冗余,作为默认值applicationSupportsShakeToEditIS YES
    DonVaughn '16

    1
    为什么要[self resignFirstResponder];提前打电话[super viewWillDisappear:animated];?这似乎很奇怪。
    JaredH '16

    94

    首先,肯德尔(Kendall)在7月10日的答覆是即时的。

    现在...我想做类似的事情(在iPhone OS 3.0+中),只是在我自己的情况下才需要在整个应用程序中使用,以便在发生震动时可以提醒应用程序的各个部分。这就是我最终要做的。

    首先,我将UIWindow子类化。这很容易。创建一个具有以下接口的新类文件MotionWindow : UIWindow:(随意选择自己的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:放在与View Controller有关的地方。确保您对震动事件的响应知道它是否“已在进行中”。否则,如果将设备连续摇动两次,将会造成交通拥堵。这样,您可以忽略其他通知,直到您真正完成对原始通知的响应为止。

    此外,您也可以选择提示的关闭motionBeganmotionEnded。由你决定。就我而言,效果总是需要在设备静止后才发生(相对于它开始晃动时),因此我使用motionEnded。尝试两者,看看哪个更有意义...或同时检测/通知两者!

    这里还有一个(好奇吗?)观察:请注意,此代码中没有急救人员管理的迹象。到目前为止,我仅使用Table View Controllers进行了尝试,并且一切似乎都可以很好地协作!我不能保证其他情况。

    肯德尔等。al-谁能说出为什么UIWindow子类可能会这样?是因为窗口位于食物链的顶部吗?


    1
    那真是太奇怪了。它按原样工作。希望这不是“尽管工作”的情况!请让我知道您在自己的测试中发现的内容。
    Joe D'Andrea

    我宁愿将此子类化为常规UIView的子类,尤其是因为您只需要将此子类放在一个地方,因此放入窗口子类似乎是很自然的选择。
    毛茸茸的青蛙,

    谢谢jdandrea,我使用您的方法,但摇晃多次!我只希望用户可以摇晃一次(以为在那里摇晃)...!我该如何处理?我实现这样的东西。我像这样实现我的代码:i-phone.ir/forums/uploadedpictures/… ----但是问题是应用程序在应用程序的生命周期中只摇了1次!不仅查看
    Momi 2010年

    1
    Momeks:如果您需要在设备处于静止状态(摇晃后)后发出通知,请使用motionEnded而不是motionBegan。那应该做到的!
    Joe D'Andrea 2010年

    1
    @Joe D'Andrea-它按原样工作,因为UIWindow在响应者链中。如果链上的某个事物拦截并消耗了这些事件,它将不会收到它们。 developer.apple.com/library/ios/DOCUMENTATION/EventHandling/...
    DougW

    34

    我偶然发现了这篇文章,希望能实现“摇晃”的实现。尽管我一直在寻找一些需要更多“摇晃动作”才能触发的东西,但是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)];

    如何像全景一样检测设备的移动?
    user2526811 2013年

    如果iPhone仅沿X方向移动,如何识别抖动?我不想检测Y或Z方向的抖动。
    曼森

    12

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

    在GLPaint示例中AppController.m底部的accelerometer:didAccelerate:方法中有不错的示例代码,该示例可从iPhone开发人员网站上获得。


    12

    在带有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!")
        }
    }

    你试过了吗?我认为要在后台运行应用程序时使其正常工作将需要一些额外的工作。
    nhgrif

    Nope ..很快就会..,但是如果您注意到iPhone 6s,它具有此“拾取唤醒屏幕”功能,当您拿起设备时,屏幕会变亮一段时间。我需要捕捉这种行为
    NR5

    9

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

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

    还要在接口中的相应代码中设置。即:

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


    如何像全景一样检测设备的移动?
    user2526811 2013年

    如果iPhone仅沿X方向移动,如何识别抖动?我不想检测Y或Z方向的抖动。
    曼森


    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 
         }

    5

    很抱歉将其发布为答案而不是评论,但是您可以看到我是Stack Overflow的新手,因此我还没有足够的声誉发表评论!

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

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


    这不能为问题提供答案。要批评或要求作者澄清,请在其帖子下方发表评论。
    Alex Salauyou 2015年

    @SashaSalauyou我在第一句话中明确指出,当时我没有足够的声誉来发表评论
    Newtz 2015年

    抱歉。5
    岁以下

    5

    首先,我知道这是一个过时的帖子,但是仍然有用,而且我发现投票率最高的两个答案没有尽早发现这种动摇。这是怎么做的:

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

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

    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,这可能会比较棘手,我不知道您在应用程序委托中确切需要的代码。


    是的,你可以得到在你的基类@implementation,因为Xcode的5
    mxcl

    这可行,我在几个应用程序中都使用了它。所以不确定为什么它被否决了。
    mxcl 2014年

    像魅力一样运作。如果您想知道,可以覆盖这样的窗口类:stackoverflow.com/a/10580083/709975
    Edu

    应用程序在后台运行时可以正常工作吗?如果没有其他想法,如何在后台进行检测?
    nr5

    如果您设置了背景模式,这可能会起作用。
    mxcl

    1

    只需使用这三种方法即可

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

    有关详细信息,您可以在此处检查完整的示例代码


    1

    基于第一个答案的迅捷版本!

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

    -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
    By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
    Licensed under cc by-sa 3.0 with attribution required.