Delay in receiving device motion updates from CMMotionManager - ios

I have an app with a view that, when loaded, gets regular device motion updates (20 per second) and uses them to display things on the screen. This was working fine using the old (deprecated) UIAccelerometer code.
I have since ported it to use Core Motion and the CMMotionManager but am having the problem that every time (except the first time) one of these views is loaded, only one or two updates are received in the first second or so, and then they start being received as expected (20 per second).
Relevant code in the view:
- (void)viewDidLoad {
[super viewDidLoad];
// Register to receive acceleration from motion manager.
motionManager = [[CMMotionManager alloc] init];
motionManager.deviceMotionUpdateInterval = 0.05;
[motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue]
withHandler:^(CMDeviceMotion *motion, NSError *error) {
// Handle motion and update UI here.
}
];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// Stop the motion updates.
[motionManager stopDeviceMotionUpdates];
}
- (IBAction)exitView:(id)sender {
// Stop the motion updates.
[motionManager stopDeviceMotionUpdates];
// Pop the view from the navigation controller.
}
I've added some NSLog statements and can confirm that motion updates are being requested as soon as the view loads, and that the handler is only receiving updates as I described above.
Is there anything I'm missing that I can do to get the CoreMotion version to behave the same as the old version, i.e. without the delay?

Related

What is best iOS GPS (CLLocationManager) battery saving technique

Ive written a GPS app for iOS 10 a while back. Ive gotten it to the point where its stable and working as expected and now I want to go after
battery saving techniques. The app sends GPS locations every 5 minutes right now but I could be flexible on how often I send the GPS locations, as long
as they aren't sent too infrequently. My app requires 'best' or '10m' GPS location setting as well which I know uses a lot of power, but I cannot use the 3km setting per my requirements (which I know would save more power).
From what I gather and reading the docs and playing with code to make it work, it seems that there are 2 techniques:
set pausesLocationUpdatesAutomatically = YES in combination with UNLocationNotificationTrigger
allowDeferredLocationUpdatesUntilTraveled:timeout:
Number 2 apparently does not work at all based on this article in iOS 10 and 11.
Number 1 gives me sporadic results when I log it to my server. Sometimes it works, sometimes Ill get a single GPS point in the middle of nowhere (without seeing a pause or resume log message) and then later it starts to work, other times it does not seem to work at all.
Sometimes Ill see 2 pauses back to back within the same second.
Other times Ive seen it resume locations back to back (minutes apart) without ever seeing the pause log message. And other times I see the pause log message and minutes later (or a while later) resume GPS sends without seeing a 'resume' log message.
- (void) setGPSBatterySavingTrigger
{
if (IS_IOS_10_OR_LATER && ([g_prefs getGPSAuthStatus] == kCLAuthorizationStatusAuthorizedAlways))
{
CLLocationCoordinate2D center = CLLocationCoordinate2DMake(g_locMgr.getLastLocation.latitude, g_locMgr.getLastLocation.longitude);
CLRegion *region = nil;
float radius_m = 150.0;
region = [[CLCircularRegion alloc] initWithCenter:center radius:radius_m identifier:#"pauseLocationsCenter"];
if (!region) {
[logger log:TSERVLOG :#"pause radius failed at radius <%f>", radius_m];
return;
}
region.notifyOnEntry = NO;
region.notifyOnExit = YES;
UNLocationNotificationTrigger *trigger = [UNLocationNotificationTrigger
triggerWithRegion:region repeats:NO];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = PROG_NAME;
content.body = #"GPS Restarted";
//content.sound = [UNNotificationSound soundNamed:nil];
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:#"GPSTextNotify" content:content trigger:trigger];
[[UNUserNotificationCenter currentNotificationCenter] removeAllPendingNotificationRequests];
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError *error)
{
if (error.code)
{
[logger log:TSERVLOG :#"UNUserNotificationCenter addRequest FAILED <%#>", error.localizedDescription];
}
}];
}
}
- (void) locationManagerDidPauseLocationUpdates:(CLLocationManager *)manager
{
[logger log:TSERVLOG :#"===== JME GPS PauseLocationUpdates at <%#> <%f, %f>",
[mydate getCurrentDateTimeStr], self.getLastLocation.latitude, self.getLastLocation.longitude];
[self setGPSBatterySavingTrigger];
}
- (void) locationManagerDidResumeLocationUpdates:(CLLocationManager *)manager
{
[logger log:TSERVLOG :#"===== GPS ResumeLocationUpdates at <%#> <%f, %f>",
[mydate getCurrentDateTimeStr], self.getLastLocation.latitude, self.getLastLocation.longitude];
}
Bottom line questions:
Has anyone been able to use either of these to get battery saving + GPS working reliably in iOS 10+?
If not, what combination of things did you do to get it to work 100% of the time while saving battery life?
Am I missing another technique that does work? (NOTE my requirements cannot use the 3km setting)
Thank you for your input!

Device motion updates in background

I am trying to get device motion updates on an iPhone or iPad in the background using CMMotionManager. I have reviewed all previous posts on this topic and thought that I had code that would work. My app also uses background audio, and this works properly in the foreground and background. In Info.plist, I have background audio and background location updates enabled.
For testing purposes, I have declared "var motionManager = CMMotionManager()" in AppDelegate, and included the following code in didFinishLaunchingWithOptions:
motionManager.deviceMotionUpdateInterval = 0.10
let queue = NSOperationQueue()
motionManager.startDeviceMotionUpdatesToQueue(queue, withHandler: {
data, error in
let accelerationVector = sqrt(pow(data!.userAcceleration.x, 2) + pow(data!.userAcceleration.y, 2) + pow(data!.userAcceleration.z, 2))
print("\(accelerationVector)")
})
When I run the app on my device, the code executes in the foreground as expected, but when I press the home button, I get about 10 more readings before it stops. When I tap on the app icon, the readings start again. I have also placed breakpoints on the code in the handler, and get similar results.
What am I missing?
Do NOT trust the NSLog or other similar log output
I used to meet this problem. I want collect the motion data in background and I made a demo about this. I found I can get all the data and log when my app in active, but the xCode log console output nothing when the application in background.
I used to think the problem is the CoreMotion data can only be collected in foreground, but I am wrong. All the callbacks still working when application enter background, Just the NSLog stop telling me the data.
If you don't believe, just collect all the data into a NSMutableArray or other collections, then check the data collected when app in background.
e.g.
#property (nonatomic, strong) NSMutableArray *arrAltimeters;
...
[self.altimeter startRelativeAltitudeUpdatesToQueue:self.operationQueue withHandler:^(CMAltitudeData * _Nullable altitudeData, NSError * _Nullable error) {
[self.arrAltimeters addObject:altitudeData];
NSLog(#"Altimate data count = %ld", self.arrAltimeters.count);
}];
I have declared the motion manager in ViewDidlLoad
CMMotionManager *motionManager;
/*--Initialising Motion Manager--*/
motionManager = [[CMMotionManager alloc] init];
motionManager.deviceMotionUpdateInterval = 1.0;
As whenever the app goes in background the motionManager stop providing callbacks in this case , You need to restart the motionmanager when the app goes in background or in foreground.To do this we need to follow the process below:
1) we first need to Register for the app transition notification :
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(appDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(appDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];
2)Notification callback functions, in which you will restart the motion manager
-(void)appDidEnterBackground{
[self restartMotionUpdates];
}
-(void)appDidBecomeActive{
[self restartMotionUpdates];
}
3)This function which will restart the motion manager
-(void)restartMotionUpdates{
[motionManager stopDeviceMotionUpdates];
[motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMDeviceMotion *motion, NSError *error) {
NSLog(#"x=%f y=%f z=%f",fabs(motion.userAcceleration.x),fabs(motion.userAcceleration.y),fabs(motion.userAcceleration.z));
}];
}
After doing more research, I figured out the problem. I was using mpmusicplayercontroller to play background audio. This will play the audio in the background, but this does not keep my app running in the background. When I switched to using avplayer to play audio, the app runs in the background including the device motion updates.

