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
Related
I am trying to get my google maps map to center on my user's location. The user's location updates properly and shows the blue dot indicating their location. However, the camera, also set to the same coordinate locations, instead places itself at (0, 0). I have modified the program below so that it does move to the proper location after it loads. However, since my Bool is a state variable, it gives me a warning that I cannot update State variables during view updates. See the code below:
import SwiftUI
import GoogleMaps
import GoogleMapsUtils
struct GoogleMapsView: UIViewRepresentable {
#ObservedObject var locationManager = LocationManager()
var marker: GMSMarker = GMSMarker()
#Binding var heatmapWeightedData: [GMUWeightedLatLng]
#State var heatmapLayer = GMUHeatmapTileLayer()
#State var isCenteredOnCamera = false
func makeUIView(context: Context) -> GMSMapView {
let camera = GMSCameraPosition.camera(withLatitude: locationManager.latitude, longitude: locationManager.longitude, zoom: 15)
let mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
heatmapLayer.radius = 75
heatmapLayer.weightedData = heatmapWeightedData
heatmapLayer.map = mapView
mapView.isMyLocationEnabled = true
return mapView
}
func updateUIView(_ mapView: GMSMapView, context: UIViewRepresentableContext<GoogleMapsView>) {
if self.isCenteredOnCamera == false {
mapView.animate(to: GMSCameraPosition(latitude: locationManager.latitude, longitude: locationManager.longitude, zoom: 15))
self.isCenteredOnCamera = true //Gives me the error
}
heatmapLayer.weightedData = heatmapWeightedData
heatmapLayer.map = mapView
}
}
Here is the locationManager code:
import Foundation
import CoreLocation
class LocationManager: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
#Published var location: CLLocation? {
willSet { objectWillChange.send() }
}
var latitude: CLLocationDegrees {
return location?.coordinate.latitude ?? 0
}
var longitude: CLLocationDegrees {
return location?.coordinate.longitude ?? 0
}
override init() {
super.init()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager( manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
print(status)
}
func locationManager( manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else {
return
}
self.location = location
}
}
Presumably, the camera is just reading the optional longitude and latitude variables and setting them to 0, for reasons I do not know.
Is there a way for me to update the camera location just once so that it does not infinitely update the camera location? If so, what should I do?
In my project, I'm fetching the data about the houses/buildings and along with other details, I show how far it is from the user. I'm only a beginner, so I will try to be as much explicit as possible.
My issue is that I don't know where to put the function that calculates the distance in KM and how to call it properly in the MVVM project. See I have a ViewModel file that includes ViewModel class responsible for Networking and a LocationManager class
responsible for tracking user location. The latitude and longitude come from the ViewModel from API and I believe the distance calculation should be made in LocationManager. I'm not sure how can I "connect" these two classes.
My main goals are:
Figure out where to put the func to calculate the distance. User coords are accessible from the LocationManager and house coords are accessible from the API. I would like to know if there's a way to merge these two classes to use the data in one func.
Understand if the distanceInKM method is correct. Despite it doesn't throw, it still displays a placeholder value.
As the minimum reproducible project, here's the code:
ContentView:
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
#StateObject var locationManager = LocationManager()
var body: some View {
VStack {
HouseListView(housesVM: viewModel)
}
.onAppear {
viewModel.fetchHouses()
}
}
}
HouseListView(A view that's called in ContentView:
struct HouseListView: View {
#ObservedObject var housesVM: ViewModel
#StateObject var locationManager = LocationManager()
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading) {
ForEach(housesVM.info, id: \.id) { house in
GetHouseCellView(
distance: Double(locationManager.distanceInKM(latitude: house.latitude, longitude: house.longitude)) ?? 0.00, //Here's the place where function is called
bedrooms: house.bedrooms)
}
}
}
}
private func GetHouseCellView(distance: Double, bedrooms: Int) -> some View {
HStack(spacing: 20) {
Label("\(bedrooms)", image: "bed-2")
Label("\(distance) km", image: "pin") //Here the final distance should be displayed, i.e 36,4 km
}
}
Quite basic ViewModel:
class ViewModel: ObservableObject {
#Published var info: [Houses] = []
func fetchHouses() {
//URL & URLRequest here
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
let decoder = JSONDecoder()
do {
if let data = data {
let result = try decoder.decode([Houses].self, from: data)
DispatchQueue.main.async {
self.info = result
}
}
} catch {
print("whoopsie! There's an error: \(error.localizedDescription)")
}
}
dataTask.resume()
}
}
}
And a LocationManager:
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
#Published var locationStatus: CLAuthorizationStatus?
#Published var lastLocation: CLLocation?
override init() {
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
var statusString: String {
guard let status = locationStatus else {
return "unknown"
}
switch status {
case .notDetermined: return "notDetermined"
case .authorizedWhenInUse: return "authorizedWhenInUse"
case .authorizedAlways: return "authorizedAlways"
case .restricted: return "restricted"
case .denied: return "denied"
default: return "unknown"
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
locationStatus = status
print(#function, statusString)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
lastLocation = location
print(#function, location)
}
func distanceInKM(latitude: Int, longitude: Int) -> Double { //Here's the method I made to calculate an actual distance
let houseCoordinates = CLLocation(latitude: CLLocationDegrees(latitude), longitude: CLLocationDegrees(longitude))
let userCoordinates = CLLocation(latitude: lastLocation?.coordinate.latitude ?? 50, longitude: lastLocation?.coordinate.longitude ?? 30)
let distance = userCoordinates.distance(from: houseCoordinates) / 1000 //.distance comes in meters so /1000 is to have a KM value
let s = String(format: "%.0f", distance)
return Double(s + "Km") ?? 35.5 // This value of 35.5 as placeholder is constantly displayed instead of the actual value
}
}
This func logic was taken from this post
Simple type error :
Double(s + "Km")
will always return nil as s + "Km" is not a valid double.
Just Return :
Double(distance)
If you want to return a String change the type of the method and return
s + "km"
EDIT :
If you want to display distance in km : in GetHouseCellView change distance label to :
Label(String(format: "%.2f km", distance), image: "pin")
It is better to format the distance Double when you use it.
I'm pretty new to coding in general and to get to this point I've been following multiple tutorials online which may have caused my code to be jumbled so I apologise for this.
I have started creating an app using MapKit with map annotations in my home town. I am currently living abroad so my location is not where these map annotations are. When I accept the location permissions the map defaults to the first annotation pin where I would like it to default to my current location.
My code is below and any help/explanation would be greatly appreciated.
Locations View Model
'''class LocationsViewModel: ObservableObject {
#Published var locations: [Location]
#Published var mapLocation: Location {
didSet {
updateMapRegion(location: mapLocation)
}
}
#Published var mapRegion: MKCoordinateRegion = MKCoordinateRegion()
let mapSpan = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
init() {
let locations = LocationsDataServices.locations
self.locations = locations
self.mapLocation = locations.first!
self.updateMapRegion(location: locations.first!)
}
private func updateMapRegion(location: Location) {
withAnimation(.easeInOut) {
mapRegion = MKCoordinateRegion(
center: location.coordinates,
span: mapSpan)
}
}
func showNextLocation(location: Location) {
withAnimation(.easeInOut) {
mapLocation = location
}
}
}'''
Locations Services Model
'''import MapKit
enum MapDetails {
static let startingLocation = CLLocationCoordinate2D(latitude: ***, longitude: ***)
static let defaultSpan = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
}
final class LocationServicesModel: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var region = MKCoordinateRegion(center: MapDetails.startingLocation, span: MapDetails.defaultSpan)
var locationManager: CLLocationManager?
func checkIfLocationServicesIsEnabled() {
if CLLocationManager.locationServicesEnabled() {
locationManager = CLLocationManager()
locationManager!.delegate = self
locationManager?.desiredAccuracy = kCLLocationAccuracyBest
} else {
print("turn on location manager")
}
}
private func checkLocationAuthorisation() {
guard let locationManager = locationManager else { return }
switch locationManager.authorizationStatus {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .restricted:
print("location is restricted")
case .denied:
print("location has been denied, go into settings to change permission")
case .authorizedAlways, .authorizedWhenInUse:
break
#unknown default:
break
}
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
checkLocationAuthorisation()
}'''
The code works to a degree, the map annotations are placed correctly and it does show my current location but I'm not sure where or how I'd get the map to centre of the user location on acceptance of location permissions instead of the coordinates I have input above.
Thanks!
Once you have the required permission you need to invoke startUpdatingLocation()
locationManager.startUpdatingLocation()
This starts the generation of updates that report the user’s current location. These updates are available to you via the delegate method:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// Ensure we have an actual location in the update.
guard let location = locations.last else { return }
// Do something with the location.
}
The location is a CLLocation and you can extract the coordinate from that.
let span = MKCoordinateSpan(latitudeDelta: 0.009, longitudeDelta: 0.009)
let region = MKCoordinateRegion(center: location.coordinate, span: span)
mapView.setRegion(region, animated: true)
I have google map and want to display user's place (like city) in snippet.
How to do that?
here's my current code:
class ViewController: UIViewController, GMSMapViewDelegate, CLLocationManagerDelegate{
#IBOutlet weak var mapView: GMSMapView!
var latitude = -7.034323799999999
var longitude = 110.42400399999997
var locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
mapView.delegate = self
let camera = GMSCameraPosition.camera(withLatitude: Double(latitude), longitude: Double(longitude), zoom: 17)
mapView.animate(to: camera)
let markerImage = UIImage(named: "ic_home_detail_marker_location")
let markerView = UIImageView(image: markerImage)
let marker = GMSMarker()
marker.position = CLLocationCoordinate2DMake(Double(latitude), Double(longitude))
marker.isDraggable = true
marker.snippet = "\(marker.position)"
mapView.selectedMarker = marker
marker.iconView = markerView
mapView.selectedMarker = marker
marker.map = mapView
}
}
If you want to get the user's city or state name you have to use CLGeocoder.
var currentLatitude:Double!
var currentLongitude:Double!
var cityName:String!
var stateName:String!
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("locationManager function called")
// Fetch current location coordinates
let locValue:CLLocationCoordinate2D = (locationManager.location?.coordinate)!
currentLatitude = locValue.latitude
currentLongitude = locValue.longitude
print("Current Location = \(currentLatitude!), \(currentLongitude!)")
// Zoom to current location
let camera: GMSCameraPosition = GMSCameraPosition.camera(withLatitude: currentLatitude!, longitude: currentLongitude!, zoom: 17.0)
viewMap.camera = camera
// check for current city
CLGeocoder().reverseGeocodeLocation(locationManager.location!, completionHandler: {(placemarks, error) -> Void in
if error != nil {
print("Reverse geocoder failed with error" + (error?.localizedDescription)!)
return
}
if (placemarks?.count)! > 0 {
let pm = placemarks?[0]
self.cityName = (pm?.locality)!
self.stateName = (pm?.administrativeArea)
print("Current City: \(self.cityName!)")
print("Curret State: \(self.stateName!)")
}
else {
print("Problem with the data received from geocoder")
}
})
locationManager.stopUpdatingLocation()
}
Now you have the current city stored in a variable.
The next step is that when the user touches a marker, it should display the cityname. For this to achieve implement this:
This delegate must be added:
GMSMapViewDelegate
and this is the marker function, when user taps on it.
func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool {
mapView.delegate = self
marker.snippet = ("Current city: \(cityName!)")
// return false so as to show the marker details or
// return true to hide marker details.
return false
}
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.