I want to allow users to zoom in/out and move freely around the map while the app is tracking and displaying their current location. Later i will try to add a button that you can press and it will move you back to the center of user location. I think the problem is that in my LocationManager file i am creating new region everytime i get a new location coordinates. But i do not know any other ways of tracking user location and displaying it.
LocationManager:
import Foundation
import CoreLocation
import MapKit
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate{
let locationManager = CLLocationManager()
#Published var location: CLLocationCoordinate2D?
#Published var region = MKCoordinateRegion()
override init(){
super.init()
locationManager.delegate = self
}
func startTracking() {
locationManager.requestAlwaysAuthorization()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.allowsBackgroundLocationUpdates = true
locationManager.startUpdatingLocation()
}
func stopTracking() {
print("stop test")
locationManager.stopUpdatingLocation()
locationManager.allowsBackgroundLocationUpdates = false
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let tempLocation = locations.last?.coordinate
print(tempLocation)
let center = CLLocationCoordinate2D(latitude: tempLocation?.latitude ?? 0.0, longitude: tempLocation?.longitude ?? 0.0)
let span = MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)
let tempRegion = MKCoordinateRegion(center: center, span: span)
DispatchQueue.main.async {
self.location = tempLocation
self.region = tempRegion
}
}
}
ContentView:
import SwiftUI
import MapKit
struct ContentView: View {
#State var isButtonPressed: Bool = false
#StateObject var locationManager = LocationManager()
var body: some View {
VStack{
Button("START"){
isButtonPressed = true
locationManager.startTracking()
}
Button("STOP"){
isButtonPressed = false
locationManager.stopTracking()
}
mapView(isButtonPressed: isButtonPressed, manager: locationManager)
}
}
}
struct mapView: View{
var isButtonPressed: Bool
#ObservedObject var manager: LocationManager
#State var defaultRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 0, longitude: 0), latitudinalMeters: 1000, longitudinalMeters: 1000)
var body: some View{
if isButtonPressed == true{
if let location = manager.location {
Map(coordinateRegion: $manager.region, interactionModes: .all, showsUserLocation: true, userTrackingMode:nil)
} else{
Map(coordinateRegion: $defaultRegion)
}
}
else
{
Map(coordinateRegion: $defaultRegion)
}
}
}
Thanks!
Creating a series of regions is the wrong way to monitor the user's location.
It's been quite a while since I've written a mapping app, but here's the general idea of what you should do:
Create an instance of the location manager.
Set yourself as the location manager's delegate.
Ask the user for permission to get location updates while your app is running (if you don't have permission already.)
Call startUpdatingLocation() to request location updates
Respond to calls to the location manager delegate's locationManager(_:didUpdateLocations:) method as needed. (If you set up the map to show the user's location you won't have to do anything special - the blue dot will update automatically.)
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?
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)
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
}
}
I'm trying to make an app that uses Google Maps and focuses on the user's location when the app is opened.
Right now I have the map initializing and im able to focus on the users location after pressing the 'myLocation' button that is inherent to GoogleMaps, BUT the map's camera keeps focusing to a specified location and not the users location.
I used these 2 tutorials to get to where I'm at now:
- https://developers.google.com/maps/documentation/ios-sdk/start
- https://www.raywenderlich.com/197-google-maps-ios-sdk-tutorial-getting-started
After searching Google and here, it seems like I need to utilize CLLocationManager() to get the user's device coordinates and then use that somehow? Im think my code regarding CLLocationManager() may be placed in the wrong file or is used incorrectly, but im not getting any errors.
My code works like this: SceneDelegate.swift sets my LandmarkList.swift as the rootViewController. Then the LandmarkList calls GoogMapView.swift to display the instance of Google maps.
SceneDelegate.swift:
I think my usage of locationManager here may be wrong?
import UIKit
import SwiftUI
import GoogleMaps
import GooglePlaces
import CoreLocation
class SceneDelegate: UIResponder, UIWindowSceneDelegate, CLLocationManagerDelegate {
var window: UIWindow?
private let locationManager = CLLocationManager()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Use a UIHostingController as window root view controller
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: LandmarkList())
self.window = window
window.makeKeyAndVisible()
}
locationManager.requestAlwaysAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
}
}
}
LandmarkList.swift:
import SwiftUI
struct LandmarkList: View {
#State private var searchText = ""
#State private var locationText = ""
var body: some View {
ZStack(alignment: Alignment.top) {
GoogMapView()
.frame(height: 750)
SlideOverCard {
VStack(alignment: .leading) {
List(landmarkData) { landmark in
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: Alignment.topLeading)
}
}
}
}
GoogMapView.swift:
Note: The print statement below only returns 'User's location is unknown'
import SwiftUI
import UIKit
import GoogleMaps
import GooglePlaces
import CoreLocation
struct GoogMapView : UIViewRepresentable {
let marker : GMSMarker = GMSMarker()
//Creates a `UIView` instance to be presented.
func makeUIView(context: Context) -> GMSMapView {
// Create a GMSCameraPosition
let camera = GMSCameraPosition.camera(withLatitude: 42.361145, longitude: -71.057083, zoom: 16.0)
let mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
mapView.setMinZoom(14, maxZoom: 20)
mapView.settings.compassButton = true
mapView.isMyLocationEnabled = true
mapView.settings.myLocationButton = true
mapView.settings.scrollGestures = true
mapView.settings.zoomGestures = true
mapView.settings.rotateGestures = true
mapView.settings.tiltGestures = true
mapView.isIndoorEnabled = false
if let mylocation = mapView.myLocation {
print("User's location: \(mylocation)")
} else {
print("User's location is unknown")
}
return mapView
}
// Updates the presented `UIView` (and coordinator) to the latestconfiguration.
func updateUIView(_ mapView: GMSMapView, context: Context) {
// Creates a marker in the center of the map.
marker.position = CLLocationCoordinate2D(latitude: 42.361145, longitude: -71.057083)
marker.title = "Boston"
marker.snippet = "USA"
marker.map = mapView
}
}
Again, I think my code regarding locationManager in SceneDelegate.swift would make the instance of GoogleMaps camera focus on the users location at startup, but it doesnt.
Anyone know what im doing wrong?
I was finally able to figure out how to focus on "my location" immediately, instead of requiring input from the user. Here are some more details...hope it helps!
updateUIView
You want updateUIView to be called when the user's location is retrieved. From what I have read updateUIView is called when a #State or #Binding object changes (or #ObservedObject). So, the UIViewRepresentable needs to have one of these. In the case of my code below, I use an #ObservedObject. Whenever one of the #ObservedObject's #Published properties changes, updateUIView will be called.
Creating The Observed Object
I created a class (LocationManager) that conforms to the ObservableObject protocol.
Within the class, I exposed the lastKnownLocation as a #Published property. As mentioned above, when the lastKnownLocation is updated, any subscribers to an instance of the LocationManager class will see those updates
Getting The User's Coordinates
CLLocationManager is used to get updates on the user's location.
The LocationManager class I created implements the delegate functions for CLLocationManager
The delegate function didUpdateLocations is called when the user's location is updated. Within that function, I then update the #Published property so that the UIViewRepresentable sees those changes, and updateUIView is automatically called accordingly.
The UIViewRepresentable
import SwiftUI
import GoogleMaps
import Combine
struct HomeView: UIViewRepresentable {
// Listen to changes on the locationManager
#ObservedObject var locationManager = LocationManager()
func makeUIView(context: Self.Context) -> GMSMapView {
// Just default the camera to anywhere (this will be overwritten as soon as myLocation is grabbed
let camera = GMSCameraPosition.camera(withLatitude: 0, longitude: 0, zoom: 16.0)
let mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
mapView.setMinZoom(14, maxZoom: 20)
mapView.settings.compassButton = true
mapView.isMyLocationEnabled = true
mapView.settings.myLocationButton = true
mapView.settings.scrollGestures = true
mapView.settings.zoomGestures = true
mapView.settings.rotateGestures = true
mapView.settings.tiltGestures = true
mapView.isIndoorEnabled = false
return mapView
}
func updateUIView(_ mapView: GMSMapView, context: Self.Context) {
// When the locationManager publishes updates, respond to them
if let myLocation = locationManager.lastKnownLocation {
mapView.animate(toLocation: myLocation.coordinate)
print("User's location: \(myLocation)")
}
}
}
The LocationManager class:
class LocationManager: NSObject, CLLocationManagerDelegate, ObservableObject {
// Publish the user's location so subscribers can react to updates
#Published var lastKnownLocation: CLLocation? = nil
private let manager = CLLocationManager()
override init() {
super.init()
self.manager.delegate = self
self.manager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
self.manager.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// Notify listeners that the user has a new location
self.lastKnownLocation = locations.last
}
}
Using comments here and others on StackOverflow, I made this solution:
//
// GoogMapView.swift
// Landmarks
//
// Created by Zahr Lyttle on 10/14/19.
// Copyright © 2019 Apple. All rights reserved.
//
import SwiftUI
import UIKit
import GoogleMaps
import GooglePlaces
import CoreLocation
import Foundation
struct GoogMapView: View {
var body: some View {
GoogMapControllerRepresentable()
}
}
class GoogMapController: UIViewController, CLLocationManagerDelegate {
var locationManager = CLLocationManager()
var mapView: GMSMapView!
let defaultLocation = CLLocation(latitude: 42.361145, longitude: -71.057083)
var zoomLevel: Float = 15.0
let marker : GMSMarker = GMSMarker()
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
locationManager.distanceFilter = 50
locationManager.startUpdatingLocation()
locationManager.delegate = self
let camera = GMSCameraPosition.camera(withLatitude: defaultLocation.coordinate.latitude, longitude: defaultLocation.coordinate.longitude, zoom: zoomLevel)
mapView = GMSMapView.map(withFrame: view.bounds, camera: camera)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.isMyLocationEnabled = true
mapView.setMinZoom(14, maxZoom: 20)
mapView.settings.compassButton = true
mapView.isMyLocationEnabled = true
mapView.settings.myLocationButton = true
mapView.settings.scrollGestures = true
mapView.settings.zoomGestures = true
mapView.settings.rotateGestures = true
mapView.settings.tiltGestures = true
mapView.isIndoorEnabled = false
// if let mylocation = mapView.myLocation {
// print("User's location: \(mylocation)")
// } else {
// print("User's location is unknown")
// }
marker.position = CLLocationCoordinate2D(latitude: 42.361145, longitude: -71.057083)
marker.title = "Boston"
marker.snippet = "USA"
marker.map = mapView
// Add the map to the view, hide it until we've got a location update.
view.addSubview(mapView)
// mapView.isHidden = true
}
// Handle incoming location events.
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location: CLLocation = locations.last!
print("Location: \(location)")
let camera = GMSCameraPosition.camera(withLatitude: location.coordinate.latitude, longitude: location.coordinate.longitude, zoom: zoomLevel)
if mapView.isHidden {
mapView.isHidden = false
mapView.camera = camera
} else {
mapView.animate(to: camera)
}
}
// Handle authorization for the location manager.
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .restricted:
print("Location access was restricted.")
case .denied:
print("User denied access to location.")
// Display the map using the default location.
mapView.isHidden = false
case .notDetermined:
print("Location status not determined.")
case .authorizedAlways: fallthrough
case .authorizedWhenInUse:
print("Location status is OK.")
}
}
// Handle location manager errors.
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
locationManager.stopUpdatingLocation()
print("Error: \(error)")
}
}
struct GoogMapControllerRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<GMControllerRepresentable>) -> GMController {
return GMController()
}
func updateUIViewController(_ uiViewController: GMController, context: UIViewControllerRepresentableContext<GMControllerRepresentable>) {
}
}
You can implement this by "pretending" your View is a ViewController. Try setting GoogleMapView as the delegate, instead. Then put your initialization code in makeUIView and then conform to the protocol:
struct GoogMapView : CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
guard status == .authorizedWhenInUse else {
return
}
locationManager.startUpdatingLocation()
mapView.isMyLocationEnabled = true
mapView.settings.myLocationButton = true
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else {
return
}
mapView.camera = GMSCameraPosition(target: location.coordinate, zoom: 15, bearing: 0, viewingAngle: 0)
locationManager.stopUpdatingLocation()
}
}
Above taken from https://www.raywenderlich.com/197-google-maps-ios-sdk-tutorial-getting-started.
I am trying to load a map and have the initial location on user's location as well as showing the user's location on the map using SwiftUI. I don't know how to do that in SwiftUI.
I have tried to put 'view.showsUserLocation = true' in updateUIView function but it is not working.
import SwiftUI
import MapKit
struct MapView : UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ view: MKMapView, context: Context) {
view.showsUserLocation = true
let coordinate = CLLocationCoordinate2D(
latitude: 34.011286, longitude: -116.166868)
let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0)
let region = MKCoordinateRegion(center: view.userLocation.location?.coordinate ?? coordinate, span: span)
view.setRegion(region, animated: true)
}
}
The location comes nil when I tried to print it.
Add key Privacy - Location When In Use Usage Description and description in Info.plist
Then try with updating below function:
func updateUIView(_ view: MKMapView, context: Context) {
view.showsUserLocation = true
// Ask for Authorisation from the User.
self.locationManager.requestAlwaysAuthorization()
// For use in foreground
self.locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
// self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
self.locationManager.startUpdatingLocation()
//Temporary fix: App crashes as it may execute before getting users current location
//Try to run on device without DispatchQueue
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
let locValue:CLLocationCoordinate2D = self.locationManager.location!.coordinate
print("CURRENT LOCATION = \(locValue.latitude) \(locValue.longitude)")
let coordinate = CLLocationCoordinate2D(
latitude: locValue.latitude, longitude: locValue.longitude)
let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0)
let region = MKCoordinateRegion(center: coordinate, span: span)
view.setRegion(region, animated: true)
})
}
}
This is the cleanest way I have found to setup CoreLocation and MapView. First, you have to create an UIViewRepresentable to use CoreLocation in SwiftUI.
Do not forget to enable in your Info.plist file this Privacy with a message:
Privacy - Location Always and When In Use Usage Description
Privacy - Location When In Use Usage Description
Privacy - Location Always Usage Description
import SwiftUI
import MapKit
// MARK: Struct that handle the map
struct MapView: UIViewRepresentable {
#Binding var locationManager: CLLocationManager
#Binding var showMapAlert: Bool
let map = MKMapView()
///Creating map view at startup
func makeUIView(context: Context) -> MKMapView {
locationManager.delegate = context.coordinator
return map
}
func updateUIView(_ view: MKMapView, context: Context) {
map.showsUserLocation = true
map.userTrackingMode = .follow
}
///Use class Coordinator method
func makeCoordinator() -> MapView.Coordinator {
return Coordinator(mapView: self)
}
//MARK: - Core Location manager delegate
class Coordinator: NSObject, CLLocationManagerDelegate {
var mapView: MapView
init(mapView: MapView) {
self.mapView = mapView
}
///Switch between user location status
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .restricted:
break
case .denied:
mapView.showMapAlert.toggle()
return
case .notDetermined:
mapView.locationManager.requestWhenInUseAuthorization()
return
case .authorizedWhenInUse:
return
case .authorizedAlways:
mapView.locationManager.allowsBackgroundLocationUpdates = true
mapView.locationManager.pausesLocationUpdatesAutomatically = false
return
#unknown default:
break
}
mapView.locationManager.startUpdatingLocation()
}
}
}
}
This is the way I use it in my SwiftUI view. An alert is toggle in case the permission is denied in the Coordinator class switch:
import SwiftUI
import CoreLocation
// MARK: View that shows map to users
struct HomeView: View {
#State var locationManager = CLLocationManager()
#State var showMapAlert = false
var body: some View {
MapView(locationManager: $locationManager, showMapAlert: $showMapAlert)
.alert(isPresented: $showMapAlert) {
Alert(title: Text("Location access denied"),
message: Text("Your location is needed"),
primaryButton: .cancel(),
secondaryButton: .default(Text("Settings"),
action: { self.goToDeviceSettings() }))
}
}
}
extension HomeView {
///Path to device settings if location is disabled
func goToDeviceSettings() {
guard let url = URL.init(string: UIApplication.openSettingsURLString) else { return }
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
The above codes will cause the app crash because of the inappropriate permission request. Use the below code for better performance and on-time permission.
struct MapView: UIViewRepresentable {
let locationManager = CLLocationManager()
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ view: MKMapView, context: Context) {
view.showsUserLocation = true
let status = CLLocationManager.authorizationStatus()
locationManager.requestAlwaysAuthorization()
locationManager.requestWhenInUseAuthorization()
if status == .authorizedAlways || status == .authorizedWhenInUse {
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
let location: CLLocationCoordinate2D = locationManager.location!.coordinate
let span = MKCoordinateSpan(latitudeDelta: 0.009, longitudeDelta: 0.009)
let region = MKCoordinateRegion(center: location, span: span)
view.setRegion(region, animated: true)
}
}
}
try this one:
struct OriginMap : UIViewRepresentable {
func makeUIView(context: UIViewRepresentableContext<OriginMap>) -> MKMapView{
MKMapView()
}
func updateUIView(_ mapView: MKMapView, context: Context) {
let locationManager = CLLocationManager()
mapView.showsUserLocation = true
locationManager.requestAlwaysAuthorization()
locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
let locValue:CLLocationCoordinate2D = locationManager.location!.coordinate
let coordinate = CLLocationCoordinate2D(
latitude: locValue.latitude, longitude: locValue.longitude)
Datas.currentLocation = coordinate
let span = MKCoordinateSpan(latitudeDelta: 0.002, longitudeDelta: 0.002)
let region = MKCoordinateRegion(center: coordinate, span: span)
mapView.setRegion(region, animated: true)
})
}}
Here is the code for SwiftUI2
import MapKit
import SwiftUI
struct MapView: View {
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 51.4353, longitude: 35.23442), span: MKCoordinateSpan(latitudeDelta: 0.15, longitudeDelta: 0.15))
var body: some View {
Map(coordinateRegion: $region)
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}
you can also check this URL for more info about other useful features added to SwiftUI2.
import SwiftUI
import MapKit
struct ContentView: View {
#State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: -23.506403, longitude: -46.1509555),
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
var body: some View {
Map(coordinateRegion: $region, showsUserLocation: true)
}
}
Available for XCode 12.
import SwiftUI
import MapKit
struct ContentView: View {
#State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 37.321127, longitude: -122.047790),
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
var body: some View {
Map(coordinateRegion: $region)
}
}
Using this you can find users region
Text(Locale.current.localizedString(forRegionCode: Locale.current.regionCode!) ?? "")
.font(.system(size: 30))
.fontWeight(.bold)