How to focus Google Maps camera on user's current location at startup using swiftUI views

Viewed 4937

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?

3 Answers

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.

Related