Jak wykryć, kiedy ktoś potrząśnie iPhone'em?

340

Chcę zareagować, gdy ktoś potrząśnie iPhone'em. Nie obchodzi mnie szczególnie, jak się nim potrząsają, tylko to, że machał energicznie przez ułamek sekundy. Czy ktoś wie, jak to wykryć?

Josh Gagnon
źródło

Odpowiedzi:

292

W 3.0 jest teraz łatwiejszy sposób - dołącz do nowych zdarzeń ruchu.

Główną sztuczką jest to, że musisz mieć UIView (nie UIViewController), który jako pierwszy odpowiada, aby otrzymywać komunikaty o zdarzeniach wstrząsających. Oto kod, którego możesz użyć w dowolnym UIView, aby uzyskać zdarzenia wstrząsania:

@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

Możesz łatwo przekształcić dowolne UIView (nawet widoki systemowe) w widok, który może uzyskać zdarzenie wstrząśnięcia, po prostu podklasując widok tylko tymi metodami (a następnie wybierając ten nowy typ zamiast typu podstawowego w IB lub używając go podczas przydzielania widok).

W kontrolerze widoku chcesz ustawić ten widok, aby stał się pierwszą odpowiedzią:

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

Nie zapominaj, że jeśli masz inne widoki, które stają się pierwszymi respondentami na podstawie działań użytkownika (np. Pasek wyszukiwania lub pole wprowadzania tekstu), musisz także przywrócić status pierwszego odpowiadającego widoku wstrząsającego, gdy inny widok zrezygnuje!

Ta metoda działa, nawet jeśli ustawisz applicationSupportsShakeToEdit na NIE.

