Display the distance between two coordinates in km in SwiftUI - swift

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.

Related

Get users current location and upload it to firebase

I am trying to get the users current location in swifti and then upload it to firebase, but nothing seems to be uploaded
I tried adding print commands to the code to check whether I was getting the location but nothing was printed to the terminal, here is my code:
import MapKit
import FirebaseFirestore
struct Maps: View {
#State private var location = CLLocationCoordinate2D()
var body: some View {
Text("Hello World")
.onAppear {
let manager = LocationManager()
manager.getLocation { location in
self.location = location
print("Latitude: \(location.latitude), Longitude: \(location.longitude)")
}
}
}
}
class LocationManager: NSObject, CLLocationManagerDelegate {
let locationManager = CLLocationManager()
func getLocation(completion: #escaping (CLLocationCoordinate2D) -> ()) {
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
self.completion = completion
}
private var completion: ((CLLocationCoordinate2D) -> ())?
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
let db = Firestore.firestore()
let locationRef = db.collection("locations").document()
locationRef.setData([
"latitude": location.coordinate.latitude,
"longitude": location.coordinate.longitude,
"identifier": UUID().uuidString
])
locationManager.stopUpdatingLocation()
completion?(location.coordinate)
}
}
struct Maps_Previews: PreviewProvider {
static var previews: some View {
Maps()
}
}
I added Privacy - Request when in use.. to info, so I don’t know what the problem is, I am not getting any errors
This code
.onAppear {
let manager = LocationManager()
// ...
}
creates and then immediately discards a LocationManager, which means that the object does not live long enough in memory to even receive one callback to the delegate method.
Move manager up to be a propery of Maps to let it stay in memory as long as the view itself.

Do something in another class when the locationManagerDidChangeAuthorization method called

In the following code, I have a LocationManager class which provides the city name of the current location via the #Published property wrapper lastSearchedCity.
Then I have a SearchManagerViewModel class that should be in charge of presenting the city name on SwiftUI views based on some conditions (not currently shown in the code below) via the #Published property wrapper cityName. It properly shows the city name when I call the searchAndSetCity() method from ContentView.swift inside an onAppear modifier.
My issue is that if the user turned Location Services off and turns it back On while he/she is in the ContentView.swift the Text view doesn't update, which is understandable since the searchAndSetCity() method would need to be called again.
How can I call the searchAndSetCity() method located inside the SearchManagerViewModel class every time the locationManagerDidChangeAuthorization(_ manager: CLLocationManager) method is called? I believed this method is called every time the authorization status changes.
LocationManager Class
final class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
#Published var lastSearchedCity = ""
var hasFoundOnePlacemark:Bool = false
func checkIfLocationServicesIsEnabled(){
DispatchQueue.global().async {
if CLLocationManager.locationServicesEnabled(){
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest/// kCLLocationAccuracyBest is the default
self.checkLocationAuthorization()
}else{
// show message: Services desabled!
}
}
}
private func checkLocationAuthorization(){
switch locationManager.authorizationStatus{
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .restricted:
// show message
case .denied:
// show message
case .authorizedWhenInUse, .authorizedAlways:
/// app is authorized
locationManager.startUpdatingLocation()
default:
break
}
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
checkLocationAuthorization()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
hasFoundOnePlacemark = false
CLGeocoder().reverseGeocodeLocation(manager.location!, completionHandler: {(placemarks, error)-> Void in
if error != nil {
self.locationManager.stopUpdatingLocation()
// show error message
}
if placemarks!.count > 0 {
if !self.hasFoundOnePlacemark{
self.hasFoundOnePlacemark = true
let placemark = placemarks![0]
self.lastSearchedCity = placemark.locality ?? ""
}
self.locationManager.stopUpdatingLocation()
}else{
// no places found
}
})
}
}
SearchManagerViewModel Class
class SearchManagerViewModel: ObservableObject{
#Published var cityName = "" // use this directly in SwifUI views
#ObservedObject private var locationManager = LocationManager()
// Call this directly fron onAppear in SwiftUI views
// This method is more complex than what is shown here. It handles other things like HTTP requests etc.
func searchAndSetCity(){
locationManager.checkIfLocationServicesIsEnabled()
self.cityName = locationManager.lastSearchedCity
}
}
ContentView.swift
struct ContentView: View {
#StateObject private var searchManager = SearchManagerViewModel()
var body: some View {
VStack {
Text(searchManager.cityName)
.font(.callout)
}
.onAppear{
searchManager.searchAndSetCity()
}
}
}

