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

340

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

Джош Ганьон
источник

Ответы:

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, или используя его при выделении Посмотреть).

В контроллере представления вы хотите установить это представление, чтобы стать первым респондентом:

- (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
3
@Kendall - UIViewControllers реализуют UIResponder и находятся в цепочке респондента. Самое верхнее окно UIWindow также. developer.apple.com/library/ios/DOCUMENTATION/EventHandling/…
DougW
1
[super respondsToSelector:не будет делать то, что вы хотите, так как это эквивалентно вызову, [self respondsToSelector:который вернется YES. Что тебе нужно [[ShakingView superclass] instancesRespondToSelector:?
Вадим Елагин
2
Вместо использования: if (event.subtype == UIEventSubtypeMotionShake) вы можете использовать: if (motion == UIEventSubtypeMotionShake)
Хашим Ахтар
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

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

millenomi
источник
7
Примечание. Ниже я добавил ответ, представляющий более простой метод для 3.0.
Кендалл Хельмштеттер Гелнер
2
Что произойдет, если они встряхнут точно на определенной оси?
jprete
5
Лучший ответ, потому что iOS 3.0 motionBeganи motionEndedсобытия не очень точны или точны с точки зрения определения точного начала и конца дрожания. Такой подход позволяет вам быть настолько точным, насколько вы хотите.
aroth
2
UIAccelerometerDelegate акселерометр: didAccelerate: устарел в iOS5.
Янтао Се
Этот другой ответ лучше и быстрее реализовать stackoverflow.com/a/2405692/396133
Abramodj
154

Я наконец заставил его работать, используя примеры кода из этого руководства 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
        }
    }
    Эран Талмор
    источник
    Просто чтобы добавить в решение Eran Talmor: вы должны использовать навигационный контроллер вместо viewcontroller, если вы выбрали такое приложение в новом окне выбора проекта.
    Роб
    Я пытался использовать это, но иногда сильное или легкое встряхивание просто прекращалось
    vinothp
    Шаг 1 теперь избыточен, так как значение по умолчанию applicationSupportsShakeToEdit- YES.
    DonVaughn
    1
    Зачем звонить [self resignFirstResponder];раньше [super viewWillDisappear:animated];? Это кажется странным.
    JaredH
    94

    Во-первых, ответ Кендалла 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 ? Это потому, что окно находится наверху пищевой цепи?

    Джо д'Андреа
    источник
    1
    Вот что странно. Работает как есть. Надеюсь, это не случай "работать вопреки себе", хотя! Пожалуйста, дайте мне знать, что вы обнаружите в своем тестировании.
    Джо д'Андреа
    Я предпочитаю это подклассу обычного UIView, тем более что вам нужно, чтобы он существовал только в одном месте, поэтому помещение подкласса окна кажется естественным соответствием.
    Лохматая лягушка
    Спасибо jdandrea, я использую ваш метод, но встряхиваю, получая несколько раз !!! я просто хочу, чтобы пользователи могли встряхивать один раз (из-за того, что там происходит встряхивание) ...! как я могу справиться с этим? Я реализую что-то вроде этого. я реализую свой код smht следующим образом: i-phone.ir/forums/uploadedpictures/… ---- но проблема в том, что приложение трясет только 1 раз за жизнь приложения! не только просмотр
    Моми
    1
    Момекс: если вам нужно, чтобы уведомление появлялось после того, как устройство находится в состоянии покоя (после встряхивания), используйте motionEnded вместо motionBegan. Это должно сделать это!
    Джо д'Андреа
    1
    @Joe D'Andrea - работает как есть, потому что UIWindow находится в цепочке респондента. Если что-то выше по цепочке перехватит и поглотит эти события, оно не получит их. developer.apple.com/library/ios/DOCUMENTATION/EventHandling/…
    DougW
    34

    Я наткнулся на этот пост в поисках «потрясающей» реализации. Ответ 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)];

    источник
    Как я могу определить движение устройства, как на панораме?
    user2526811
    Как определить дрожание, если iphone перемещается только в направлении X? Я не хочу обнаруживать дрожание, если в направлении Y или Z.
    Manthan
    12

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

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

    Дейв Вервер
    источник
    12

    В 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!")
        }
    }
    nhgrif
    источник
    Вы пробовали это? Я бы предположил, что для того, чтобы заставить это работать, когда приложение находится в фоновом режиме, потребовалось бы немного дополнительной работы.
    nhgrif
    Нет ... скоро будет ... но если вы заметили 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
    Как определить дрожание, если iphone перемещается только в направлении X? Я не хочу обнаруживать дрожание, если в направлении Y или Z.
    Manthan
    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, и поэтому я еще недостаточно авторитетен, чтобы оставлять комментарии!

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

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

    Newtz
    источник
    Это не дает ответа на вопрос. Чтобы критиковать или запросить разъяснения у автора, оставьте комментарий под своим постом.
    Алекс Салаю
    @SashaSalauyou Я ясно сказал в самом первом предложении, что у меня не было достаточно репутации, чтобы
    оставить
    Прости. Здесь 5-летний ответ может попасть в очередь на просмотр))
    Алекс Салаю
    5

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

    1. Свяжите CoreMotion с вашим проектом на этапах сборки цели: 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];
        //…
    }

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

    mxcl
    источник
    И да, вы можете получить свой базовый класс в @implementationначиная с Xcode 5.
    mxcl
    Это работает, я использую его в нескольких приложениях. Так что не уверен, почему он был отклонен.
    mxcl
    работал как шарм. Если вам интересно, вы можете переопределить класс окна следующим образом: stackoverflow.com/a/10580083/709975
    Edu
    Будет ли это работать, когда приложение находится в фоновом режиме? Если нет другой идеи, как определить, когда в фоновом режиме?
    №5
    Это, вероятно, будет работать, если у вас установлен фоновый режим.
    mxcl
    1

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

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

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

    Машхади
    источник
    1

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

    override func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?) {
        if ( event?.subtype == .motionShake )
        {
            print("stop shaking me!")
        }
    }
    user3069232
    источник
    -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
    Майк
    источник