didEnterRegion firing for all my geofences - swift

All my geofences are triggering when GPS enters a defined region, at first I thought it was because of the radius, however even after halving it I am having the same problem.
import UIKit
import CoreLocation
class itemDesc {
var title: String
var coordinate: CLLocationCoordinate2D
var regionRadius: CLLocationDistance
var location: String
var type: String
init(title: String, coordinate: CLLocationCoordinate2D, regionRadius: CLLocationDistance, location: String, type: String) {
self.title = title
self.coordinate = coordinate
self.regionRadius = regionRadius
self.location = location
self.type = type
}
}
class ViewController: UIViewController, CLLocationManagerDelegate {
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
locationManager.requestAlwaysAuthorization()
locationManager.startUpdatingLocation()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
setupData()
}
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
}
func locationManager(manager: CLLocationManager, monitoringDidFailForRegion region: CLRegion?, withError error: NSError) {
print("Monitoring failed for region with identifier: \(region!.identifier)")
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
print("Location Manager failed with the following error: \(error)")
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let locValue:CLLocationCoordinate2D = manager.location!.coordinate
print("locations = \(locValue.latitude) \(locValue.longitude)")
}
func handleRegionEvent(region: CLRegion!) {
print("Geofence triggered \(region.identifier)")
}
func locationManager(manager: CLLocationManager, didEnterRegion region: CLRegion) {
if region is CLCircularRegion {
handleRegionEvent(region)
}
}
func locationManager(manager: CLLocationManager, didExitRegion region: CLRegion) {
if region is CLCircularRegion {
}
}
func setupData(){
if CLLocationManager.isMonitoringAvailableForClass(CLCircularRegion.self) {
let itemRegion = [
itemDesc( title: "DOOR", coordinate: CLLocationCoordinate2DMake(00.497699, 00.575095), regionRadius: 0.5, location: "DOOR", type: "exterior"),
itemDesc( title: "BARN FRONT", coordinate: CLLocationCoordinate2DMake(00.49751, 00.575149), regionRadius: 0.5, location:"BARN FRONT", type: "exterior"),
itemDesc( title: "GRASS", coordinate: CLLocationCoordinate2DMake(00.497337, 00.575069), regionRadius: 0.5, location: "GRASS ", type: "nature")]
for item in itemRegion {
let coordinate = item.coordinate
let regionRadius = item.regionRadius
let title = item.title
let region = CLCircularRegion(center: coordinate, radius: regionRadius, identifier: title)
locationManager.startMonitoringForRegion(region)
}
} else{
print("system can't track regions")
}
}
}
Using (0.497337, 0.575069) I'd only expect the GRASS fence to be triggered, this is not happening.
Outputs:
regionRadius = 1.0
locations = 37.33233141 -122.0312186
locations = 37.33233141 -122.0312186
locations = 0.497337 0.575069
Geofence triggered BARN FRONT
Geofence triggered DOOR
Geofence triggered GRASS

regionRadius = 0.5
locations = 37.33233141 -122.0312186
locations = 37.33233141 -122.0312186
locations = 0.497337 0.575069
Geofence triggered BARN FRONT
Geofence triggered DOOR
Geofence triggered GRASS
Although even at 1m this should not have been a problem:
The fourth decimal place is worth up to 11 m
The fifth decimal place is worth up to 1.1 m
The sixth decimal place is worth up to 0.11 m

The best accuracy with the GPS chip and kCLLocationAccuracyBestForNavigation is often just 10 meters.
Apple says (in the Location & Maps PG) that the minimum distance for regions should be assumed to be 200m
as pointed out by this answer - it will help but not please you
https://stackoverflow.com/a/23931552/2027018

If it helps anybody out who stumbles across this, I didn't get any further with CLRegion.
Instead, I went with CLLocation class and use .distanceFromLocation and worked out the distance for each of my regions/locations.

Related

Onchange of variable in class