Accessing the accelerometer in iOS while running in the background

I'm new to iOS programming. I'm wanting to write an app that has constant access to the accelerometer, and barometric pressure sensor of the iPhone 6, while running in the background.
In my research, I've found that people have done it using the Location Updates background mode, but this only worked for 10 minutes before the app was suspended (and this changed to 3 minutes in iOS 6?). I'd also read about using the audio background mode playing a mute sound as a way to work around that time limit. This seems like a substandard solution though.
I'm wondering then how an app something like Sleep Cycle does this, as it seems like it must have constant access to the accelerometer.
It can be done by using CoreMotion framework.
You have to import CoreMotion framework, then #import <CoreMotion/CoreMotion.h> in your appdelegate.
Here motionManager is object of CMMotionManager.
xData, yData, zData are double values to store accelerometer data.
if (motionManager ==nil) {
motionManager= [[CMMotionManager alloc]init];
}
[motionManager startAccelerometerUpdates];
[motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) {
xData = accelerometerData.acceleration.x;
yData = accelerometerData.acceleration.y;
zData = accelerometerData.acceleration.z;
}];
You have to do it in - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions.
You can then use these value of xData, yData, zData where ever you want by appdelegate object, even in background.

Getting CMAccelerometer data in background

Currently I'm able to get updates from the accelerometer using CMAccelerometer
_motionManager.accelerometerUpdateInterval = kUpdateInterval;
[_motionManager startAccelerometerUpdatesToQueue:self.queue withHandler:
^(CMAccelerometerData *accelerometerData, NSError *error) {
[(id) self setAcceleration:accelerometerData.acceleration];
[self performSelectorOnMainThread:#selector(update) withObject:nil waitUntilDone:NO];
}];
- (void)update {
... code to handle data ...
}
And this is working fine, however when I close my app (i.e. put it into the background not kill the instance), I no longer receive updates from my _motionManager. How do I handle this?
You can register your app to listen to location, and use CoreMotion. Then you can get accelerometer data in background.
Basically your app needs to be allowed to run in the background for other reasons.
Example:

Fetch GPS location for every 5 minutes only

In my Location Application implemented didUpdateToLocation method. This method called every second and provides location data. But I dont need to fetch location for every second, I need to fire this method every 5 minutes only. Is it possible to do this?
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//initialize location lisetener on Application startup
self.myLocationManager = [[CLLocationManager alloc]init];
self.myLocationManager.desiredAccuracy = kCLLocationAccuracyBest;
self.myLocationManager.delegate = self;
[self.myLocationManager startUpdatingLocation];
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
self.viewController = [[INNOViewController alloc] initWithNibName:#"INNOViewController_iPhone" bundle:nil];
} else {
self.viewController = [[INNOViewController alloc] initWithNibName:#"INNOViewController_iPad" bundle:nil];
}
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}
-(void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
/*
if(self.executingInBackground)
{
NSLog(#"Aplication running in background");
}
else
{
NSLog(#"Aplication NOT running in background");
}
*/
//NSLog(#"new location->%# and old location -> %#",newLocation,oldLocation);
NSString *urlAsString = #"http://www.apple.com";
NSURL *url=[NSURL URLWithString:urlAsString];
NSURLRequest *urlRequest=[NSURLRequest requestWithURL:url];
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if ([data length] > 0 && error == nil) {
NSString *html = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"Downloaded html -> %#",html);
//NSLog(#"Downloaded successfully");
}
else if([data length] == 0 && error == nil)
{
NSLog(#"Nothing downloaded");
}
else
{
NSLog(#"Error occured -> %#",error);
}
}];
}
It is called that quickly because you asked for kCLLocationAccuracyBest. Back off a bit. It's not based on time, it's based on delta distance. At that accuracy even a small change in distance will trigger an update in an area with good reception. Use a different value.
Again, these methods are not meant to be used based on time. They are meant to be used based on delta distance.
Reduce the accuraccy and the distance filter,this will reduce the frequency in which the method is called
If you want it to be called after 5 minutes then you can forcefully call the methods stopupdating and startupdating every five minutes
You could use startMonitoringSignificantLocationChanges instead of startUpdatingLocation. You would be updated only when user moves around 500 meters from last position
Instead of:
[self.myLocationManager startUpdatingLocation];
use:
[self.myLocationManager startMonitoringSignificantLocationChanges];
If you use: startUpdatingLocation it'll call the delegate method in each second. When you use startMonitoringSignificantLocationChanges it'll call the delegate method when a significant change in location occurs or after 5 minute intervals.
startMonitoringSignificantLocationChanges
Starts the generation of updates based on significant location
changes.
- (void)startMonitoringSignificantLocationChanges
Discussion
This method initiates the delivery of location events asynchronously,
returning shortly after you call it. Location events are delivered to
your delegate’s locationManager:didUpdateLocations: method. The first
event to be delivered is usually the most recently cached location
event (if any) but may be a newer event in some circumstances.
Obtaining a current location fix may take several additional seconds,
so be sure to check the timestamps on the location events in your
delegate method.
After returning a current location fix, the receiver generates update
events only when a significant change in the user’s location is
detected. For example, it might generate a new event when the device
becomes associated with a different cell tower. It does not rely on
the value in the distanceFilter property to generate events. Calling
this method several times in succession does not automatically result
in new events being generated. Calling
stopMonitoringSignificantLocationChanges in between, however, does
cause a new initial event to be sent the next time you call this
method.
If you start this service and your application is subsequently
terminated, the system automatically relaunches the application into
the background if a new event arrives. In such a case, the options
dictionary passed to the locationManager:didUpdateLocations: method of
your application delegate contains the key
UIApplicationLaunchOptionsLocationKey to indicate that your
application was launched because of a location event. Upon relaunch,
you must still configure a location manager object and call this
method to continue receiving location events. When you restart
location services, the current event is delivered to your delegate
immediately. In addition, the location property of your location
manager object is populated with the most recent location object even
before you start location services.
In addition to your delegate object implementing the
locationManager:didUpdateLocations: method, it should also implement
the locationManager:didFailWithError: method to respond to potential
errors.
Note: Apps can expect a notification as soon as the device moves 500
meters or more from its previous notification. It should not expect
notifications more frequently than once every five minutes. If the
device is able to retrieve data from the network, the location manager
is much more likely to deliver notifications in a timely manner.
Declared In CLLocationManager.h
Reference CLLocationManager

Resources