I am trying to fetch some arrival times for buses when a user approaches a stop, I have tested to ensure that the regions are correctly being trigged by sending a basic local notification and I have also tested my web service call to ensure it is working properly.
However I am having a hard time fetching the info then sending a notification.
Here is my code:
var bgTask: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier()
bgTask = UIApplication.shared().beginBackgroundTask {
self.webService?.getStopEstimates(routeStopIds: stopRouteIdSet, routeNameDict: routeNameDict, completion: { (result) in
if result == "error" {
return
}
let notification = UILocalNotification()
notification.fireDate = Date(timeIntervalSinceNow: 1)
notification.alertTitle = region.identifier + " Stop Details"
notification.alertBody = result
notification.soundName = UILocalNotificationDefaultSoundName
UIApplication.shared().scheduleLocalNotification(notification)
})
UIApplication.shared().endBackgroundTask(bgTask)
}
Any one see why it might not be sending? I have enabled background fetch and location services. This is inside didEnterRegion
I did the same thing (when a user approach a store that has a discount he/she will be notified).
First of all I would I would suggest you to download all the data and save them on Core Data or SQLite if that is an option.
Second take a look at Significant Location Change here. It will update your location in the background each 500 meters if you are moving and it will save a lot of battery instead of the didUpdateLocation.
Third after using SLC, on each call, fetch from your Core Data or SQLite the 20 nearest locations of your current location and add them for monitoring(Use Haversine to calculate all the points distances from your current location and then get the 20 nearest). Each time SCL is called update the regions that you monitor(if saving your data offline is not an option I would recommend to send the request at your webservice here. The ideal scenario is that you will send your location to your webservice and it will reply back with the nearest points. then add them for monitoring)
When the didEnterRegion called make sure you have your data downloaded and then create a local notification.
Let me know if you need more details
PS. Have in mind that your app might be terminated (in background) by the system if is consuming a lot of resources. In that case you have to reinitialise everything you need for pulling and showing the data (Networks requests, Location manager etc.) You can detect that the App relaunched because of a location event in AppDelegate's didFinisingLaunchingWithOptions:
if ([launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey]) {}
check this example on GitHub because I think your issue here is how you are handling the background task. If the web service call took too long, the background task will be invalidated. I don't know how well you know ObjC but in the example above, you will find a BackgroundTaskManager class handling that for you.
Things you should make sure of are:
Background Fetch in Target Settings (Background Modes section) are
ON.
You have asked and obtained "always" authorization (in plist)
Location Updates in Target Settings (Background Modes section) are ON.
Registered for User Notifications in appDidFinishLaunching in application delegate.
Renew expired background tasks if service took too long.
In the case of calling startUpdatingLocation while your app is in the background, CoreLocation still responds by starting the location updates for your app, but the system will no longer hold an assertion for you, and your app will be suspended once the allowed time in the background is spent. This time is (currently) is 10 seconds. But actually your app is now suspended and can no longer receive the updates. There is a way to extend this duration to (currently) up to 3 minutes by employing beginBackgroundTaskWithExpirationHandler: read more about it in Apple's CoreLocation Documentation
Whether this extended time is enough will depend on your app.
For your use case, Significant Location Change service is quite efficient.
You can start location updates when your app is in the foreground and defer location updates while your app is in the background. You can also read Apple's documentation about Significant Location Change
Related
I'm working on a navigation application, everything working in terminated, background and fore ground state.
But in one scenario of terminated state startMonitoringSignificantLocationChanges is not handling itself.
The issue is
when i start the startMonitoringSignificantLocationChanges and killed the app, then I'm getting location event like after 0.5-1km because of that it draws straight line from my initial position to the first location event I get.But when the location event starts coming then everything work smoothly
Same issue occur again when in the middle of travelling I open the application to check my route status and then kill the application, again location events start coming after 0.5-1km and a straight line was drawn.
The code is straight
significantLocationManager = CLLocationManager()
significantLocationManager?.allowsBackgroundLocationUpdates = true
significantLocationManager?.pausesLocationUpdatesAutomatically = false
significantLocationManager?.requestAlwaysAuthorization()
and call the tracking when user needs by
significantLocationManager?.startMonitoringSignificantLocationChanges()
Rest I have handled the incoming location event in the app delegate to save in db.
So question is how should I handle this scenario in which straight line is drawn ?
From Apple documentation:
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.
If you need to receive location updates as soon as possible I'd recommend to use startUpdatingLocation() with desired distanceFilter of CLLocationManager.
You can use Location update in background mode. From Apple documentation:
When you start the significant-change location service, a recently
cached value may be reported to your delegate immediately. As new
location data is obtained, the location manager calls your delegate's
locationManager(_:didUpdateLocations:) method with the updated values.
The locations parameter always contains at least one location and may
contain more than one. Locations are always reported in the order in
which they were determined, so the most recent location is always the
last item in the array, as shown in Listing 2.
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let lastLocation = locations.last!
// Do something with the location.
}
Here you will get the last cached location in your device, and it should be very precise if you have location service turned on in your device of course.
Another thing to know is this. Note form Apple:
The significant-change location service requires authorization. For
more information Requesting Authorization for Location Services.
I am looking for a way to get barometric pressure data (not altitude) every 10 or so minutes while the app is in the background or while another app is running in the foreground. Is this possible? If so, how?
If not, then how do apps like Pressur work?
Core Motion gives you CMAltimeter. CMAltimeter gives you CMAltitudeData. CMAltitudeData gives you pressure.
(Running in the background is another matter; you cannot run in the background merely in order to use Core Motion, so you'll have to find some other reason to run in the background.)
In order to receive background CoreMotion updates you need to do several things, but it boils down to running CoreLocation in the background. CoreMotion will not run in the background on its own.
Request authorization via Info.plist Privacy strings ("Location Always and When In Use" and "Location When In Use")
Add the Background Modes Capability to your project via the second tab on your app's Project pane. Enable Location Updates.
Create the managers and instances you require:
let locationManager = CLLocationManager()
let motionManager = CMMotionManager() // Not required in this case
let altimeter = CMAltimeter()
Set your location manager's allowsBackgroundLocationUpdates to true
Register the appropriate callbacks, in this case you want a CMAltimeter instance and something like:
if CMAltimeter.isRelativeAltitudeAvailable() {
altimeter.startRelativeAltitudeUpdates(to: OperationQueue.main) { (data, error) in
/* Handle Altitude changes here */
}
}
Unlike the other sensors (e.g. motionManager.gyroUpdateInterval) you don't appear to have control over how often this is called; it's up to you to average or discard values as appropriate.
I've got an (increasingly less and less) simple demo of background sensor data collection in a repo here.
I would like to clarify a few moments about traking user location while app is suspended. I have read a lot of articles about it but didn't find any clear answer.
Is it possible to create a local notification based on a user's location when application is suspended?
If it's possible, how my app's architecture has look like? Is my CLLocationManager subclass instance needs to be declared in AppDelegate file or it can be created as variable of some controller?
There are a couple of different ways to handle this.
You can set up a region-based local notification. That displays a message to the user if your app is not in the foreground. Your app only gets notified/launched if the user taps the action button on the local notification.
Another way to handle it:
You use the Core Location manager to create "geofence" regions that the system monitors on your app's behalf.
When your app is launched you should create an instance of the location manager and set up a delegate. You need to handle the process of asking the user for permission for location updates, and permission for always monitoring the user's location. That is a fussy, multi-step process, and if you miss a step it doesn't work. See the docs for more information. (I always have to back and re-read them when I'm setting up a new app with location services, and usually don't get it right the first time.)
When you've done that, the system will launch your app if it's not running when you receive a region enter/exit event. Once you create the location manager and set up a delegate, that delegate gets notified about the region enter/exit event.
In your handler for region enter/exit events you can post a local notification to yourself if you want to.
You can register the user for a local notifications using the region property.
//latitude & longitude come from your CLLocationManager delegates
let region = CLCircularRegion(center: CLLocationCoordinate2DMake(45.5017, 73.5673), radius: 1500, identifier: "identifier")
region.notifyOnExit = false
region.notifyOnEntry = true
let notification = UILocalNotification()
notification.region = region
notification.regionTriggersOnce = true //only show this notification once
notification.alertTitle = "Foo"
notification.alertBody = "Hello World"
UIApplication.shared.scheduleLocalNotification(notification)
Note that you can have at most 64 local notifications:
https://developer.apple.com/library/ios/documentation/iPhone/Reference/UILocalNotification_Class/
UPDATE: "You can only monitor a maximum of 20 location regions at one time. (and that's a combined total of 20 geofence regions and beacon regions.)" - DuncanC
Is it possible to create a local notification based on a user's location when application is suspended?
Yes. When you use background location monitoring, if your app is not active, it is woken in the background long enough to receive an event from the runtime. Thus, your app is now temporarily running. At that moment, creating a local notification is legal.
If it's possible, how my app's architecture has look like? Is my CLLocationManager subclass instance needs to be declared in AppDelegate file or it can be created as variable of some controller?
The event from the runtime is going to be sent to your location manager's delegate. Therefore your location manager needs to exist and it needs to have a delegate. It doesn't have to be a property of the app delegate, but it certainly needs to be a property of some instance that actually exists, so it if is a view controller, it had better not be a view controller that is not always present.
Note that if your app has been terminated while suspended (which is always a possibility), it will be launched from scratch (in the background) in order to receive this event. In that case, you can learn from the options: dictionary in didFinishLaunchingWithOptions: that this is because of an incoming location event, and thus you can respond by doing whatever is necessary in order to get yourself a location manager and a delegate.
I've been working on a new app that leverages the use of location based reminders via geofencing.
As far as I understand, in order to do that, the app must request authorisation via:
CLLocationManager.requestAlwaysAuthorization()
So far, it works as expected, but I'm curious how for example the "Reminders" app, and others such as Omnifocus only request for "When in use" or:
locationManager.requestWhenInUseAuthorization()
Those apps are still able to provide location based reminders without requesting always authorisation. I also noticed that these apps won't trigger the "location" icon on the status bar while in background, which my app does. How do they get these apps to deliver those notifications then?
Thanks!
D
Found the answer while watching the WWDC CLLocation updates to iOS 8 available here:
https://developer.apple.com/videos/wwdc/2014/?id=706
Reminders and similar apps work without requiring for AlwaysAuthorization since they take advantage of the UILocalnotification framework changes. Since iOS 8, it supports Region Based Triggering.
So now if you do not need to actually "Launch" your app in the background and do something when location changes, and just present a notification, you can just use UILocalNotification and RegionBased Triggering.
#NSCopying var region: CLRegion!
in iOS8, UILocalnotification has the 'region' property, which correspondes to a CLRegion object.
From Apple docs:
Assigning a value to this (region) property causes the local notification to be delivered when the user crosses the region’s boundary. The region object itself defines whether the notification is triggered when the user enters or exits the region. The default value of this property is nil.
You may specify a value for this property or the fireDate property but not both. Attempting to schedule a local notification that contains both a region and fire date raises an exception.
Overview
My compagny is asking me to release an application that can check location of the device every two hours. The app would send these location data through a TCP/IP socket to my server and then receive information accordingly to these very data (straight away, and through the same TCP/IP socket). So I'm not trying to make my application running in background mode continuously (which, actually, seems to be a hot topic in iOS, and it's also not suitable for my project).
Question
In order to be reliable, what is the best practice to achieve this? So, I would like to know:
Since my app is suspended (= inactive), does Apple allow to open a socket to send location when it's woken up by didUpdateToLocation?
How long do I have to perform my send/receive task via my socket?
Should I create a real background task with beginBackgroundTaskWithExpirationHandler and use the 10 minutes allowed by Cocoa to perform my send/receive task?
It is possible (and allowed by Apple) to ask for a 10 mins background task every 2 hours, without human interaction (ie. the user should re-open the app, etc)?
What I achieved/found so far
I added the location key in my Info.plist to be able to run the didUpdateToLocation handler when my app is inactive.
I am able to send and received data through a socket I have opened when my application was in foreground (= active).
I tried to check the backgroundTimeRemaining when didUpdateToLocation is called. I got a very large result number, which seems to be normal because, at this point, the applicationState is not in UIApplicationStateBackground but in UIApplicationStateActive.
These points are not very clear in the official documentation, and I did not found topics related to my specific case.
Thanks for your help.
According to Apple's documentation, you can achieve these by using a very similar approach to the one you described. What I would do is something similar to what is explained in this post by mindsizzlers:
As a recommendation, turn on significant location updates when the app enters in background, so you save battery. You can do this when the app goes to background:
- (void) applicationDidEnterBackground:(UIApplication *) application
{
// Create the location manager if you do not already have one.
if (nil == locationManager)
locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
[locationManager startMonitoringSignificantLocationChanges];
}
Then, the system will wake your app when location changes, as explained in the documentation.
If you leave this service running and your app is subsequently suspended or terminated, the service automatically wakes up your app when new location data arrives. At wake-up time, your app is put into the background and given a small amount of time to process the location data. Because your app is in the background, it should do minimal work and avoid any tasks (such as querying the network) that might prevent it from returning before the allocated time expires. If it does not, your app may be terminated.
In order to avoid the action of sending the new location to the server from being highly unreliable (it may work sometimes) you should tell iOS in advance that you are doing a background task that should be allowed to run to completion.
Change your location manager delegate (didUpdateToLocation) to handle background location updates.
- (void) locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation
fromLocation:(CLLocation *)oldLocation
{
if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
// Send the new location to your server in a background task
// bgTask is defined as an instance variable of type UIBackgroundTaskIdentifier
bgTask = [[UIApplication sharedApplication]
beginBackgroundTaskWithExpirationHandler:
^{
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
}];
// Make a SYNCHRONOUS call to send the new location to our server
// Close the task
if (bgTask != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}
} else {
// Handle location updates in the normal way
}
}
This way, you will not need to have the timer running. Since you will be waken up automatically and sending the updated location to your server every time it changes significantly.
Of course, if you want to make sure this happens in a specific interval, you can still go with the approach of setting a timer to start receiving location updates and as soon as you get it, you send it to the server. Take a look to this post talking about Background Modes in iOS (section: Receiving Location Updates) and this other questions to see how to do this in detail.