I'm currently trying to find a way to range beacons in the background in iOS by using location monitoring and then triggering the ranging like so:
func locationManager(manager: CLLocationManager, didDetermineState state: CLRegionState, forRegion region: CLRegion) {
if (state == .Inside) {
locationManager.startRangingBeaconsInRegion((region as? CLBeaconRegion)!)
}
}
I'm then trying to get an API call to be made in the beacon ranging
func locationManager(manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], inRegion region: CLBeaconRegion) {
for beacon in beacons {
let minor = beacon.minor as Int
let major = beacon.major as Int
do {
try APICall.canSeeBeacons(major, minor: minor)
} catch {
print("Error making API call")
}
}
}
However this only works for about ten minutes while the phone is in the background, after ten minutes it no longer works but I'm hoping to make it continuous so that API calls can always be made when a beacon is found. I do also have the correct keys set in the permissions and I'm using requestAlwaysAuthorization() on my locationManager
Take a look on the Apple's iOS Dev Library regarding Background Executions:
https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html
If the type of your app is one of the accepted types for longer background executions, add the key Required background modes to your Info.plist with an array, containing one or more of the enabled types: audio, location, voip, newsstand-content, external-accessory and/or bluetooth-central.
Just note that this will be reviewed by Apple before publishing your app on the App Store.
EDITED:
I didn't use this for a while, but tested now and it seems it's even easier since Xcode 6.
Follow the steps:
1. Add the key Required background modes to your Info.plist
2. Go to Capabilities
3. Select the background mode(s) that fit.
I had the same problem. As said a commentator I've moved location manager delegate methods into the App Delegate. And also I added this:
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false
And it works.
Related
I make an application with iBeacon.
And I decide to use CoreLocation and Ranging.
But, I think Ranging use too much energy.
As an alternative to Ranging, I tried to use Monitoring.
Then, I profile Ranging's usage energy level and Monitoring's usage energy level.
Ranging use about 2 energy usage levels more than Monitoring
Result: (Ranging's level: 10/20, Monitoring's level: 8/20)
But, Monitoring doesn't called didExit or didDetermineState immediately.
I want My app has a Real-Time as Ranging.
My Solution:
Monitoring Start
If I enter Monitoring region, I start Ranging
If I exit region, ranging result is nil, and stop Ranging
Monitoring Stop and Start.
Monitoring doesn't called exit or determineState method immediately.
But, I discovered stop and rerun monitoring make my application has Real-Time.
I think that solution reduce energy usage when standby.
And It Actually Working!
▼ Here is my Code.
class Service: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, CLLocationManagerDelegate{
private let constraint = CLBeaconIdentityConstraint(uuid: Constants.beaconUUID!,
major: Constants.beaconMajor,
minor: Constants.beaconMinor)
private let region = CLBeaconRegion(beaconIdentityConstraint:
CLBeaconIdentityConstraint(uuid: Constants.beaconUUID!,
major: Constants.beaconMajor,
minor: Constants.beaconMinor),
identifier: Constants.beaconIdentifier)
var locationManager: CLLocationManager!
override init() {
super.init()
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.requestAlwaysAuthorization()
}
}
extension Service {
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
if manager.authorizationStatus == .authorizedAlways {
region.notifyOnExit = true
region.notifyOnEntry = true
region.notifyEntryStateOnDisplay = true
manager.startMonitoring(for: region)
}
}
func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) {
if state == .inside {
didEnterEvents(manager)
}
}
func locationManager(_ manager: CLLocationManager, didRange beacons: [CLBeacon], satisfying beaconConstraint: CLBeaconIdentityConstraint) {
if beacons.first == nil {
didExitEvents(manager)
stopAndRerunMonitoring(manager, for: region)
}
}
}
extension Service {
private func stopAndRerunMonitoring(_ manager: CLLocationManager, for region: CLRegion) {
print("reboot!")
manager.stopMonitoring(for: region)
manager.startMonitoring(for: region)
}
private func didEnterEvents(_ manager: CLLocationManager) {
print("inside")
manager.startRangingBeacons(satisfying: constraint)
}
private func didExitEvents(_ manager: CLLocationManager) {
print("outside")
manager.stopRangingBeacons(satisfying: constraint)
}
}
I know my solution is terrible.
But I can't find any other solution.
Plz, Can you find any other better solution?
etc:
didEnter and didExit called determineState method.
I need to call requestState() method in other code. so I write a Enter event logic in determineState method.
You need Real-Time?
Yes, because i will make safety system with beacon. So My application's requirements has Real-Time.
I need Real-Time regioning and less energy usage.
The approach you describe of starting ranging on region entry and stopping it on region exit is not "terrible". It is in fact quite common. It is often used to determine the exact identifiers of the beacons detected, which would otherwise not be possible by monitoring alone.
For your use case, it is also true that ranging while inside a region will expedite the region exit event, because it forces a constant Bluetooth scan rather than relying on the slower hardware filters used for monitoring. While ranging is active, region exits come 30 seconds after the beacon was last detected.
For this to be a practical solution, you must take into account background behavior:
Unless you do special tricks, iOS won't let you range for more than a few seconds when the screen is off. As soon as ranging stops, you lose the expedited region exits.
While the screen is on, there is no point in saving battery by not ranging all the time, because the illuminated screen uses over 100x as much battery as the constant Bluetooth scan of ranging.
To get any benefit from the technique you describe, you must extend the ranging time in the background as described in my blog post here. Be aware that since I wrote that article, Apple has reduced the time you can extend ranging in the background from 180 seconds to 30 seconds, which may not be enough for your needs. You can get unlimited background ranging by:
Add the location background mode to Info.plist
Obtain "always" location permission from the user. It is not sufficient to get "when in use" permission.
Start a background task as described here: http://www.davidgyoungtech.com/2014/11/13/extending-background-ranging-on-ios
Request 3km location updates from CoreLocation. This will keep the app running in the background without extra battery drain from GPS, as 3km accuracy only uses the cell radio You don't need to do anything with these results. You just need to request them to keep the app alive.
(iOS 11, Swift 4.1)
I wrote a cordova plugin in swift to handle region monitoring, but it isn't working properly when my app is suspended or killed. Here is my class with the relevant functions:
class GeofenceManager : NSObject, CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) {
switch state {
case .inside:
log ("Did Enter Region: " + region.identifier)
self.postGeofenceTransition(region: region, transitionType: 1)
break
case .unknown:
log ("Unknown Transition for region: " + region.identifier)
// self.postGeofenceTransition(region: region, transitionType: 1)
break
default:
break
}
}
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
log ("Did Exit Region: " + region.identifier)
self.postGeofenceTransition(region: region, transitionType: 2)
}
func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) {
log("Did start monitoring region: " + region.identifier)
self.locationManager.requestState(for: region)
}
}
(I do it this way to handle someone that's already in the region when we start monitoring)
I've also got "didChangeAuthorizationStatus" that fetches and sets all the geofences, that is working fine.
In my AppDelegate (objc) I'm attempting to start Location Services if the app is opened from a region event, with
if([launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey]) {
_locationManager = [[CLLocationManager alloc] init];
[_locationManager setDelegate:self];
[_locationManager setDistanceFilter:kCLHeadingFilterNone];
//change the desired accuracy to kCLLocationAccuracyBest
[_locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
//SOLUTION: set setPausesLocationUpdatesAutomatically to NO
[_locationManager setPausesLocationUpdatesAutomatically:NO];
[_locationManager startUpdatingLocation];
}
My question is, if my app is opened from an event, i.e. "didEnterRegion", I immediately start location services, but will I hit my "didEnterRegion" delegate again? And am I doing something wildly wrong here?
I think I'm missing some understanding of iOS application states, have dug into the docs but it hasn't clicked yet. Any insight is very appreciated.
My question is, if my app is opened from an event, i.e. "didEnterRegion", I immediately start location services, but will I hit my "didEnterRegion" delegate again?
If your app was suspended or terminated, and if it is awakened or launched just to receive didEnterRegion, it stays in the background. It is given time just to handle this one event and then it is suspended.
You cannot start location updates with startUpdatingLocation at that point. You are in the background! Even if you are authorized for background updates, you cannot start getting updates while you are in the background. And even if you could, your code would fail, because you never set allowsBackgroundLocationUpdates. And even if you could do that, you wouldn't get any events, because you have no implementation of didUpdateLocation.
The correct procedure is: do nothing. Just handle didEnterRegion and get out. If the user enters the region again, your didEnterRegion will be called again. Region monitoring just goes on forever until you stop it (and make sure you do stop it, or the user may be be forced to delete your app).
Addendum: How to respond to being launched from scratch in the background like this? You can detect the key in didFinishLaunchWithOptions and handle it there and return false, or you can ignore it and return true and receive didEnterRegion. But either way you must immediately create a location manager and appoint its delegate or you will get nothing, obviously. This is why you should always create the location manager and set its delegate in your app delegate or root view controller, something that always exists, and exists as early as possible in the life of the app, as soon as it launches.
Still very new to Swift. I have come from an Android background where there is BroadcastReceiver that can deliver location info to a service even though the app isn't running.
So I was looking for something similar in iOS/Swift and it appears that before this wasn't possible but it may be now. I am developing for iOS 10 but would be great if it was backwards compatible.
I found
startMonitoringSignificantLocationChanges
which I can execute to start delivering location updates, although this raises a few questions. Once I call this and my app is NOT running, are the updates still being sent ? And how would the app wake up to respond ?
Also restarting the phone and when it return, does this mean I still need call startMonitoringSignificantLocationChanges again meaning that I would have to wait for the user to execute my app. Or does it remember the setting after reboot ?
Still a little confused how to get around this, here's a brief explanation of what I am trying to do.
I would like to update the location of the phone even though the app is not running, this would be sent to a rest service every so often.
This way on the backend services I could determine if somebody is within X meters of somebody also and send them a push notification.
It may or may not be a good solution but if I were you I would have used both startMonitoringSignificantLocationChanges and regionMonitoring.
Here is the sample I made which worked well with iOS 13.
Lets take regionMonitoring first. We have certainly no problems when the app is in foreground state and we can use the CLLocationManager's didUpdate delegate to get the location and send it to the server.
Keep latest current location in AppDelegate's property, lets say:
var lastLocation:CLLocation?
//And a location manager
var locationManager = CLLocationManager()
We have two UIApplicationDelegates
func applicationDidEnterBackground(_ application: UIApplication) {
//Create a region
}
func applicationWillTerminate(_ application: UIApplication) {
//Create a region
}
So whenever the user kills the app or makes the app go to background, we can certainly create a region around the latest current location fetched. Here is an example to create a region.
func createRegion(location:CLLocation?) {
if CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) {
let coordinate = CLLocationCoordinate2DMake((location?.coordinate.latitude)!, (location?.coordinate.longitude)!)
let regionRadius = 50.0
let region = CLCircularRegion(center: CLLocationCoordinate2D(
latitude: coordinate.latitude,
longitude: coordinate.longitude),
radius: regionRadius,
identifier: "aabb")
region.notifyOnExit = true
region.notifyOnEntry = true
//Send your fetched location to server
//Stop your location manager for updating location and start regionMonitoring
self.locationManager?.stopUpdatingLocation()
self.locationManager?.startMonitoring(for: region)
}
else {
print("System can't track regions")
}
}
Make use of RegionDelegates
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
print("Entered Region")
}
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
print("Exited Region")
locationManager?.stopMonitoring(for: region)
//Start location manager and fetch current location
locationManager?.startUpdatingLocation()
}
Grab the location from didUpdate method
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if UIApplication.shared.applicationState == .active {
} else {
//App is in BG/ Killed or suspended state
//send location to server
// create a New Region with current fetched location
let location = locations.last
lastLocation = location
//Make region and again the same cycle continues.
self.createRegion(location: lastLocation)
}
}
Here I have made a 50m region radius circle. I have tested this and it is called generally after crossing 100m from your center point.
Now the second approach can me using significantLocationChanges
On making the app go background or terminated, we can just stop location manager for further updating locations and can call the startMonitoringSignificantLocationChanges
self.locationManager?.stopUpdatingLocation()
self.locationManager?.startMonitoringSignificantLocationChanges()
When the app is killed, the location is grabbed from didFinishLaunching method's launchOptions?[UIApplicationLaunchOptionsKey.location]
if launchOptions?[UIApplicationLaunchOptionsKey.location] != nil {
//You have a location when app is in killed/ not running state
}
Make sure to keep BackgroundModes On for Location Updates
Also make sure to ask for locationManager?.requestAlwaysAuthorization() by using the key
<key>NSLocationAlwaysUsageDescription</key>
<string>Allow location</string>
in your Info.plist
There can be a third solution by taking 2 LocationManagers simultaneously.
For region
Significant Location Changes
As using significantLocationChanges
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.
as per the give Apple Doc
So it totally depends on your requirements as the location fetching depends on many factors like the number of apps opened, battery power, signal strength etc when the app is not running.
Also keep in mind to always setup a region with good accuracy.
I know that this will not solve your problem completely but you will get an idea to move forward as per your requirements.
im trying to build a demo app using Estimote Beacons. i want the app to open e specific viewcontroller when the user is near a beacon. im using performseguewithidentifier but when the app starts is opens only the first viewcontroller representing the first beacon which is in the range and it doesnt open the other ones when i go near the other beacons. it somehow just stops ranging for other beacons.
below is the code im using to range for beacons:
func beaconManager(manager: AnyObject, didRangeBeacons beacons: [CLBeacon],
inRegion region: CLBeaconRegion) {
let knownBeacons = beacons.filter{ $0.proximity != CLProximity.Unknown}
if (knownBeacons.count > 0) {
let closestBeacon = knownBeacons [0] as CLBeacon
if(closestBeacon.minor.integerValue==50557){
performSegueWithIdentifier("VC1", sender: nil)
}
else if(closestBeacon.minor.integerValue==37890){
performSegueWithIdentifier("VC2", sender: nil)
}
else if(closestBeacon.minor.integerValue==18976){
performSegueWithIdentifier("VC3", sender: nil)
}
else {
self.view.backgroundColor = UIColor.brownColor()
}
I haven't used Estimote's custom library, but I assume it's similar to the location manager's.
In the Core Location Manager, if your app is in the background you get an entered region notification when you first enter a new beacon region, and then you only get ranging information for a few seconds.
If you've set up your region with a unique UUID and Major ID but no minor ID, then all beacons with that UUID and Major ID are considered part of the same region and you won't reliably get ranging notifications as the beacons with different minor IDs become the closest beacon.
If you want to handle multiple beacons in range at the same time and distinguish between them you'll need to create separate beacon regions for each beacon's UUID, Major ID and minor ID.
I don't know if that's the issue you're facing, but it could be.
We have developed a construction project application in iOS, where a project manager can invite users to work in that construction project. Whenever the linked users/project manager of a particular project reaches the project site location we send them notifications to tell them to watch site safety videos before entering the site. If the users have seen those videos then we don’t send these notifications again to those users on next time entering the project location. This is all working good.
Now we have added a new functionality in this app with which attendance of these users can be marked as they enter in the project site location. For this we have used Geofencing feature, we create geofences for nearby projects of user location, as soon as the user enters a particular project geofence we mark him as checked in (with entry timing) and on exiting from the geofence, the user gets marked as checked out(with exit timings). This works 100% if we keep the iOS app in background (if app is kept open in recent apps), but it does NOT ALWAYS work when the app is closed (if the app is removed from recent apps). We have tried Significant Region monitoring as well, but not getting any consistency in the results for all the users.
So our question here is that does iOS support this GPS call when app is closed, means can we execute any code in the app when app is closed (removed from recent apps)? If yes then to what accuracy because our GPS code call does not work 100% when the app is closed?
My code with Geofencs is here :-
func locationManager(manager: CLLocationManager, monitoringDidFailForRegion region: CLRegion?, withError error: NSError) {
NSLog("Monitoring failed for region with identifier: \(region!.identifier) Error: \(error.description)")
if let region = region as? CLCircularRegion
{
self.locationMngr.stopMonitoringForRegion(region)
self.locationMngr.startMonitoringForRegion(region)
}
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
NSLog("Location Manager failed with the following error: \(error)")
}
func locationManager(manager: CLLocationManager!, didEnterRegion region: CLRegion!) {
println("Entered Region \(region.identifier)")
if let region = region as? CLCircularRegion
{
// Send region.identifier(Project ID) to server with User ID for CHECK-IN
}
}
func locationManager(manager: CLLocationManager!, didExitRegion region: CLRegion!) {
println("Exited Region \(region.identifier)")
if let region = region as? CLCircularRegion
{
// Send region.identifier(Project ID) to server with User ID for CHECK-OUT }
}
func locationManager(manager: CLLocationManager!, didStartMonitoringForRegion region: CLRegion!) {
NSLog("***Starting monitoring****** \(region.identifier)")
}
func locationManager(manager: CLLocationManager!, didDetermineState state: CLRegionState, forRegion region: CLRegion!) {
if state == CLRegionState.Inside {
//// Send region.identifier(Project ID) to server with User ID for CHECK-IN
}else{
NSLog("STATE is OUT of region");
}
}