Google Maps does not animate to user's location

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?

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

continuous center of user with Mapkit

Good afternoon,
I am having trouble displaying a map where it only centers around the user and will stay on the user with movement. My error is in my view file where I mark //HERE.
My error is Type '()' cannot conform to 'View'
Why is it that this line is giving me an error? If that line runs, my assumption would be that the map region is changing but it still would return the Map which conforms. View file is presented below.
If I run it without the commented line, it does not display my current location. I have my simulator settings are set to Features > Location > Apple. Even when I am zooming way out, nothing is marked on the map.
import SwiftUI
import MapKit
struct Location: Identifiable {
let id = UUID()
let name: String
let content: String?
let lat: Double
let lon: Double
var dangerLevel: CGFloat? = 10.0
var coord: CLLocationCoordinate2D { CLLocationCoordinate2D(latitude: lat, longitude: lon) }
}
struct ContentView: View {
#EnvironmentObject var locationManager: LocationManager
#State private var userTrackingMode: MapUserTrackingMode = .follow
#State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 40.7128, longitude: 74.0060), span: MKCoordinateSpan( latitudeDelta: 0.03, longitudeDelta: 0.03))
var body: some View {
var locationManager = LocationManager()
region = MKCoordinateRegion(center: locationManager.location!.coordinate, span: MKCoordinateSpan( latitudeDelta: 0.03, longitudeDelta: 0.03)) // HERE!!!!
VStack {
Map(coordinateRegion: $region,
interactionModes: .all,
showsUserLocation: true)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(LocationManager())
}
}
This is the next file where I define my locationManager
import Foundation
import CoreLocation
class LocationManager: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
#Published var location: CLLocation?
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
}
extension LocationManager : CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
self.location = location
}
}
There were a couple of issues with your code. First, to answer the question asked, you should refrain from putting variables that are NOT views directly into the var body. While there are ways of getting around this restriction, there is not good reason to any longer. Since region is not a view, the code through the error. And yes, I know you defined var locationManager and the ViewBuilder took it as it was the init of a variable, not the variable itself. However, you already ha a reference to locationManager that you defined in the header. Use that.
I put a few more changes into your code with comments to help things along. Let me know if you have further questions.
struct ContentView: View {
// Unless you are using locationManager all over your code, you don't need to pass it as an
// .environmentObject, though you can if needed. Since this is the first instance in this example
// of locationManager, I made it a #StateObject.
#StateObject var locationManager = LocationManager()
#State private var userTrackingMode: MapUserTrackingMode = .follow
#State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 37.60697453, longitude: -122.42798519), span: MKCoordinateSpan( latitudeDelta: 0.03, longitudeDelta: 0.03))
var body: some View {
VStack {
Map(coordinateRegion: $region,
interactionModes: .all,
showsUserLocation: true)
// Since locationManager is an ObservableObject, you can watch for changes with .onChange(of:)
.onChange(of: locationManager.location) { newLocation in
// Never force unwrap an optional unless you just set it yourself in the code.
guard let newLocation = newLocation else { return }
region = MKCoordinateRegion(center: newLocation.coordinate, span: MKCoordinateSpan( latitudeDelta: 0.03, longitudeDelta: 0.03)) // HERE!!!!
}
}
}
}
import CoreLocation
// I would consider renaming this class. It can be confusing to see
// locationManager.locationManager in code.
class LocationManager: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
#Published var location: CLLocation?
override init() {
super.init()
// You can generally drop .self, with some exceptions. The compiler will correct you.
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
}
extension LocationManager : CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
// This is an exception to dropping self when a variable in a closure has the same name as a
// self variable.
self.location = location
}
}