Kendall Helmstetter Gelner
źródło
5
To nie do końca działało: musiałem zastąpić kontroler viewDidAppearzamiast viewWillAppear. Nie jestem pewien dlaczego; może widok musi być widoczny, zanim będzie mógł zrobić wszystko, aby zacząć otrzymywać zdarzenia wstrząsania?
Kristopher Johnson,
1
Jest to łatwiejsze, ale niekoniecznie lepsze. Jeśli chcesz wykryć długi, ciągły wstrząs, takie podejście nie jest przydatne, ponieważ iPhone lubi strzelać, motionEndedzanim wstrząs rzeczywiście się zatrzyma. Tak więc, stosując to podejście, otrzymujesz rozłączną serię krótkich wstrząsów zamiast jednego długiego. Druga odpowiedź działa w tym przypadku znacznie lepiej.
aroth
3
@Kendall - UIViewControllers implementują UIResponder i znajdują się w łańcuchu odpowiadającym. Najwyższe UIWindow jest również. developer.apple.com/library/ios/DOCUMENTATION/EventHandling/…
DougW
1
[super respondsToSelector:nie zrobi tego, co chcesz, ponieważ jest to równoważne z połączeniem, [self respondsToSelector:które powróci YES. Potrzebujesz tego [[ShakingView superclass] instancesRespondToSelector:.
Vadim Yelagin
2
Zamiast używać: if (event.subtype == UIEventSubtypeMotionShake) możesz użyć: if (motion == UIEventSubtypeMotionShake)
Hashim Akhtar
179

Z mojej aplikacji 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

Histereza zapobiega wielokrotnemu wyzwalaniu zdarzenia wstrząsania, dopóki użytkownik nie zatrzyma wstrząsania.

millenomi
źródło
7
Uwaga: Poniżej dodałem odpowiedź przedstawiającą łatwiejszą metodę dla wersji 3.0.
Kendall Helmstetter Gelner,
2
Co się stanie, jeśli potrząśnie nim dokładnie na określonej osi?
jprete
5
Najlepsza odpowiedź, ponieważ iOS 3.0 motionBegani motionEndedwydarzenia nie są bardzo precyzyjne ani dokładne pod względem wykrywania dokładnego początku i końca wstrząsu. Takie podejście pozwala być tak precyzyjnym, jak chcesz.
aroth
2
UIAccelerometerDelegat akceleracyjny: didAccelerate: został przestarzały w iOS5.
Yantao Xie,
Ta druga odpowiedź jest lepsza i szybsza do wdrożenia stackoverflow.com/a/2405692/396133
Abramodj
154

W końcu udało mi się go uruchomić, korzystając z przykładów kodu z tego samouczka Menedżera cofania / ponawiania .
To jest dokładnie to, co musisz zrobić:

  • Ustaw właściwość applicationSupportsShakeToEdit w delegacie aplikacji:
  • 
        - (void)applicationDidFinishLaunching:(UIApplication *)application {
    
            application.applicationSupportsShakeToEdit = YES;
    
            [window addSubview:viewController.view];
            [window makeKeyAndVisible];
    }

  • Dodaj / Zastąp canBecomeFirstResponder , viewDidAppear: i viewWillDisappear: metody w kontrolerze widoku:
  • 
    -(BOOL)canBecomeFirstResponder {
        return YES;
    }
    
    -(void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
        [self becomeFirstResponder];
    }
    
    - (void)viewWillDisappear:(BOOL)animated {
        [self resignFirstResponder];
        [super viewWillDisappear:animated];
    }

  • Dodaj metodę motionEnded do kontrolera widoku:
  • 
    - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
    {
        if (motion == UIEventSubtypeMotionShake)
        {
            // your code
        }
    }
    Eran Talmor
    źródło
    Wystarczy dodać do rozwiązania Eran Talmor: powinieneś użyć kontrolera nawigacji zamiast kontrolera widoku, jeśli wybierzesz taką aplikację w nowym wyborze projektu.
    Rob
    Próbowałem użyć tego, ale czasami ciężki shake lub lekki shake to po prostu przestało
    vinothp
    Krok 1 jest teraz zbędny, ponieważ domyślną wartością applicationSupportsShakeToEditjest YES.
    DonVaughn
    1
    Po co dzwonić [self resignFirstResponder];przed [super viewWillDisappear:animated];? To wydaje się dziwne.
    JaredH
    94

    Po pierwsze, odpowiedź Kendalla z 10 lipca jest natychmiastowa.

    Teraz ... chciałem zrobić coś podobnego (w iPhone OS 3.0+), tylko w moim przypadku chciałem, aby obejmowała całą aplikację, aby móc ostrzegać różne części aplikacji, gdy wystąpi wstrząs. Oto co skończyłem.

    Najpierw podklasowałem UIWindow . To jest łatwe. Utwórz nowy plik klasy z interfejsem takim jak MotionWindow : UIWindow(możesz wybrać własny, natch). Dodaj taką metodę:

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

    Zmień @"DeviceShaken"na wybraną nazwę powiadomienia. Zapisz plik.

    Teraz, jeśli używasz pliku MainWindow.xib (zapasowe szablony Xcode), wejdź tam i zmień klasę swojego obiektu Window z UIWindow na MotionWindow lub jakkolwiek to nazwałeś . Zapisz xib. Jeśli skonfigurujesz UIWindow programowo , użyj tam nowej klasy Window.

    Teraz Twoja aplikacja korzysta ze specjalistycznej klasy UIWindow . Gdziekolwiek chcesz otrzymywać informacje o shake'u, zarejestruj się, aby otrzymywać powiadomienia! Lubię to:

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

    Aby usunąć siebie z roli obserwatora:

    [[NSNotificationCenter defaultCenter] removeObserver:self];

    Wyświetlam mój widok WillAppear: i viewWillDisappear: przypadku kontrolerów View. Upewnij się, że Twoja reakcja na zdarzenie wstrząsowe wie, czy „już trwa”, czy nie. W przeciwnym razie, jeśli urządzenie zostanie wstrząśnięte dwa razy pod rząd, będziesz miał mały korek. W ten sposób możesz zignorować inne powiadomienia, dopóki nie skończysz odpowiadać na oryginalne powiadomienie.

    Ponadto: możesz wybrać cue off z motionBegan vs. motionEnded . To zależy od Ciebie. W moim przypadku efekt zawsze musi mieć miejsce po zatrzymaniu urządzenia (w przeciwieństwie do momentu, w którym zaczyna się trząść), więc używam motionEnded . Spróbuj obu i zobacz, który z nich jest bardziej sensowny ... lub wykryj / powiadom o obu!

    Jeszcze jedna (ciekawa?) Obserwacja tutaj: zauważ, że w tym kodzie nie ma śladu zarządzania pierwszą odpowiedzią. Do tej pory próbowałem tego tylko z kontrolerami widoku tabeli i wszystko wydaje się działać całkiem dobrze razem! Nie mogę jednak ręczyć za inne scenariusze.

    Kendall i in. al - czy ktoś może mówić, dlaczego tak jest w przypadku podklas UIWindow ? Czy to dlatego, że okno znajduje się na górze łańcucha pokarmowego?

    Joe D'Andrea
    źródło
    1
    To jest takie dziwne. Działa jak jest. Mamy jednak nadzieję, że nie jest to przypadek „działania wbrew sobie”! Daj mi znać, czego się dowiesz we własnych testach.
    Joe D'Andrea,
    Wolę to od podklasowania zwykłego UIView, zwłaszcza że potrzebujesz go tylko w jednym miejscu, więc umieszczenie podklasy okien wydaje się naturalne.
    Shaggy Frog
    Dziękuję jdandrea, używam twojej metody, ale drżenie dostaje się wiele razy !!! Chcę tylko, aby użytkownicy mogli wstrząsnąć jeden raz (w widoku, w którym trzęsienie się tam dzieje) ...! jak sobie z tym poradzić? wdrażam coś takiego. wdrażam mój kod smht w następujący sposób: i-phone.ir/forums/uploadedpictures/… ---- ale problem polega na tym, że aplikacja trzęsie tylko 1 raz na całe życie aplikacji! nie tylko widok
    Momi
    1
    Momeks: jeśli chcesz, aby powiadomienie pojawiło się, gdy urządzenie będzie w spoczynku (po wstrząśnięciu), użyj motionEnded zamiast motionBegan. To powinno to zrobić!
    Joe D'Andrea
    1
    @Joe D'Andrea - Działa tak, jak jest, ponieważ UIWindow znajduje się w łańcuchu odpowiadającym. Gdyby coś wyższego łańcucha przechwyciło i pochłonęło te zdarzenia, nie otrzymałoby ich. developer.apple.com/library/ios/DOCUMENTATION/EventHandling/…
    DougW
    34

    Natknąłem się na ten post w poszukiwaniu „wstrząsającej” implementacji. Odpowiedź millenomi działała dla mnie dobrze, chociaż szukałem czegoś, co wymagałoby nieco więcej „wstrząsania”, aby uruchomić. Zamieniłem na wartość logiczną int shakeCount. Ponownie wdrożyłem metodę L0AccelerationIsShaking () w Objective-C. Możesz dostosować ilość wstrząsów wymaganą przez ulepszenie ilości dodanej do shakeCount. Nie jestem pewien, czy znalazłem optymalne wartości, ale wydaje się, że jak dotąd działa dobrze. Mam nadzieję, że to pomoże komuś:

    - (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: Ustawiłem interwał aktualizacji na 1/15 sekundy.

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

    źródło
    Jak mogę wykryć ruch urządzenia tak jak panoramę?
    user2526811
    Jak rozpoznać wstrząs, jeśli iPhone jest przesuwany tylko w kierunku X? Nie chcę wykryć wstrząsu, jeśli w kierunku Y lub Z.
    Manthan
    12

    Musisz sprawdzić akcelerometr za pomocą akcelerometru: didAccelerate: metoda będąca częścią protokołu UIAccelerometerDelegate i sprawdzić, czy wartości przekraczają próg dla ilości ruchu potrzebnej do wstrząśnięcia.

    W akcelerometrze znajduje się przyzwoity przykładowy kod: didAccelerate: metoda na dole AppController.m w przykładzie GLPaint, który jest dostępny na stronie programisty iPhone'a.

    Dave Verwer
    źródło
    12

    W iOS 8.3 (być może wcześniejszym) z Swift jest to tak proste, jak zastąpienie metod motionBeganlub motionEndedw kontrolerze widoku:

    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
    źródło
    Próbowałeś? Zakładam, że aby to zadziałało, gdy aplikacja jest w tle, potrzeba trochę dodatkowej pracy.
    nhgrif
    Nie… wkrótce… ale jeśli zauważysz iPhone'a 6s, ma on funkcję „podnoszenia do wybudzania ekranu”, dzięki której ekran rozjaśnia się na pewien czas po podniesieniu urządzenia. Muszę uchwycić to zachowanie
    nr5
    9

    To jest podstawowy kod delegowany, którego potrzebujesz:

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

    Ustaw także odpowiedni kod w interfejsie. to znaczy:

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

    Benjamin Ortuzar
    źródło
    Jak mogę wykryć ruch urządzenia tak jak panoramę?
    user2526811
    Jak rozpoznać wstrząs, jeśli iPhone jest przesuwany tylko w kierunku X? Nie chcę wykryć wstrząsu, jeśli w kierunku Y lub Z.
    Manthan
    7

    Dodaj następujące metody w pliku ViewController.m, działa poprawnie

        -(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 
         }
    Himanshu Mahajan
    źródło
    5

    Przepraszam, że opublikowałem to jako odpowiedź, a nie jako komentarz, ale jak widać, jestem nowy w stosie przepełnienia stosu, więc nie jestem jeszcze wystarczająco wiarygodny, aby publikować komentarze!

    Tak czy inaczej, po raz drugi @ zastanawiam się nad ustawieniem statusu pierwszego odpowiadającego, gdy widok jest częścią hierarchii widoku. Tak więc na przykład ustawienie statusu pierwszego odpowiadającego w viewDidLoadmetodzie kontrolerów widoku nie zadziała. A jeśli nie masz pewności, czy działa, [view becomeFirstResponder]zwraca wartość logiczną, którą możesz przetestować.

    Kolejny punkt: Państwo może użyć kontrolera widoku, aby uchwycić zdarzenia wstrząsnąć, jeśli nie chcesz utworzyć podklasę UIView niepotrzebnie. Wiem, że to nie tyle kłopotów, ale wciąż istnieje opcja. Po prostu przenieś fragmenty kodu, które Kendall umieścił w podklasie UIView do swojego kontrolera i wysyłaj wiadomości becomeFirstResponderoraz resignFirstResponderdo selfpodklasy UIView.

    Newtz
    źródło
    To nie daje odpowiedzi na pytanie. Aby skrytykować lub poprosić autora o wyjaśnienia, zostaw komentarz pod postem.
    Alex Salauyou,
    @SashaSalauyou Wyraźnie stwierdziłem w pierwszym zdaniu, że nie mam wystarczającej reputacji, aby opublikować komentarz w tym czasie
    Newtz
    Przepraszam. Możliwe jest, aby odpowiedź 5-letnia trafiła do kolejki recenzji))
    Alex Salauyou
    5

    Po pierwsze, wiem, że jest to stary post, ale nadal jest aktualny i stwierdziłem, że dwie najwyżej ocenione odpowiedzi nie wykryły wstrząsu tak wcześnie, jak to możliwe . Oto jak to zrobić:

    1. Połącz CoreMotion z projektem w fazach kompilacji celu: CoreMotion
    2. W twoim ViewController:

      - (BOOL)canBecomeFirstResponder {
          return YES;
      }
      
      - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
      {
          if (motion == UIEventSubtypeMotionShake) {
              // Shake detected.
          }
      }
    Dennis
    źródło
    4

    Najłatwiejszym rozwiązaniem jest uzyskanie nowego okna głównego dla aplikacji:

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

    Następnie w aplikacji deleguj:

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

    Jeśli używasz Storyboard, może to być trudniejsze, nie wiem dokładnie, jaki kod będzie potrzebny w delegacie aplikacji.

    mxcl
    źródło
    I tak, można czerpać swoją klasę bazową w @implementationod Xcode 5.
    mxcl
    To działa, używam go w kilku aplikacjach. Nie jestem pewien, dlaczego został odrzucony.
    mxcl
    działał jak urok. Jeśli się zastanawiasz, możesz zastąpić klasę okna w ten sposób: stackoverflow.com/a/10580083/709975
    Edu
    Czy to zadziała, gdy aplikacja będzie działać w tle? Jeśli nie jakiś inny pomysł, jak wykryć, kiedy jesteś w tle?
    nr5
    Prawdopodobnie zadziała, jeśli masz ustawiony tryb tła.
    mxcl
    1

    Wystarczy użyć tych trzech metod, aby to zrobić

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

    Szczegółowe informacje można sprawdzić kompletny przykładowy kod na nie

    Meszadi
    źródło
    1

    Swiftease wersja oparta na pierwszej odpowiedzi!

    override func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?) {
        if ( event?.subtype == .motionShake )
        {
            print("stop shaking me!")
        }
    }
    użytkownik3069232
    źródło
    -1

    Aby włączyć tę aplikację, utworzyłem kategorię w UIWindow:

    @implementation UIWindow (Utils)
    
    - (BOOL)canBecomeFirstResponder
    {
        return YES;
    }
    
    - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
    {
        if (motion == UIEventSubtypeMotionShake) {
            // Do whatever you want here...
        }
    }
    
    @end
    Mikrofon
    źródło