I'm testing with iBeacon for doing some task related Bluetooth in a iOS app after killed.
Actually It works out very well, but I'm still curious how it works.
Here is a code that I used.
private func startMonitoring() {
if CLLocationManager.isMonitoringAvailable(for: CLBeaconRegion.self) {
self.log("startMonitoring")
let region = CLBeaconRegion(...)
self.locationManager.startMonitoring(for: region)
}
}
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
if let region = region as? CLBeaconRegion {
if CLLocationManager.isRangingAvailable() {
self.log("didEnterRegion")
self.locationManager.startRangingBeacons(satisfying: region.beaconIdentityConstraint)
}
}
}
func locationManager(_ manager: CLLocationManager, didRange beacons: [CLBeacon], satisfying beaconConstraint: CLBeaconIdentityConstraint) {
if !beacons.isEmpty {
self.log("didRange")
self.doSomething()
}
}
I called startMonitoring() once when app is initially started and used two CLLocationManagerDelegate methods. And also I added log() for test.
I expected after I kill the app, didEnterRegion is called first and then didRange is called and finally do the task.
But it turns out, I just see 3 logs about "startMonitoring", which means (I guess) iBeacon called startMonitoring() somehow.
How is it possible? Why doesn't the app call delegate methods, and why does it even works out well?
Launching an app based on beacon detection works well on iOS because beacon monitoring is built on top of the same CoreLocation framework functionality as geofence region monitoring. It works like this:
When your app registers a Region for monitoring, the operating system remembers your app and the region, adding this pair to an OS-level tracking list.
Whenever iOS senses a location change (lat/lon for CLCircularRegion monitoring or BLE advert packets for CLBeaconRegion monitoring), it compares the change against this tracking list.
If a change in state is detected, iOS checks if the app is running. If so, it calls the didEnter or didExit delegate methods as appropriate.
If the app is not running, it first launches the app into the background, calling the app delegate’s onCreate method. After didFinishLaunching returns, iOS checks if the region state change that triggered the launch is registered with CoreLocation. If so, it calls didEnter or didExit.
The sequence in step 4 is critical for making this work — if you re-start monitoring before the end of didFinishLaunching in your app delegate, you get the didEnter callback.
And, yes, this all works even after killing an app from the task switcher because iOS does not remove an app’s monitored regions when the app is killed. It is one of the few ways you can relaunch and app after that action.
If you are not seeing log lines consistent with the above, there may be an issue with your logging. Try setting breakpoints and you will see the calls made in the sequence I describe above.
See this page for Apple's description of how didFinishLaunching is called when a CoreLocation change launches the app. That page is specifically for the significant location change service, but the same mechanism applies to beacon monitoring:
If you start this service and your app is subsequently terminated, the system automatically relaunches the app into the background if a new event arrives. In such a case, the options dictionary passed to the application:willFinishLaunchingWithOptions: and application:didFinishLaunchingWithOptions: methods of your app delegate contains the key UIApplicationLaunchOptionsLocationKey to indicate that your app 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.
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've created a simple app which tracks user location and creates local notification for every time location is updated.
I enabled the background modes below,
let locationManager = CLLocationManager()
open override func viewDidLoad() {
locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
locationManager.distanceFilter = 10
locationManager.allowsBackgroundLocationUpdates = true
locationManager.startUpdatingLocation()
}
open func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let notification = UILocalNotification()
notification.alertBody = "location updated"
notification.fireDate = Date()
UIApplication.shared.scheduleLocalNotification(notification)
}
I set string for NSLocationAlwaysUsageDescription and ask for permission. User grant permission for always usage when the app loaded first time.
It's working well when app is in the foreground, when it goes background still working at least in 5-40 minutes time range which is changeable
by battery or other opened apps.
The problem is why it stops working, Isn't it expected to be keep working?
I've never seen a time limit in Apple docs.
Switch to significant location updates when the app moves to background. iOS will unload the app if it keep alive in the background indefinitely.
locationManager.pausesLocationUpdatesAutomatically = false
Reduce Accuracy
Set the desiredAccuracy property of the location manager object
self.locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
You can use one of the CLLocationAccuracy constants
IMPORTANT
By default, standard location updates on iOS devices run with an accuracy level of best. Change these settings to match
your app’s requirements. Otherwise, your app will unnecessarily waste
energy.
Auto-Pause
Set the pausesLocationUpdatesAutomatically property of the location manager object to true
self.locationManager.pausesLocationUpdatesAutomatically = true
IMPORTANT
For apps that have in-use authorization, a pause to location updates
ends access to location changes until the app is launched again and
able to restart those updates. If you do not wish location updates to
stop entirely, consider disabling this property and changing location
accuracy to kCLLocationAccuracyThreeKilometers when your app moves
to the background. Doing so allows you to continue receiving location
updates in a power-friendly manner.
Allow background updates
Set the allowsBackgroundLocationUpdates property of the location manager object to true
self.locationManager.allowsBackgroundLocationUpdates = true
Apps that want to receive location updates when suspended must include
the UIBackgroundModes key (with the location value) in their app’s
Info.plist file and set the value of this property to true. The
presence of the UIBackgroundModes key with the location value is
required for background updates
Specify an Activity Type
Set the activityType property to let Core Location know what type of location activity your app is performing at a given time
self.locationManager.activityType = .automotiveNavigation
You can use one of the CLActivityType cases
Defer Location Update
On supported devices with GPS hardware, you can let the location
manager defer the delivery of location updates when your app is in the
background. For example, a fitness app that tracks the user’s location
on a hiking trail can defer updates until the user has moved a certain
distance or a certain period of time has elapsed.
Energy Efficiency Guide for iOS Apps - Location Best Practices
GettingLocationWhenSuspended
After searching for references (talking about any limitation), I assume that Apple Core Location Best Practices video session could be useful! at 06:53 talking about standard location in the background:
furthermore, Core Location won't take any action to ensure your app
continues to run, so if you have background run for some reason and
you decide to start a location session you might get some updates, but
you might also get suspended before you receive all information that
you hope to receive...
Actually, I faced this issue before, -as a workaround- the core location was used to keep tracking the location of the user to do unrelated functionality to its location -which is uploading files-, but this workaround didn't work since iOS 9 has been released; I even posted a question referring to this issue.
However, it seems your case is not identical to what I faced, if you are aiming to:
... creates local notification for every time location is updated.
then you might need to follow the approach of integrating with User Notification Framework - UNLocationNotificationTrigger:
The geographic location that the user must reach to enable the
delivery of a local notification.
It is also mentioned in the video session (08:59).
Probably, this is could be not what are you looking for, but since we have no guarantee that the background execution will continue running, you might -somehow- find a way to integrate it in your app to achieve the desired functionality.
Update for iOS 11:
You might need to check this answer for the proper way to request the location access.
By the sound of it the app is being killed due to memory constraints.
It should however be re-launched when a new location becomes available, as described here: https://developer.apple.com/documentation/uikit/uiapplicationlaunchoptionskey/1623101-location
You should see application(_:didFinishLaunchingWithOptions:) being called, and the 'location' key should be present in the launch options. You'll can then re-create whatever is consuming the locations and continue recording.
If it's not being re-launched it could be too memory hungry. Check the memory consumption of the app and see if applicationDidReceiveMemoryWarning(_:) is being called.
I assume you haven't implement background task. You can read here.
In the above link under section "Implementing Long-Running Tasks" point no. 3 is your situation, so it's valid you can use background location update in your project and for same you need to implement a background task too.
There are three way to track user location(as per above link under section "Tracking the User’s Location" ) :-
Foreground-only location services (which works in your case)
The significant-change location service (Recommended), but I think it is not usable in your case as you want to update user location per 10 meter and it works for ~500 meters, for more please see here
Background location services (I think you are trying for this) and solution is to add a background task.
Below is example of background task and you can modify as per your requirement, it works for me since last 2 hours and my app still update location in background.
.
In your AppDelegate class please update below function and then run your app in background.
func applicationDidEnterBackground(_ application: UIApplication) {
application.beginBackgroundTask(withName: "") {}
}
And below is my ViewController class
import UIKit
import CoreLocation
class ViewController: UIViewController, CLLocationManagerDelegate {
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self;
locationManager.requestAlwaysAuthorization()
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
locationManager.allowsBackgroundLocationUpdates = true
locationManager.distanceFilter = kCLDistanceFilterNone
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
self.locationManager.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("didUpdateLocations \(Date())")
self.locationManager.stopUpdatingLocation()
}
}
I ran into this where a QT app would stop getting location events in the background after an hour or two.
In the App Delegate when the app would go into the background I would stop the CLLocationManager, decrease the Accuracy from kCLLocationAccuracyBest to kCLLocationAccuracyNearestTenMeters AND increase the distance filter from None to 50 meters and it would then track for 24+ hours.
I try to use an iOS device as a beacon(let's say my iPhone5), which should be detected by another iOS devices, let's say (my iPad).
When both the devices are in foreground, my iPad is able to detect the beacon signal generated by my iPhone correctly. From my didRangeBeacons delegate method, I did
func locationManager(manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], inRegion region: CLBeaconRegion) {
if beacons.count > 0{
print(beacons)
}
}
I logged the beacon info
[CLBeacon (uuid:<__NSConcreteUUID 0x124e483b0> AD065817-291B-4422-BD59-CBBFBDB81F17, major:9, minor:6, proximity:2 +/- 0.81m, rssi:-56)]
When my iPhone(the beacon) in a background mode(the screen is not active), I am not able to detect any beacon signal from my iPad, are there any solution for this scenario? I have done a lot of research online, but I couldn't find something that is illuminating.
Monitoring for beacons works when an app is in the background because iOS is performing that service for the app. However ranging only works when the app is in the foreground. If short term background ranging is not enough then you need to enable the Background Modes option in the Capability tab of your project settings and use UIBackgroundModes set to location in your Info.plist.
Yes, you can range a beacon in the background. Normally iOS only allows you to do this for 10 seconds after you go to the background, however, you can use this code to extend your background awake time to up to 3 minutes:
func extendBackgroundRunningTime() {
if backgroundTask != UIBackgroundTaskInvalid {
// if we are in here, that means the background task is already running.
// don't restart it.
return
}
NSLog("Attempting to extend background running time")
var self_terminate: Boolean = true
self.backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithName("DummyTask", expirationHandler: {() -> Void in
NSLog("Background task expired by iOS")
if self_terminate != nil {
UIApplication.sharedApplication().endBackgroundTask(backgroundTask)
self.backgroundTask = UIBackgroundTaskInvalid
}
})
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {() -> Void in
NSLog("Background task started")
while true {
NSLog("background time remaining: %8.2f", UIApplication.sharedApplication().backgroundTimeRemaining)
NSThread.sleepForTimeInterval(1)
}
})
}
You actually don't need background modes for this. You need to set NSLocationAlwaysUsageDescription in info.plist. Your phone will range any of the CLBeaconRegions you set it to look for. I like to use this app to test with iBeacons because it provides simulation and detection, although there are many beacon simulator apps out there, or you can buy an actual ibeacon.
I would try to use ranging in the background as sparingly as possible, because this process can be battery intensive if done in excess. Try to use monitoring in situations where you don't need to know things like Proximity and RSSI that are provided through ranging in order to conserve battery life.
Monitoring Beacons works well in foreground as well as in background.
Monitoring
Monitoring a region enables your app to know when a device enters or exits the range of beacons defined by the region. Imagine a museum with an audio guide application and beacons installed by the two entrances. The app is monitoring for a region encompassing both beacons (think: ‘all entrance beacons’ region) and is notified whenever the user enters the museum. Then it launches a notification, reminding users about the audio guide feature.
Ranging
While Monitoring enables you to detect movement in-and-out of range of the beacons, Ranging is more granular. It returns a list of beacons in range, together with an estimated proximity to each of them.Coming back to our museum example: imagine an "all beacons near the exhibits" region. The audio guide app can scan for all beacons in this region and then check which beacon is the closest. Since each beacon is associated with a particular exhibit, the app will play a description of an artwork relevant to the user’s context.
Ranging works only when the app is running, which most of the time means that the user is actively using the app. This also means that it only needs permission to "Access Your Location While You Use the App".
If you're sure you need persistent background ranging for beacons, you'll need to activate the Background Modes capability for your application—specifically, the Location Updates mode.
Note that for startRangingBeaconsInRegion to work in the background you'll also need to start Standard Location Updates via CLLocationManager's startUpdatingLocation (meaning you need both a CLLocationManager and an ESTBeaconManager in your app).
Note: In iOS 9, you also need to set the allowsBackgroundLocationUpdates property of your CLLocationManager to true.
(from community.estimote.com )
https://community.estimote.com/hc/en-us/articles/203914068-Is-it-possible-to-use-beacon-ranging-in-the-background-
I need to check for device location every one hour or so and if the location is outside a particular area ( say the device has been taken out of the office premises ), do some action (like show a local notification saying "Hey! The device is outside the office").
To do this, I need to keep checking for location every one hour even though the app is killed. How to keep the app stay alive like forever though it has been terminated by the user.
Yes you can do it but if your system is deployment target is greater then 7.0.
In ios7.1 there is method called startMonitoringSignificantLocationChanges. This method updates location in background and even if application is terminated as per apple document:
Starts the generation of updates based on significant location changes.
If you start this service and your app is subsequently terminated, the
system automatically relaunches the app into the background if a new
event arrives. In such a case, the options dictionary passed to the
application:willFinishLaunchingWithOptions: and
application:didFinishLaunchingWithOptions: methods of your app
delegate contains the key UIApplicationLaunchOptionsLocationKey to
indicate that your app 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.
I found one demo for this may this help you. look this link http://mobileoop.com/getting-location-updates-for-ios-7-and-8-when-the-app-is-killedterminatedsuspended
Thanks for your answers. The best way do this is to use Geofencing.
It uses startMonitoringForRegion:(CLRegion *)region where we provide the latitude and longitude of the centre of the region and also the radius of the region.
When the device moves into this region, didEnterRegion gets called.
When the device moves out of this region, didExitRegion gets called.
And yes it works even when the app is terminated/backgrounded.
In addition, we have to set NSLocationAlwaysUsageDescription in the App's Info.plist file which you will find under the Supporting Files.
View this article for detailed information.
http://www.devfright.com/how-to-use-the-clregion-class-for-geofencing/
Inorder to test this in a simulator, I used local notifications when it enters and exits the region using UILocalNotification in the didEnterRegion and didExitRegion methods.
`UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.fireDate = [[NSDate date] dateByAddingTimeInterval:1];
notification.alertBody = #"You have entered the region";
[[UIApplication sharedApplication] scheduleLocalNotification:notification];`
Now change the custom location of the iOS simulator (Debug -> Location -> Custom Location), and provide latitude and longitude within the region and you get a notification saying "You have entered the region" and change the custom location to latitude and longitude outside the region, you will get a notification saying "You have exited the region".
I've implemented the use of GeoFences in my app. I've created a new CLLocationManager property and initialised it in my app's viewDidLoad method simply like so:
[[self.locationManager alloc] init];
I set the delegate to self, start monitoring for regions using startMonitoringForRegion:
Then, I emulate my location while running it using Xcode and the methods didEnterRegion and didExitRegion. The app works perfect while running, but I haven't got the chance to test it as I don't know how to emulate my location while the app is terminated, see my other question
Therefore, I was hoping to get some answers on these questions:
Can I manage background work just like normal with the didEnterRegion and didExitRegion methods? Like calculating, etc?
If my app is terminated, I enter a region, open my app - are my variables from didEnterRegion initialised and set up then?
Do I need to do anything else to set it up to work when my app is terminated, except for the normal CLLocationManager setup that I've done so far?
Thanks!