Как определить, когда кто-то трясет iPhone?

Я хочу реагировать, когда кто-то трясет iPhone. Меня не особо волнует, как они его трясут, просто то, что его энергично размахивали на долю секунды. Кто-нибудь знает, как это обнаружить?

17 ответов

Решение

В 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.

Из моего приложения 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

Гистерезис предотвращает многократное срабатывание события встряхивания, пока пользователь не остановит встряхивание.

Я наконец заставил его работать, используя примеры кода из этого руководства Undo/Redo Manager.
Это именно то, что вам нужно сделать:

  • Установите свойство applicationSupportsShakeToEdit в делегате приложения:
  • 
        - (void)applicationDidFinishLaunching:(UIApplication *)application {
    
            application.applicationSupportsShakeToEdit = YES;
    
            [window addSubview:viewController.view];
            [window makeKeyAndVisible];
    }
    

  • Добавить / переопределить canBecomeFirstResponder, viewDidAppear: и 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
        }
    }
    

    Во-первых, ответ Кендалла 10 июля - точный.

    Теперь... Я хотел сделать что-то подобное (в iPhone OS 3.0+), только в моем случае я хотел, чтобы это было в масштабе всего приложения, чтобы я мог предупреждать различные части приложения, когда происходит сотрясение. Вот что я в итоге сделал.

    Во-первых, я подклассифицировал UIWindow. Это легко peasy. Создайте новый файл класса с интерфейсом, таким как MotionWindow : UIWindow (не стесняйтесь выбирать свой, естественно). Добавьте метод так:

    - (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: в отношении контроллеров представления. Убедитесь, что ваш ответ на событие встряхивания знает, "оно уже выполняется" или нет. В противном случае, если устройство дважды встряхнуть подряд, у вас возникнет пробка. Таким образом, вы можете игнорировать другие уведомления, пока вы действительно не ответите на исходное уведомление.

    Кроме того: Вы можете отключить motionBegan против motionEnded. Тебе решать. В моем случае эффект всегда должен иметь место после того, как устройство находится в состоянии покоя (по сравнению с тем, когда оно начинает дрожать), поэтому я использую motionEnded. Попробуйте оба и посмотрите, какой из них имеет больше смысла... или обнаружите / сообщите обоим!

    Еще одно (любопытное?) Наблюдение: обратите внимание, что в этом коде нет признаков управления первым респондентом. До сих пор я пробовал это только с контроллерами табличного представления, и все, кажется, прекрасно работает вместе! Я не могу поручиться за другие сценарии, хотя.

    Kendall, et. al - может кто-нибудь говорить, почему это может быть так для подклассов UIWindow? Это потому, что окно находится наверху пищевой цепи?

    Я наткнулся на этот пост в поисках "потрясающей" реализации. Ответ millenomi сработал хорошо для меня, хотя я искал что-то, что требовало немного более "дрожащего действия", чтобы сработать. Я заменил на логическое значение int shakeCount. Я также переопределил метод L0AccelerationIsShaking() в Objective-C. Вы можете настроить количество встряхивания, необходимое для настройки количества, добавленного в 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)];
    

    В iOS 8.3 (возможно, ранее) с Swift это так же просто, как переопределение motionBegan или же 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!")
        }
    }
    

    Вам необходимо проверить акселерометр с помощью акселерометра:didAccelerate: метод, который является частью протокола UIAccelerometerDelegate, и проверить, превышают ли значения пороговое значение для количества движения, необходимого для встряхивания.

    В акселерометре есть хороший пример кода:didAccelerate: метод в нижней части AppController.m в примере GLPaint, который доступен на сайте разработчиков iPhone.

    Это основной код делегата, который вам нужен:

    #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

    Добавьте следующие методы в файл 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 
         }
    

    Посмотрите на пример GLPaint.

    http://developer.apple.com/library/ios/

    Извините, что опубликовал это как ответ, а не как комментарий, но, как вы можете видеть, я новичок в Stack Overflow, и поэтому я еще недостаточно авторитетен, чтобы оставлять комментарии!

    Во всяком случае, я повторяю @cire, чтобы убедиться, что установил статус первого респондента, когда представление является частью иерархии представлений. Так что настройте статус первого респондента в ваших контроллерах представления viewDidLoad метод не будет работать, например. И если вы не уверены, работает ли он [view becomeFirstResponder] возвращает вам логическое значение, которое вы можете проверить.

    Еще один момент: вы можете использовать контроллер представления для захвата события встряхивания, если не хотите создавать подкласс UIView без необходимости. Я знаю, что это не так уж много хлопот, но все же есть возможность. Просто переместите фрагменты кода, которые Кендалл поместил в подкласс UIView, в свой контроллер и отправьте becomeFirstResponder а также resignFirstResponder сообщения для self вместо подкласса UIView.

    Во-первых, я знаю, что это старый пост, но он все еще актуален, и я обнаружил, что два ответа с наибольшим количеством голосов не выявили сотрясение как можно раньше. Вот как это сделать:

    1. Свяжите CoreMotion с вашим проектом на этапах сборки цели:CoreMotion
    2. В вашем ViewController:

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

    Самое простое решение - получить новое корневое окно для вашего приложения:

    @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];
        //…
    }
    

    Если вы используете раскадровку, это может быть сложнее, я не знаю точно, какой код вам понадобится в делегате приложения.

    Версия Swiftease, основанная на самом первом ответе!

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

    Просто используйте эти три метода, чтобы сделать это

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

    для деталей вы можете проверить полный пример кода там

    В Swift 5 вы можете захватывать движение и проверять

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

    Чтобы включить это приложение, я создал категорию в UIWindow:

    @implementation UIWindow (Utils)
    
    - (BOOL)canBecomeFirstResponder
    {
        return YES;
    }
    
    - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
    {
        if (motion == UIEventSubtypeMotionShake) {
            // Do whatever you want here...
        }
    }
    
    @end
    
    Другие вопросы по тегам