I have an app which monitors significant location changes.
Upon receiving a new calculation I want to calculate the duration from the current location to a specified location.
To calculate the duration I use calculateETAWithCompletionHandler: from the MKDirections class.
Everything works as expected as long as the app is in the foreground.
When I send the app to the background, it is correctly receives location updates in the background and everything works until I call calculateETAWithCompletionHandler:, which will never return results.
MKDirectionsHandler, the completion handler of calculateETAWithCompletionHandler:. is never called when being in the background.
As soon as the app is coming into the foreground again, all the waiting completion handlers are receiving results.
MKMapItem* origin = [MKMapItem mapItemForCurrentLocation];
MKMapItem* destination = [[MKMapItem alloc] initWithPlacemark:destinationPlacemark];
MKDirectionsRequest* request = [MKDirectionsRequest new];
[request setSource:origin];
[request setDestination:destination];
[request setTransportType:MKDirectionsTransportTypeAutomobile];
MKDirections* directions = [[MKDirections alloc] initWithRequest:request];
[directions calculateETAWithCompletionHandler:^(MKETAResponse *response, NSError *error) {
completion(response.expectedTravelTime, error);
}];
Is calling calculateETAWithCompletionHandler: in the background not allowed?
Is there any way to resolve this issue?
I believe the way you are making use of MKMapItem is the problem, you need to run this on the main thread. So I don't think it will work for what you need. When collecting the location in the background you should use CoreLocation instead.
The documentation around MKDirection is not very specific on this issue, the most relevant section I could find was:
An MKDirections object provides you with route-based directions data from Apple servers. You can use instances of this class to get travel-time information or driving or walking directions based on the data in an MKDirectionsRequest object that you provide. The directions object passes your request to the Apple servers and returns the requested information to a block that you provide.
Since you are trying to calculate travel-time, it would appear that calculateETAWithCompletionHandler: tries to perform a network request to the apple servers. With the application being in a background state, the request is put on hold until the application enters foreground again.
Unfortunately I don't think there is an easy way around this. You could try and use a "guesstimation" approach where, before the application enters a background state it calculates the ETA for a user, and then while it is in the background it increases or decreases the ETA proportionally to the direct distance between your current location and the destination. Depending on how precise you want your results to be this broad estimation could be enough to satisfy your requirements.
Related
I am a pilot and ios developer. I would like to know if it is possible to create two methods that can send notifications when the altitude increases and another when the altitude decreases (takeoff and landing). I have already created a code that can retrieve the altitude.
- (CMAltimeter *)altimeter
{
if (!_altimeter) {
_altimeter = [[CMAltimeter alloc] init];
}
return _altimeter;
}
if you want, I can share the project with Dropbox to show you my code.
Your code only creates a CMAltimeter instance.
To get altitude data, use startRelativeAltitudeUpdatesToQueue after checking if your device actually supports altimeter measurements, and send the notifications when you've detected a takeoff or landing in the callback:
if ([CMAltimeter isRelativeAltitudeAvailable]) {
CMAltimeter* altimeter = [[CMAltimeter alloc] init];
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[altimeter startRelativeAltitudeUpdatesToQueue:queue withHandler:^(CMAltitudeData* altitudeData, NSError* error) {
// your code here
}];
}
Few remarks:
You probably need to filter out altimeter signal noise using a low pass filter.
For sure you need to define a threshold value for altimeter changes, because you don't want to be triggered continuously at every 0.1m change.
The altimeter is a relative measurement. This means that you'd have to tell the app when you're on the ground; a kind of 0-level set.
Of course you can't use this in pressurised plane.
Plane speed probably influences the local pressure inside a non-pressurised plane.
Fuselage vibrations probably influences the local pressure.
#Geroen's answer shows how to get altimeter updates.
I think you should first make the app to just show the altimeter value on a large UILabel and see how that looks during a flight. This will give you an idea how messy the data is.
So I am using parse to get an update of the users location and data from that query like so:
-(void) getUserLocationAndData:(BOOL) showProgress
{
[PFGeoPoint geoPointForCurrentLocationInBackground:^(PFGeoPoint *geoPoint, NSError *error)
{
if (!error)
{
NSLog(#"Got current location - Zooming Map!");
CLLocationCoordinate2D zoomLocation;
zoomLocation.latitude = geoPoint.latitude;
zoomLocation.longitude = geoPoint.longitude;
[self getNearByData:geoPoint];
}
else
{
[[[UIAlertView alloc] initWithTitle:#"Unable to get Current Location" message:#"This app needs your current location in order to locate near by data. Please check your internet connection and make sure you enabled access to the location information." delegate:self cancelButtonTitle:#"Ok" otherButtonTitles:nil] show];
}
}];
}
When the app first launches the user location shows up, the data is pulled and everything works. If you wait about 15 seconds and don't move the user location icon turns gray (see image) and any updates result in a failure. If I reload the view it works again but the same thing happens.
I am not using a CLLocationManager because I do not need to constantly pull the data near the user. Just every so often or on demand by the user.
I have also noticed if I wait long enough the user location goes Blue again and all seems to work. What is causing this timeout? Can I set this timeout, or do I need to just use a CLLocationManager to have any control of this? Is Parse just timing out with an internal CLLocationManager or something?
Thanks for the help!
So evidently when you use the Parse function: geoPointForCurrentLocationInBackground Parse will take over with it's own internal CLLocationManager. After it does it's initial thing it stops updating the location (or possibly only updates the user location if the user moves enough.. again just a guess here.. and if that feature is supported on the device).
The solution is to create your own CLLocationManager in your view controller and instead of calling
geoPointForCurrentLocationInBackground
You simply store the updated user location and call:
PFGeoPoint *usersGeoPoint = [PFGeoPoint geoPointWithLocation:usersLastKnownLocation];
And now we can use that to query near by data. We are now responsible for starting and stopping the updating of the user location. I strongly recommend using this post to setup your own LocationManager and be sure to scroll down to the updated answers for more up to date info.
How can I get current location from user in iOS
After reading about the geoPointWithLocation in the Parse docs I made the conclusions above. Please read it for yourself (I know it's really hard to find and just one line...)
My Objective C app on iOS 7 gets location updates in the background from either the startUpdatingsignificantLocationChanges or startUpdatingLocation delegate (which one depends on the mode that the app is in, but I don't think it matters).
In the delegate, I gather the location info, write it to a dictionary, and then write the dictionary to a Firebase.
// this code is in the location update delegate routine
// the code that gathers the various elements that go into the dictionary
// are omitted for clarity, I don't think that they matter
// we may be running in the background on iOS 7 when we are called!
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:
[[NSNumber numberWithFloat:newLocation.coordinate.latitude] stringValue], #"Latitude",
[[NSNumber numberWithFloat:newLocation.coordinate.longitude] stringValue], #"Longitude",
[[NSNumber numberWithFloat:newLocation.horizontalAccuracy] stringValue], #"Accuracy",
formattedDateString, #"TimeNow",
[dateFormatter stringFromDate:newLocation.timestamp], #"TimeStamp",
[[NSNumber numberWithDouble:interval] stringValue], #"Date",
self.mode, #"Mode",
nil];
// Write it once to CurrentLocation
[ref setValue:dictionary];
// yes, I know this is clumsy
fbTmp = [NSMutableString stringWithString: fbRoot];
[fbTmp appendString : #"/locationHistory"];
ref = [[Firebase alloc] initWithUrl:fbTmp];
// Now write it again to the locationHistory list
ref = [ref childByAutoId];
[ref setValue:dictionary];
Sometimes it works, sometimes it doesn't (i.e. in the same run of the app, sometimes the location gets written to the Firebase successfully as expected, and sometimes it doesn't. There isn't any obvious rhyme or reason to when it seems to work and when it doesn't).
I suspect that the issue is that the Firebase write is not completing successfully in background mode, but I'm not sure. I am very new to iOS and Objective C and Firebase.
My app is marked in its Capabilities as requiring background services for Location updates and Background fetch (the latter my random attempt to fix this problem, the former I know that I need).
My suspicion is that I need to tell the OS that I need time to complete the write with a backkgroundTask, and then terminate the background task in the completion block of the firebase write - has anyone verified that that will work when running in background mode?
If so, do I just need to do that in the second write (assuming that they are completed in order), or in both (with a counter that I count down as each write completes)?
Any hints most appreciated.
Yes, you need to finish your task in background. It says so in the Apple Documentation:
If your app is in the middle of a task and needs a little extra time to complete that task, it can call the beginBackgroundTaskWithName:expirationHandler: or beginBackgroundTaskWithExpirationHandler: method of the UIApplication object to request some additional execution time. Calling either of these methods delays the suspension of your app temporarily, giving it a little extra time to finish its work. Upon completion of that work, your app must call the endBackgroundTask: method to let the system know that it is finished and can be suspended.
Just put your code in a background task, give it maximum time (3 minutes I think).
Read up on Apple app lifecycle and everything should clear up for you for future reference.
i have to execute a high number of reverseGeocodeLocation request,
i use this method for doing that:
for (Photo *photo in arrayWhitPicture) {
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
[queue addOperationWithBlock:^{
[geocoder reverseGeocodeLocation:[[CLLocation alloc] initWithLatitude:[photo.latitude doubleValue] longitude:[photo.longitude doubleValue]] completionHandler:^(NSArray *placemarks, NSError *error) {
if (error){
NSLog(#"Geocode failed with error: %#", error);
return;
}
CLPlacemark *myPlacemark = [placemarks objectAtIndex:0];
NSString *city = myPlacemark.locality;
NSLog(#"My country code: %#", city);
}];
}];
}
this code actual work, but the problem is that some of these request (like half) get this error:
Error Domain=kCLErrorDomain Code=2
after a few research i think this happen because i do a lot of request in short amount of time in fact the apple documentations say:
Send at most one geocoding request for any one user action.
When you want to update the user’s current location automatically
(such as when the user is moving), issue new geocoding requests only
when the user has moved a significant distance and after a reasonable
amount of time has passed. For example, in a typical situation, you
should not send more than one geocoding request per minute.
so my question is: my error is really caused by the fact that i do a lot of request, and in that case what can i do to resolve this problem, do you know other system apart to use the reverseGeocoding?
I have had exactly this problem, and my solution was to throttle down the number of reverse geocode queries that I performed (i.e. temporarily suspend the queue if already processed a whole bunch). This worked so well that I did not have to implement plan B, which was to switch to a different service.
See for example this question for discussion of alternate services. Google has a similar limit of 2500 requests per API key and 24 hour period. There is also Bing.
Apple most definitely limits how many geocoding requests you can issue at a time. Other users are reporting that the limit is around 50, though that could change at any time. The recommendation seems to be to do the geocoding in batches and to issue only one batch at a time, starting each batch only after the previous one completes.
Apple limits to perform number of Reverse GeoCode requests your application can make at a time. Sometimes I have seen this limited to ONE.
The solution is to implement your own Reverse Geocoder Queue (you can implement it as a separate class), in which you can add all of your requests. This queue need to execute one request at a time and after first is done, execute next. You can add callback blocks to notify you once the reverse geocoding is done for each request.
Example API in the Reverse Geocoder queue class can be like:
- (void) reverseGeocodeLocation: (CLLocation *) location completion: (CLGeocodeCompletionHandler) completionHandler
{
// Create some queue (NSMutableArray) in the class
// Create some ReverseGeoLocationObject with location and completionHandler as members
// Add ReverseGeoLocationObject to queue
// Check is queue is not already processing. If NO then process next request. You have have API named processNextRequest which you can call here (put code you posted in this API for single request)
}
Also call processNextRequest when CLGeocoder returns.
Im developing ios application which is getting data from the web server. I want everything else to wait, until one of the handlers of this class is called and completed. I know it is possible by using dispatch/threads, but i just can't figure out how.
-(void)callWebService:(NSString*)URL:(NSString*)SOAP{
NSURL *url = [NSURL URLWithString:URL];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
[req setHTTPMethod:#"POST"];
[req setHTTPBody:[SOAP dataUsingEncoding:NSUTF8StringEncoding]];
NSURLConnection *con = [[NSURLConnection alloc] initWithRequest:req delegate:self];
if(con){
[con start];
}
}
and at the end of this method continues code outside this class. but i want to wait until this handler is called (and completed):
-(void)connection:(NSURLConnection *)c didReceiveData:(NSData *)data{
NSString *res = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"%#",res);
Ukol_Parser *parser = [Ukol_Parser alloc];
[parser parseUkol:res];
}
because the parser here puts data into sqlite db and outside this class data are being read. But the "outside" code is being executed faster than i get response and handler is called....
If you want "everything else to wait", then it sounds like what you really want to do are synchronous requests.
Check out [NSURLConnection sendSynchronousRequest:returningResponse:error:]
However, make sure to do this thing on a background thread because if you do it on the main thread, your UI will block and your app will look unresponsive to user touches or anything else.
I'm nervous that you accepted the answer regarding sendSynchronousRequest from the background queue because, from a practical perspective, this is no different than your didReceiveData-based implementation. Specifically, if you do perform synchronous request from a background queue, it will not make "everything else wait".
But if you neglect to do this synchronous request from the background queue (i.e. if you do it from the main thread), you end up with a horrible UX (the app is frozen and the user is wondering whether the app has crashed), and worse, your app could be killed by the iOS "watchdog process" if it takes too long.
With all deference to the various answers, sending a synchronous request on a background queue is indistinguishable from the existing NSURLConnectionDataDelegate-based approach. What you really need to do is accept the fact that the rest of the app will not freeze, and therefore simply update the UI to let the user know what's happening, namely that (a) provide some visual cue that the app is not dead; and (b) prevent the user from interacting with your existing UI.
For example, before issuing your network request, add a view that will cover/dim the rest of your UI, prevent user interaction with the existing UI, and add a spinner to let the user know that the app is busy doing something. So, define a class property for a UIView that will dim the rest of the UI:
#property (nonatomic, strong) UIView *dimView;
And then, before the network request, update the UI:
// add a view that dims the rest of the UI, so the user has some visual cue that they can't use the UI yet
// by covering the whole UI, you're effectively locking the user out of using the app
// until the network request is done
self.dimView = [[UIView alloc] initWithFrame:self.view.bounds];
self.dimView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
self.dimView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.view addSubview:self.dimView];
// add a spinner that shows the user that something is really happening
UIActivityIndicatorView *indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
indicatorView.center = self.dimView.center;
indicatorView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
[indicatorView startAnimating];
[self.dimView addSubview:indicatorView];
[self callWebService:URL withSOAP:SOAP];
And then, in the connectionDidFinishLoading method (or if you used sendSynchronousRequest from a background queue, in the latter portion of code to dispatched to that background queue), after you finish parsing your data, you want to:
[self.dimView removeFromSuperview];
self.dimView = nil;
// now do what ever to need do to update your UI, e.g.:
//
// [self.tableView reloadData]
But the key is that you absolutely do not want to issue a synchronous network request from the main queue. You should (a) do your network request asynchronously (either synchronously on a background queue, or as you originally implemented) so that the iOS watchdog process doesn't kill your app; (b) let the user know the app is making some network request and hasn't frozen on them (e.g. a UIActivityIndicatorView; and (c) when the request is done, remove these "the app is doing network request" UI elements and refresh the rest of the UI now that your network request is done.
Finally, when testing your app, make sure you test it in real-world networking situations. I'd suggest you install the Hardware IO tools (available in Xcode menu, under "Open Developer Tools" - "More Developer Tools") and check out the Network Link Conditioner. This lets you simulate real-world network situations (e.g. a bad 3G or Edge network condition) on the iOS simulator. We get lulled into a false sense of performance when we test our apps in typical development environments with ideal network connectivity. Devices "in the wild" suffer a wide range of degraded network situations, and it's good to test your app in a similar, suboptimal network situation.
kind of a wild solution, but this actually worked https://gist.github.com/62940 :D
use synchronous calls . but It'd change the design of your class because a synchronous call will block and leave the app hanging
Post a notification from your didReceiveData: method, and have your other class observe that notification (or you could use a delegate setup, if it's easy to get a reference to this class from the other so you can set your other class as the delegate of this one). In the notification's selector method, start executing the rest of your code.