Search code examples
iosgoogle-mapsswiftuigoogle-maps-sdk-iosgmsmapview

How to make form sheet appear after Google Maps marker infoWindow is tapped in iOS?


I'm currently building an app using SwiftUI and Google Maps. I'm trying to get a form sheet to appear after a Google Maps marker's infoWindow is tapped, but I'm having trouble getting it working.

In other parts of my app I display sheets using this method: Example method here

I tried using the same method above to display a sheet after a marker's infoWindow is tapped, but am having trouble doing it from within a function. My code snippets below give more detail.

-

Below is a stripped down version of my GMView.swift file which controls my instance of Google Maps. (My file looks different from typical Swift + Google maps integration because im using SwiftUI). You'll notice 3 main parts of the file: 1. the view, 2. the GMController class, and 3. the GMControllerRepresentable struct:

import SwiftUI
import UIKit
import GoogleMaps
import GooglePlaces
import CoreLocation
import Foundation



struct GoogMapView: View {
    var body: some View {
        GoogMapControllerRepresentable()
    }
}


class GoogMapController: UIViewController, CLLocationManagerDelegate, GMSMapViewDelegate {
    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()

//        Control location data
        locationManager.requestAlwaysAuthorization()
        locationManager.requestWhenInUseAuthorization()
        if CLLocationManager.locationServicesEnabled() {
            locationManager.delegate = self
            locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
            locationManager.distanceFilter = 50
            locationManager.startUpdatingLocation()
        }


        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


        marker.position = CLLocationCoordinate2D(latitude: 42.361145, longitude: -71.057083)
        marker.title = "Boston"
        marker.snippet = "USA"
        marker.map = mapView


//        view.addSubview(mapView)
        mapView.delegate = self
        self.view = mapView

    }

    // Handle incoming location events.
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
      let location: CLLocation = locations.last!
      print("Location: \(location)")
    }

    // 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>) {

    }
}

This is the function I've added to the GMController class in my GMView.swift file above that Google's documentation says to use to handle when a marker's infoWindow is tapped:

// Function to handle when a marker's infowindow is tapped
    func mapView(_ mapView: GMSMapView, didTapInfoWindowOf didTapInfoWindowOfMarker: GMSMarker) {
        print("You tapped a marker's infowindow!")
//        This is where i need to get the view to appear as a modal, and my attempt below
        let venueD2 = UIHostingController(rootView: VenueDetail2())
        venueD2.view.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height - 48)
        self.view.addSubview(venueD2.view)
        return
    }

My function above currently displays a view when the infowindow is tapped, but it just appears over my google maps view, so I dont get the animation nor am I able to dismiss the view like a typical iOS form sheet.

Does anyone know how I can display a sheet after a Google maps marker infowindow is tapped in SwiftUI instead of just adding it as a subview?


Solution

  • Hi there to interact with UIViewController from a struct View you need to bind a variable.. fist we declare @Binding var isClicked : Bool and If you need to pass more parameter to the struct you need to declare it with announcement @Binding. any way in UIViewController an error will show that isClicked Property 'self.isClicked' not initialized to fix this we declare:

    @Binding var isClicked
    init(isClicked: Binding<Bool>) {
            _isClicked = isClicked
            super.init(nibName: nil, bundle: nil) 
        }
    

    also The designated initialiser for UIViewController is initWithNibName:bundle:. You should be calling that instead. If you don't have a nib, pass in nil for the nibName (bundle is optional too). now we have all setup for the UIViewController and we move to UIViewControllerRepresentable: the same what we did at first we need to declare the @Binding var isClicked because the viewController will request a new parameter at initializations so we will have something like this:

    @Binding var isClicked: Bool
    func makeUIViewController(context: UIViewControllerRepresentableContext<GMControllerRepresentable>) -> GMController {
            return GMController(isClicked: $isClicked)
        }
    

    in the struct View:

    @State var isClicked: Bool = false
    var body: some View {
            GoogMapControllerRepresentable(isClicked: $isClicked)
    .sheet(isPresented: $isShown) { () -> View in
                <#code#>
            }
        }
    

    and one more thing we just need to toggle this variable on marker click like this:

    func mapView(_ mapView: GMSMapView, didTapInfoWindowOf didTapInfoWindowOfMarker: GMSMarker) {
            print("You tapped a marker's infowindow!")
    //        This is where i need to get the view to appear as a modal, and my attempt below
           self.isClicked.toggle()
    // if you want to pass more parameters you can set them from here like self.info = //mapView.coordinate <- Example
            return
        }