iOS Motion Detection: Motion Detection Sensitivity

2020-05-26 12:27发布

问题:

I have a simple question. I'm trying to detect when a user shakes the iPhone. I have the standard code in place to detect the motion and this works no problem. However, in testing this on my actual phone, I've realized that you have to shake the device quite hard to get the motion detection to trigger. I would like to know if there is a way to implement a level of sensitivity checking. For example, a way to detect if a user lightly shakes the device or somewhere between light and hard shake. This will be targeted towards iOS 7 so any tips or advice that is not deprecated from older iOS version would be greatly appreciated. I've done my googling but have yet to find any good solutions to this problem (If there are any.)

Thanks!

-(void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
    if(motion == UIEventSubtypeMotionShake)
    {
       //Detected motion, do something about it 
       //at this point.
    }
}

-(BOOL)canBecomeFirstResponder
{
    return YES;
}

-(void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [self becomeFirstResponder];
}

-(void)viewWillDisappear:(BOOL)animated
{
    [self resignFirstResponder];
    [super viewWillDisappear:animated];
}

回答1:

Use core motion. Link your binary with CoreMotion framework. Include #import in your class. Create an instance of CMMotionManager. Set the deviceMotionUpdateInterval property to a suitable value. Then call startDeviceMotionUpdatesToQueue. You will get continuous updates inside the block, which includes acceleration, magnetic field, rotation, etc. You will get the data you require. One thing to be taken care of is that the update shall be so rapid if the interval is too small, and hence you will have to employ suitable logic to handle the same.



回答2:

Here is the solution I found. This works well but you do have to play with the deviceMotionUpdateInterval time value as well as the accelerationThreshold which can be tricky to get a fine balancing act for a actual "light shake" vs "picking up the phone and moving it closer to your face etc..." There might be better ways but here is one to start. Inside of my view didLoad I did something like this:

#import <CoreMotion/CoreMotion.h> //do not forget to link the CoreMotion framework to your project
#define accelerationThreshold  0.30 // or whatever is appropriate - play around with different values

-(void)viewDidLoad
{
      CMMotionManager *motionManager; 

      motionManager = [[CMMotionManager alloc] init];
      motionManager.deviceMotionUpdateInterval = 1;

      [motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue]  withHandler:^(CMDeviceMotion *motion, NSError *error)
             {
                [self motionMethod:motion];
             }];
}

-(void)motionMethod:(CMDeviceMotion *)deviceMotion
{
    CMAcceleration userAcceleration = deviceMotion.userAcceleration;
    if (fabs(userAcceleration.x) > accelerationThreshold
        || fabs(userAcceleration.y) > accelerationThreshold
        || fabs(userAcceleration.z) > accelerationThreshold)
        {
           //Motion detected, handle it with method calls or additional
           //logic here.
           [self foo];
        }
}


回答3:

This is a swift version based on zic10's answer, with the addition of a flag that prevents getting a few extra calls to your motion handler even when the first line in that handler is motionManager.stopDeviceMotionUpdates().

Also, a value of around 3.0 can be useful if you want to ignore the shake, but detect a bump. I found 0.3 to be way too low as it ended up being more like "detect move". In my tests, the ranges were more like:

  • 0.75 - 2.49 is a better range for shake sensitivity
  • 2.5 - 5.0 is a good range for "ignore shake, detect bump"

Here is the complete view controller for an Xcode single VC template:

import UIKit
import CoreMotion

class ViewController: UIViewController {

    lazy var motionManager: CMMotionManager = {
        return CMMotionManager()
    }()

    let accelerationThreshold = 3.0

    var handlingShake = false

    override func viewWillAppear(animated: Bool) {
        handlingShake = false
        motionManager.startDeviceMotionUpdatesToQueue(NSOperationQueue.currentQueue()!) { [weak self] (motion, error) in
            if
                let userAcceleration = motion?.userAcceleration,
                let _self = self {

                print("\(userAcceleration.x) / \(userAcceleration.y)")

                if (fabs(userAcceleration.x) > _self.accelerationThreshold
                    || fabs(userAcceleration.y) > _self.accelerationThreshold
                    || fabs(userAcceleration.z) > _self.accelerationThreshold)
                {
                    if !_self.handlingShake {
                        _self.handlingShake = true
                        _self.handleShake();
                    }
                }

            } else {
                print("Motion error: \(error)")
            }
        }
    }

    override func viewWillDisappear(animated: Bool) {
        // or wherever appropriate
        motionManager.stopDeviceMotionUpdates()
    }

    func handleShake() {
        performSegueWithIdentifier("showShakeScreen", sender: nil)
    }

}

And the storyboard I used for this test looks like this:

It's also worth noting that CoreMotion is not testable in the simulator. Because of this constraint you may still find it worthwhile to additionally implement the UIDevice method of detecting motion shake. This would allow you to manually test shake in the simulator or give UITests access to shake for testing or tools like fastlane's snapshot. Something like:

class ViewController: UIViewController {

    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        becomeFirstResponder()
    }

    override func canBecomeFirstResponder() -> Bool {
        return true
    }

    override func motionEnded(motion: UIEventSubtype, withEvent event: UIEvent?) {
        if TARGET_OS_SIMULATOR != 0 {
            if event?.subtype == .MotionShake {
                // do stuff
            }
        }
    }

}

And then use Ctrl-Cmd-Z to test shake in the simulator.



回答4:

Heres how I did this using Swift 3.

Import CoreMotion and create an instance

import CoreMotion
let motionManager = CMMotionManager()

On ViewDidLoad or wherever you want to start checking for updates:

 motionManager.startDeviceMotionUpdates(to: OperationQueue.current!, withHandler:{
            deviceManager, error in
            if(error == nil){
                if let mgr = deviceManager{
                    self.handleMotion(rate: mgr.rotationRate)
                }
            }
        })

This function takes the rotation rate and gets a sum for the absolute values for x,y and z movements

  func handleMotion(rate: CMRotationRate){
        let totalRotation = abs(rate.x) + abs(rate.y) + abs(rate.z)

        if(totalRotation > 20) {//Play around with the number 20 to find the optimal level for your case
            start()
        }else{
       print(totalRotation)
        }
    }

func start(){

//The function you want to trigger when the device is rotated

}