I have a class such as:
class LocationViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var lastSeenLocation: CLLocation?
#Published var currentPlacemark: CLPlacemark?
#Published var authorizationStatus: CLAuthorizationStatus
private let locationManager: CLLocationManager
override init() {
locationManager = CLLocationManager()
authorizationStatus = locationManager.authorizationStatus
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
func requestPermission() {
locationManager.requestAlwaysAuthorization()
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
authorizationStatus = manager.authorizationStatus
}
}
I'm trying to check if last seen location = cordinates using this code:
let radius: Double = 5 // miles
let userLocation = CLLocation(latitude: locationViewModel.lastSeenLocation?.coordinate.latitude, longitude: locationViewModel.lastSeenLocation?.coordinate.longitude)
let venueLocation = CLLocation(latitude: 51.500909, longitude: -0.177366)
let distanceInMeters = userLocation.distanceFromLocation(venueLocation)
let distanceInMiles = distanceInMeters * 0.00062137
if distanceInMiles < radius {
// user is near the venue
}
The only problem is, that I don't know how to run that code to check constantly. I was thinking .onChange but couldn't figure out how to test for lastSeenlocation in a class. What can I do?
I found a solution here.
For a quick overview:
//I put this code in my LocationViewModel class
func getUserLocation() {
locationManager.startUpdatingLocation()
locationManager.delegate = self
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
print("latitude: \(location.coordinate.latitude), longitude: \(location.coordinate.longitude)")
}
}
//I put this code in my ContentView()
.onAppear {
locationViewModel.getUserLocation()
}
Then it prints your latitude & longitude every time your location changes.

Code not working for monitoring >20 regions. Swift

