I am building a feature related to region monitoring while starting region monitoring I am requesting the state as shown below in code. On some of the devices, I am getting region state Unknown all the time. If I switch Wifi On or Off or plug the charger into it. It starts working fine.
How can I make it more reliable on a cellular network?
Please, note I took all location permissions from the user before making any region monitoring or state request calls.
private func initiateLocationManager() {
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.distanceFilter = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
}
func startMonitoring(alarm: StationAlarm) {
if LocationManager.sharedInstance.isRegionMonitoringAvailable() {
let coordinate = CLLocationCoordinate2D(latitude: stationLatitude, longitude: stationLongitude)
// 1
let region = CLCircularRegion(center: coordinate, radius: CLLocationDistance(radius * 1000), identifier: alarm.alarmId)
// 2
region.notifyOnEntry = true
region.notifyOnExit = false
// 4
locationManager.startMonitoring(for: region)
Utility.delay(0.1) { [weak self] in
self?.locationManager.requestState(for: region)
}
}
}
func locationManager(_: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) {
Log.event("Region State is \(state.rawValue)")
}
The issue is, you are calling the requestState using a hard-coded delay - (0.1). How do you make sure the Location Manager started monitoring your region within 0.1 seconds? You will get the exact region state of a region, only if started monitoring it.
The better method for overcoming this problem is, implement the didStartMonitoringForRegion delegate and call requestStateForRegion
locationManager.startMonitoring(for: region)
func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) {
manager.requestState(for: region)
}
func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) {
if (region is CLBeaconRegion) && state == .inside {
locationManager(manager, didEnterRegion: region)
}
}
From the CLLocationManager requestState(for:) docs:
region: The region whose state you want to know. This object must be an instance of one of the standard region subclasses provided by Map Kit. You cannot use this method to determine the state of custom regions you define yourself.
You defined the region yourself so you can't use requestState(for:) to get its state. You use that function with regions that you get back from Core Location (via the delegate methods).
If you want to know whether the device is currently inside a region, start a standard location update request (startUpdatingLocation() etc) and when you get back a recent and accurate coordinate, use the CLCircularRegion contains() function to check the coordinate.
// In the locationManager(_:didUpdateLocations:) delegate method
if myCircularRegion.contains(myCoordinate) {
// ...
}
Related
I am new to iOS development and struggling with many of the interactions between my program and the device hardware so please excuse my very minimal knowledge.
I am trying to build into my app the ability to run code when a beacon is detected. Ultimately I need this to happen in the background as well but for now I am just working on getting it to work in the foreground.
With the code that I currently have, the print message located within the callback function is never called even with the beacon about two feet from the phone. I have double checked with an android device that the UUID broadcasted by the beacon and the UUID that iOS is searching for is one and the same.
This is the class that I created to manage the location aspects.
import CoreLocation
class LocationManager: NSObject, CLLocationManagerDelegate {
var locationManager: CLLocationManager!
override init() {
super.init()
locationManager = CLLocationManager()
locationManager.delegate = self
}
func locationPermission() {
locationManager.requestAlwaysAuthorization()
}
func startScanning() {
let beaconRegion = CLBeaconRegion(uuid: Beacon.beaconUUID!, identifier: "CarBeacon")
print(Beacon.beaconUUID!)
locationManager.startMonitoring(for: beaconRegion)
locationManager.startRangingBeacons(in: beaconRegion)
}
func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
if let beacon = beacons.first {
print(beacon.uuid)
}
}
}
This is called from a SwiftUI View with me creating the object in the beginning:
let locationManager = LocationManager()
with this in the view:
.onAppear() {
locationManager.locationPermission()
locationManager.startScanning()
}
Additionally, the UUID is from a separate file
enum Beacon {
static let beaconUUID = UUID(uuidString: "1810C112-B26E-49EB-8EB0-B8DB2DDF2DFB")
}
I also tried replacing the startScanning() function with startMonitoring():
func startMonitoring() {
let beaconRegion = CLBeaconRegion(uuid: Beacon.beaconUUID!, identifier: "CarBeacon")
locationManager.startMonitoring(for: beaconRegion)
}
and adding a new callback function:
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
if let beaconRegion = region as? CLBeaconRegion, beaconRegion.uuid == Beacon.beaconUUID {
print("Hello There")
}
}
Which led to the same results of having no console output.
Many of the tutorials that I found seemed to be outdated or not written in swift and I know that the startRangingBeacons() function is already depreciated so I struggled to put random bits of information together. I would appreciate any help that you could give me.
A few things to check:
Use an off the shelf beacon scanner like Locate Beacon to confirm it can detect your transmitter. Be sure to configure your UUID with the iOS scanner app.
Go to settings -> apps -> your app -> permissions and confirm location permission is granted
Add debug lines or print statements when you start ranging and make sure you see them.
I'm trying to monitoring region and detecting beacons when the app is killed (in foreground everything works fine). I've read that it should be sufficient to set allowsBackgroundLocationUpdates=true and pausesLocationUpdatesAutomatically=false to wake up the app, but only if an enter/exit event is detected.
Now, my problem is that when I turn off the beacon, didDetermineState and didExitRegion are never called. And if I request the state explicitly, it return that it is still inside the region. What am I missing?
This is my code, entirely in the AppDelegate.
func requestLocationPermissions() {
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.requestAlwaysAuthorization()
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false
startMonitoring()
}
func startMonitoring() {
let constraint = CLBeaconIdentityConstraint(uuid: Config.Beacons.uuid, major: Config.Beacons.major, minor: Config.Beacons.minor)
let beaconRegion = CLBeaconRegion(beaconIdentityConstraint: constraint, identifier: Config.Beacons.beaconID)
beaconRegion.notifyEntryStateOnDisplay = true
locationManager.startMonitoring(for: beaconRegion)
locationManager.startRangingBeacons(satisfying: constraint)
}
func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) {
if state == .inside {
print("AppDelegate: inside beacon region")
} else {
print("AppDelegate: outside beacon region")
}
}
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
print("AppDelegate: entered region")
}
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
print("AppDelegate: exited region")
}
func locationManager(_ manager: CLLocationManager, didRange beacons: [CLBeacon], satisfying beaconConstraint: CLBeaconIdentityConstraint) {
guard beacons.count > 0 else {
let constraint = CLBeaconIdentityConstraint(uuid: Config.Beacons.uuid, major: Config.Beacons.major, minor: Config.Beacons.minor)
locationManager.requestState(for: CLBeaconRegion(beaconIdentityConstraint: constraint, identifier: Config.Beacons.beaconID))
return
}
// Other stuff
}
A few tips:
You must request and obtain “always” location permission from the user in order to get a region exit callback.
If the above is granted, the region exit callback should happen 30 seconds after the beacon stops transmitting.
set locationManager.notifyEntryStateOnDisplay=true which will give you an extra 10 seconds of background scanning when the display is illuminated.
If you have trouble with the above, turn on beacon ranging and log the number of beacons that are detected each second for your beacon region. This will tell you if iOS truly believes the beacon is still being seen. If you have the setting from (3) enabled, each time you illuminate the display you should get 10 secs of ranging callbacks.
If you stop getting ranging callbacks entirely, this may indicate a permissions issue.
Make sure you really do have always location permission granted and not just “while using” permission.
I've been working on detecting iBeacons on iOS 10 with swift 3 and Xcode 8.2.1.
I've been following this tutorial and so far i've been able to get the beacons detected in the foreground.
However, what i need is for the beacons to be detected in the background as well as when the app is closed.
I've set the key NSLocationAlwaysUsageDescription in info.plist and also added Location updates in the apps Background Modes.
I'm also requesting requestAlwaysAuthorization from the user.
Everything works fine till i add the following statement to the code: locationManager.startMonitoring(for: beaconRegion)
After i add the above statement and run the code, my app detects the beacon in the foreground and prints me a message. But as soon as i minimise the app and reopen it, the app cant seem to find the beacon. If i comment the line out and then rerun my program, the app detects the beacon in foreground as well as when i minimise and reopen the app again.
I don't understand what i'm doing wrong.
Here is my ViewController code:
import UIKit
import CoreLocation
class ViewController: UIViewController, CLLocationManagerDelegate {
var locationManager: CLLocationManager!
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.allowsBackgroundLocationUpdates = true
locationManager.requestAlwaysAuthorization()
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedAlways {
if CLLocationManager.isMonitoringAvailable(for: CLBeaconRegion.self) {
if CLLocationManager.isRangingAvailable() {
startScanning()
}
}
}
}
func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
if beacons.count > 0 {
NSLog("Found beacon")
} else {
NSLog("Beacon not found")
}
}
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
let beaconRegion = region as! CLBeaconRegion
print("Did enter region: " + (beaconRegion.major?.stringValue)!)
}
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
let beaconRegion = region as! CLBeaconRegion
print("Did exit region: " + (beaconRegion.major?.stringValue)!)
}
func startScanning() {
let uuid = UUID(uuidString: "2f234454-cf6d-4a0f-adf2-f4911ba9ffa6")!
let beaconRegion = CLBeaconRegion(proximityUUID: uuid, major: CLBeaconMajorValue(0), minor: CLBeaconMinorValue(1), identifier: "MyBeacon")
beaconRegion.notifyEntryStateOnDisplay = true
beaconRegion.notifyOnEntry = true
beaconRegion.notifyOnExit = true
// locationManager.startMonitoring(for: beaconRegion)
locationManager.startRangingBeacons(in: beaconRegion)
}
}
Here's what my log looks like:
Found beacon
Found beacon
Found beacon
// App minimised and reopened here
Found beacon
Found beacon
Found beacon
Found beacon
Found beacon
Found beacon
Beacon not found
Beacon not found
Beacon not found
Beacon not found
Beacon not found
Beacon not found
Try adding the monitoring delegate callbacks didEnter(region: region) and didExit(region: region). Not sure why it would change things, bit it is unusual that the code starts monitoring but does not have them.
EDIT: After seeing updated code, I suspect the issue may be the way ranging is started. Instead of starting it in the didChangeAuthorization callback, just start it in viewDidLoad. The code will not crash if authorization has not been given. It just won't actually scan until it is.
Interaction with iBeacons can be done with ranging en monitoring. Ranging for a beacon provides you the value/data of the iBeacon, like uuid, major & minors.
The problem with ranging is that apple accepts only a few (circa 10 seconds) of ranging time in the background. Monitoring can be done in the background. You should check if you can range in background (print/logs) ifso, detecting the ibeacon is the problem.
ps: When you start ranging inside a region the didEnter delegate method doesn't get called, because you are already in the region.
I am working on watch heart beat app and I want when user heart rate critical then we will get his current location in(foreground, background and terminate) and send it to our server. Is there any way through which I get user location only at that position. I don't want to update his location every time.
To get user location you have to declare :
let locationManager = CLLocationManager()
in your controller.
Then, in viewDidLoad you have to request for location and initialize the CLLocationManager get process :
// Ask for Authorisation from the User.
self.locationManager.requestAlwaysAuthorization()
// For use in foreground
self.locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
}
You will get location in CLLocationManagerDelegate :
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
var location:CLLocationCoordinate2D = manager.location.coordinate
print("locations = \(location.latitude) \(location.longitude)")
}
In your info.plist you have to add NSLocationAlwaysUsageDescription and custom alert message to show while requesting for location.
cheers...
Same code, I'm assuming that the device is actually updating the location twice for some reason, even though I only call startUpdatingLocation() once and I run some stopUpdatingLocations() inside of didUpdateLocations
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
manager.stopUpdatingLocation()
let loc: CLLocation = locations[locations.count - 1]
let id = 0
let type = 0
let number = 0
createNewDataPoint(id, loc: loc, type: type, number: number)
}
In this case, createNewDataPoint gets called twice, creating 2 new datapoints. It only happens once in the simulator, so I'm assuming it has something to do with the actual device and the GPS since the simulator fakes its location.
startUpdatingLocation() is only in my code one time, on a button. Basically, you click the button, go go manager.startUpdatingLocations(), didUpdateLocations hits once on simulator, twice on device (identical coordinates) and it creates 2 new data points.
The only other code that mentions anything related is setting the accuracy, filter, authorization requests, and the previously mentioned startUpdatingLocation(). Is there something I can do to make sure I'm not creating twice as many data points as necessary?
Location Manager delegate methods can be called very frequently and at any time.
You may however, apply following algorithm to safeguard yourself:
Create a global bool say didFindLocation.
Set didFindLocation to false when you call startUpdatingLocation.
Inside delegate call back didUpdateLocations:, if didFindLocation was false, set didFindLocation to true and then call stopUpdatingLocation.
Hope this helps.
The best way is do as following:
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
manager.stopUpdatingLocation()
manager.delegate = nil
}
Best solution for iOS 10.0+
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
[locationManager stopUpdatingLocation]; // stop location manager
locationManager.delegate = nil;
//Your logics...
//This will be called only one time now.
}
But don't forget to set the delegate again.
After getting the desired latitude and longitude just call stopUpdatingLocation()and set the delegate to nil.
In Swift 3:
locationManager.stopUpdatingLocation()
locationManager.delegate = nil
In Objective-C:
[locationManager stopUpdatingLocation]
locationManager.delegate = nil
Here locationManager is the object of CLLocationManager.
You will not get frequently on simulator. and on device when you will move far away then only you get didUpdateLocations. just move in a open space so GPS can identify you device location so it get best accuracy.
Instead of starting / ending the location update and setting delegate to nil, there is a method called requestLocation which is ideal when your application need quick fix on the user's location:
From the docs:
override func viewDidLoad() {
// Create a location manager object
self.locationManager = CLLocationManager()
// Set the delegate
self.locationManager.delegate = self
}
func getQuickLocationUpdate() {
// Request location authorization
self.locationManager.requestWhenInUseAuthorization()
// Request a location update
self.locationManager.requestLocation()
// Note: requestLocation may timeout and produce an error if authorization has not yet been granted by the user
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// Process the received location update
}
Use this method when you want the user’s current location but do not need to leave location services running.
#Zumry Mohamed 's solution is right
i try the code like this:
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
[self.locationManager stopUpdatingLocation];
self.locationManager.delegate = nil;
self.locationManager = nil;
}
finally this delegate is called only once, i understand now why the problem is occurred, just because manager call the stopUpdatingLocationmethod but system doesn't help us to make the delegate invalid, so we can receive the callback every time location updates due to your desiredAccuracy and distanceFilter property settings of your CLLocationManager, so the final solution is just like what #Zumry Mohamed said, we can manually set the delegate to nil when we stopUpdateLocation. hope it will help you understand what happens why this could solve the problem.
locationManager.startUpdatingLocation() fetch location continuously and didUpdateLocations method calls several times,
Just set the value for locationManager.distanceFilter value before calling locationManager.startUpdatingLocation().
As I set 200 meters(you can change as your requirement) working fine
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = 200
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
Another way is to set a time interval to turn on and off the delegate and so the location manager. A sorta of this
var locationManagerUpdate:Bool = false //Global
func scheduledTimerWithTimeInterval(){
// Scheduling timer to Call the function "updateCounting" with the interval of 10 seconds
timer = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(self.updateLocationManager), userInfo: nil, repeats: true)
}
#objc func updateLocationManager() {
if locationManagerUpdate == false {
locationManager.delegate = self
locationManagerUpdate = true
}
}
extension lm_gest: CLLocationManagerDelegate {
// Handle incoming location events.
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if locationManagerUpdate == true {
manager.stopUpdatingLocation()
manager.delegate = nil
}
//your code here...
}