I am using the core location framework to detect beacons in my App. I register the beacon region with a specific Proximity UUID.
uuidString = "74278bda-b644-4520-8f0c-720eaf059935"
beaconRegionIdentifier = "ios.test"
beaconUUID = UUID(uuidString: uuidString)
beaconRegion = CLBeaconRegion(proximityUUID: beaconUUID!, identifier: beaconRegionIdentifier)
and then implement the following CLLocationManagerDelegate methods
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
manager.startRangingBeacons(in: region as! CLBeaconRegion)
print(region)
manager.startUpdatingLocation()
print("Detected a beacon")
}
func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
...
// Ranging beacons here
print(beacons)
}
The log that I am getting is this:
[CLBeacon (uuid:74278BDA-B644-4520-8F0C-720EAF059935, major:200, minor:51, proximity:1 +/- 0.17m, rssi:-45)]
[CLBeacon (uuid:74278BDA-B644-4520-8F0C-720EAF059935, major:200, minor:51, proximity:1 +/- 0.17m, rssi:-45)]
[CLBeacon (uuid:74278BDA-B644-4520-8F0C-720EAF059935, major:200, minor:51, proximity:0 +/- -1.00m, rssi:0)]
[CLBeacon (uuid:74278BDA-B644-4520-8F0C-720EAF059935, major:200, minor:51, proximity:0 +/- -1.00m, rssi:0)]
[]
[]
[]
i.e The didRangeBeacons method detects the beacon for only once then suddenly the [CLBeacon] array becomes empty even though the beacon is transmitting data packets the entire time.
Can someone please explain this strange behaviour?
PS: Once the app is in the background state, the beacons are detected perfectly. These logs are only seen when the app is in foreground
TL;DR
iOS beacon app not detecting beacons properly in foreground.
EDIT
The beacons use the same code for detecting in background, i.e. foreground and background detection is done using the same functions. I just request additional time from iOS to scan beacons in background
func extendBackgroundTime() {
if backgroundTask != UIBackgroundTaskInvalid {
return
}
let self_terminate = false
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "RangeBeacons", expirationHandler: {() -> Void in
print("Background Task Terminated By iOS")
if self_terminate {
UIApplication.shared.endBackgroundTask(self.backgroundTask)
self.backgroundTask = UIBackgroundTaskInvalid
}
})
}
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 followed an online tutorial and had the following code working fine in my project, it can detect an iBeacon with a specific uuid/major/minor and do some logic with it. I'm wondering if there is a way where I can accept multiple uuid's or multiple major/minors and pass them on to other functions? Here is the code I have so far:
class BeaconDetector: NSObject, ObservableObject, CLLocationManagerDelegate {
var locationManager: CLLocationManager?
#Published var lastDistance = CLProximity.unknown
override init(){
super.init()
locationManager = CLLocationManager()
locationManager?.delegate = self
locationManager?.requestAlwaysAuthorization()
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus){
if status == .authorizedAlways{
if CLLocationManager.isMonitoringAvailable(for: CLBeaconRegion.self) {
if CLLocationManager.isRangingAvailable(){
startScanning()
}
}
}
}
func startScanning(){
let uuid = UUID(uuidString: "2F234454-CF6D-4A0F-ADF2-F4911BA9FFA6")!
let constraint = CLBeaconIdentityConstraint(uuid: uuid, major: 0, minor: 0)
let beaconRegion = CLBeaconRegion(beaconIdentityConstraint: constraint, identifier: "MyBeacon")
locationManager?.startMonitoring(for: beaconRegion)
locationManager?.startRangingBeacons(satisfying: constraint)
}
func locationManager(_ manager: CLLocationManager, didRange beacons: [CLBeacon], satisfying beaconConstraint: CLBeaconIdentityConstraint) {
if let beacon = beacons.first{
update(distance: beacon.proximity)
} else{
update(distance: .unknown)
}
}
func update(distance: CLProximity) {
lastDistance = distance
}
}
I think I know how to pass the values by just adding it to the update function, but how would I be able to detect and accept multiple beacons would be the biggest question I have right now, thanks!
You can set up location monitoring in the location manager for up to 20 regions. Those regions can either be GPS based geofence regions or "Beacon regions", or any mix of the two, but you are limited to 20. To register multiple beacon regions, you'd just call your locationManager?.startMonitoring(for: beaconRegion) code more than once.
When you create beacon regions you must specify a UUID. You can make it a specific UUID and wildcard major/minor, A specific UUID and major and wildcard minor, or specific values for UUID, major, and minor ID. (I don't remember if you can specify UUID and minor and have it work for any major. It's been a while.)
If you use a wildcard for major or minor ID, the system treats any device that matches as being part of the same region, and you have to write code that figures out which specific beacon was detected. I seem to remember that once you've entered a region with a wildcard (say a specific UUID and any major or minor) then you won't get new "entered region" notifications if a second beacon with a different major/minor is detected. That’s treated as part of the same region. In that case you need to start listening to specific beacon notifications and look at the values you get for each one.
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) {
// ...
}
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.