I have 42 geolocation regions to monitor, I know that Apple only allows 20 at a time, so I tried to employ the answer that was given here: How to monitor more than 20 regions?
But I still can't trigger a notification at a region above 20. I've been trying to figure this out for days now and I feel like I'm just not seeing something. Can someone help please? The CLLocationManagerDelegate block of code is below, but if you wanted to see the entire ViewController for this part I put it here: full ViewController
extension SearchFormViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
var currentLocation : CLLocation?{
didSet{
evaluateClosestRegions()
}
}
let allRegions : [CLRegion] = [] // Fill all your regions
func evaluateClosestRegions() {
var allDistance : [Double] = []
//Calulate distance of each region's center to currentLocation
for region1 in allRegions{
let circularRegion = region1 as! CLCircularRegion
let distance = currentLocation!.distance(from: CLLocation(latitude: circularRegion.center.latitude, longitude: circularRegion.center.longitude))
allDistance.append(distance)
}
guard let location = locations.last else {
return
}
currentLocation = location
// a Array of Tuples
let distanceOfEachRegionToCurrentLocation = zip(allRegions, allDistance)
//sort and get 20 closest
let twentyNearbyRegions = distanceOfEachRegionToCurrentLocation
.sorted{ tuple1, tuple2 in return tuple1.1 < tuple2.1 }
.prefix(20)
// Remove all regions you were tracking before
for region1 in locationManager.monitoredRegions{
locationManager.stopMonitoring(for: region1)
}
twentyNearbyRegions.forEach{
locationManager.startMonitoring(for: $0.0)
}
}
}
A number of things, first define your regions, move that let allRegions to be a property on the view controller.
I haven't tested this but I would change allRegions to be an array of CLCircularRegion since that's all we need anyway, that gets rid of the type casting:
SearchFormViewController {
let allRegions : [CLCircularRegion] = [
// TODO actually have your regions in here
CLCircularRegion(center: CLLocationCoordinate2D(latitude: 1, longitude: 2), radius: 200, identifier: "A"),
CLCircularRegion(center: CLLocationCoordinate2D(latitude: 2, longitude: 3), radius: 100, identifier: "B"),
// etc
]
Second move evaluateClosestRegions out into a method on the view controller, no need for it to be a nested function. I also have it take a location in as an argument:
func evaluateClosestRegions(from location: CLLocation) {
// sort and get 20 closest
let twentyNearbyRegions: [(CLCircularRegion, CLLocationDistance)] = allRegions.map { region in
let center = CLLocation(latitude: circularRegion.center.latitude,
longitude: circularRegion.center.longitude)
let distance = center.distance(from: location)
}
.sorted { $0.1 < $1.1 }
.prefix(20)
// Remove all regions you were tracking before
for region in locationManager.monitoredRegions {
locationManager.stopMonitoring(for: region)
}
twentyNearbyRegions.forEach {
locationManager.startMonitoring(for: $0.0)
}
}
}
Importantly, in the location manager delegate, call the evaluateClosestRegions function, if you have a location. You may also want to consider only calling that if the user has moved enough since the last time you checked
extension SearchFormViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let currentLocation = locations.last {
evaluateClosestRegions(from: currentLocation)
}
}
I would also suggest one idea to improve your code which is basically to make your data smarter so that your code doesn't have to be so smart. If you introduce a struct that represents your data:
struct Content: Identifiable, Equatable, Hashable {
static func == (lhs: SearchFormViewController.Content, rhs: SearchFormViewController.Content) -> Bool {
lhs.id == rhs.id
}
var id: Int
var title: String
var center: CLLocationCoordinate2D
var radius: CLLocationDistance = 150
var region: CLCircularRegion {
let region = CLCircularRegion(center: center, radius: radius, identifier: "Geofence\(id)")
region.notifyOnEntry = true
region.notifyOnExit = true
return region
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
Now you can define any number of content items:
var allContent: [Content] = [
Content(id: 1, title: "The Lime Light", center: .init(latitude: 45.49894, longitude: -73.5751419)),
Content(id: 2, title: "Sans Soleil Bar", center: .init(latitude: 45.5065647, longitude: -73.5626957)),
Content(id: 3, title: "S.A.T.", center: .init(latitude: 45.5098557, longitude: -73.5658257))
]
And put them into a collection when they are found etc:
var found: Set<Content> = []
var library: Set<Content> = []
This becomes simple:
func resetContentOnSignOut() {
found = []
library = []
}
func didFind(contentId: Int) {
if let content = allContent.first(where: { $0.id == contentId }) {
found.insert(content)
library.insert(content)
}
}
func hasFound(_ contentId: Int) -> Bool {
found.contains { $0.id == contentId }
}
func content(withRegionIdentifier id: String) -> Content? {
found.first { $0.region.identifier == id }
}
func locationManager(_ manager: CLLocationManager, didEnterRegion region1: CLRegion) {
print("User has entered \(region1.identifier)")///
if let content = content(withRegionIdentifier: region1.identifier),
!hasFound(content.id) {
didFind(content)
}
}
And you can remove a lot of duplicate code like there are lots of places that do the same things:
func didFind(_ content: Content) {
found.insert(content)
library.insert(content)
contentFoundButtonActive()
sendNotification()
storeUserGeofences()
addToAudioGemCounter()
updateGemsCollectedCounter()
print("Content \(content.id) Found: \(Array(self.found)).")
}
Just as a general idea, this isn't meant to be working code

Swift request geolocation from non-view

I'm trying to get geolocation data in an arbitrary class. I'm very new to Swift, so I have no idea why this isn't working?
Any pointers?
import Foundation
import UIKit
import CoreLocation
class GeolocationPlugin:NSObject, CLLocationManagerDelegate {
var locationManager: CLLocationManager!
var lat: Double = 0
var long: Double = 0
func getLocation() {
print("Getting location")
// For use in foreground
self.locationManager = CLLocationManager()
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
// locationManager.startMonitoringSignificantLocationChanges()
func locationManager(manager: CLLocationManager!, didFailWithError error: NSError) {
print("Error while updating location " + error.localizedDescription)
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [CLLocation]) {
let locValue:CLLocationCoordinate2D = manager.location!.coordinate
print("locations = \(locValue.latitude) \(locValue.longitude)")
}
self.locationManager.requestLocation()
print("gets here")
}
}
I currently see Getting location and then an error:
2017-03-26 15:42:32.634 IonicRunner[42304:5668243] *** Assertion failure in -[CLLocationManager requestLocation], /BuildRoot/Library/Caches/com.apple.xbs/Sources/CoreLocationFramework_Sim/CoreLocation-2100.0.34/Framework/CoreLocation/CLLocationManager.m:865
2017-03-26 15:42:32.638 IonicRunner[42304:5668243] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Delegate must respond to locationManager:didUpdateLocations:'
The solution wound up being to move the methods out of getLocation(), to properly activate a location in the simulator, and to move where this class was initiated from, so it wasn't immediately released as soon as getLocation() completes.
import Foundation
import UIKit
import CoreLocation
class GeolocationPlugin:NSObject, CLLocationManagerDelegate {
var locationManager = CLLocationManager()
var lat: Double = 0
var long: Double = 0
var cb: ((Double, Double) -> Void)? = nil
func getLocation(callback: #escaping (Double, Double) -> Void) {
print("Getting location")
// For use in foreground
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestLocation()
self.cb = callback
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: NSError) {
print("Error while updating location " + error.localizedDescription)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let locValue:CLLocationCoordinate2D = manager.location!.coordinate
//print("locations = \(locValue.latitude) \(locValue.longitude)")
if( self.cb != nil) {
self.cb!(locValue.latitude, locValue.longitude)
}
}
}

Swift2 didRangeBeacons never called

I'm trying to implement a dedicated class to scan for a specific iBeacon (in foreground and in background) but the function didRangeBeacons is never called.
I also set the parameter NSLocationAlwaysUsageDescription in my info.plist.
here is the class I developed:
class iBeacon: NSObject, UIApplicationDelegate, CLLocationManagerDelegate {
var locationManager: CLLocationManager!
var beaconRegion:CLBeaconRegion!
override init(){
super.init()
print("init...")
let uuidString = "B737D0E7-AF53-9B83-E5D2-922140A91234"
let beaconIdentifier = "nbuit-06B908"
let beaconUUID:NSUUID = NSUUID(UUIDString: uuidString)!
beaconRegion = CLBeaconRegion(proximityUUID: beaconUUID,
identifier: beaconIdentifier)
beaconRegion.notifyOnEntry = true
locationManager = CLLocationManager()
locationManager.delegate = self
if(locationManager!.respondsToSelector("requestAlwaysAuthorization")) {
locationManager!.requestAlwaysAuthorization()
}
locationManager!.pausesLocationUpdatesAutomatically = false
locationManager!.startMonitoringForRegion(beaconRegion)
locationManager!.startRangingBeaconsInRegion(beaconRegion)
locationManager!.startUpdatingLocation()
}
func sendLocalNotificationWithMessage(message: String!) {
let notification:UILocalNotification = UILocalNotification()
notification.alertBody = message
UIApplication.sharedApplication().scheduleLocalNotification(notification)
}
func locationManager(manager: CLLocationManager!, didStartMonitoringForRegion region: CLRegion!){
locationManager.requestStateForRegion(region)
}
func locationManager(manager: CLLocationManager!, didDetermineState state: CLRegionState, forRegion region: CLRegion!) {
if state == CLRegionState.Inside {
locationManager.startRangingBeaconsInRegion(beaconRegion)
}
else {
locationManager.stopRangingBeaconsInRegion(beaconRegion)
}
}
func locationManager(manager: CLLocationManager,
didRangeBeacons beacons: [CLBeacon],
inRegion region: CLBeaconRegion) {
print("didRangeBeacons");
var message:String = ""
if(beacons.count > 0) {
let nearestBeacon:CLBeacon = beacons[0] as! CLBeacon
switch nearestBeacon.proximity {
case CLProximity.Far:
message = "You are far away from the beacon"
case CLProximity.Near:
message = "You are near the beacon"
case CLProximity.Immediate:
message = "You are in the immediate proximity of the beacon"
case CLProximity.Unknown:
return
}
} else {
message = "No beacons are nearby"
}
print("%#", message)
sendLocalNotificationWithMessage(message)
}
func locationManager(manager: CLLocationManager!, didEnterRegion region: CLRegion!) {
print("Beacon in range")
}
func locationManager(manager: CLLocationManager!, didExitRegion region: CLRegion!) {
print("No beacons in range")
}
func locationManager(manager: CLLocationManager!, monitoringDidFailForRegion region: CLRegion!, withError error: NSError!) {
print("Failed monitoring region: \(error.description)")
}
func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!) {
print("Location manager failed: \(error.description)")
}
}
I'm not able to find the reason why my iBeacon is not detected. I also checked for the UUID and name in a BLE scanner app and it seems to be correct.
If you have any idea to help me that would be great.
EDIT:
I Finally found the reason why the LocationManager was never started, I was instantiating my class like this:
let qualityOfServiceClass = DISPATCH_QUEUE_PRIORITY_DEFAULT
let backgroundQueue = dispatch_get_global_queue(qualityOfServiceClass, 0)
dispatch_async(backgroundQueue, {
print("Running beacon detection in the background queue")
beaconSharedInstance
})
I was expecting to get this instance running in background, but by simply calling my instance like this
beaconSharedInstance
it starts and scans for regions correctly. but as soon as my app is inactive, I have the following log:
Ending background task
and my app stops scanning. I set the following parameters
locationManager.allowsBackgroundLocationUpdates = true
and also set Background Mode capability of my project.
any idea why my instance is stopped when the app is in background ?

Im trying to show my current location on a map but when I run my code in the simulator it doesn't work

// Here is some more info on the problem = Trying to start MapKit location updates without prompting for location authorization. Must call -[CLLocationManager requestWhenInUseAuthorization] or -[CLLocationManager requestAlwaysAuthorization] first.
import UIKit
import MapKit
import CoreLocation
class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet var myMap : MKMapView!
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
myMap.showsUserLocation = true }
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let locValue:CLLocationCoordinate2D = manager.location!.coordinate
print("locations = \(locValue.latitude) \(locValue.longitude)")
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError)
{
print("Errors: " + error.localizedDescription)
}
#IBAction func satelliteView(){
myMap.mapType = MKMapType.Satellite
}
#IBAction func normalView(){
myMap.mapType = MKMapType.Standard
}
#IBAction func pin(sender: UILongPressGestureRecognizer) {
let location = sender.locationInView(self.myMap)
let lCoord = self.myMap.convertPoint(location, toCoordinateFromView: self.myMap)
let anno = MKPointAnnotation()
anno.coordinate = lCoord
anno.title = "store"
anno.subtitle = "loctaion of Store"
self.myMap.removeAnnotations(myMap.annotations)
self.myMap.addAnnotation(anno)
}
}
The simulator does not know your present location. Your need to let the simulator know your location.
In the simulator menu select Debug > Location > Custom Location
You can enter the Lat and Long of your physical location or any other location.
The following is from an app I wrote. You need to call something similar when locationManager(manager:didUpdateLocations: is called
func addPinAtCoordinate(latlong: Vector2D, plotting: Bool = defaultPlotState) {
if self.plotting && !plotting {
self.removeAnnotations(annotationStack)
annotationStack = []
self.plotting = plotting
}
let lat = latlong.x
let lon = latlong.y > 180.0 ? latlong.y - 360.0 : latlong.y
let coord = CLLocationCoordinate2DMake(lat, lon)
let region = self.regionThatFits(MKCoordinateRegionMake(coord, MKCoordinateSpan(latitudeDelta: mapSpanDegrees, longitudeDelta: mapSpanDegrees)))
if region.center.latitude != -180.0 || region.center.longitude != -180.0 {
self.setRegion(region, animated: true)
let note = MKPointAnnotation()
let annotPoint = CLLocationCoordinate2D(latitude: lat, longitude: lon)
note.coordinate = annotPoint
self.addAnnotation(note)
self.annotationStack.append(note)
if annotationStack.count > annotationStackLength {
self.removeAnnotation(annotationStack[0])
annotationStack.removeAtIndex(0)
}
}
else {
NSLog("Can't place invalid cordinate: ", latlong)
}
}
Most specifically useful relative to your problem are the lines creating a region and calling setRegion.