I've implemented three methods of GMSMapViewDelegate, of which two (markerInfoWindow, willMove) are getting called as expected while didTapMyLocationButtonForMapView is never triggered when I click the 'My Location' button.
Initialization:
class MapViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
GMSServices.provideAPIKey("KEY")
mapView = GMSMapView(frame: view.frame)
mapView.settings.myLocationButton = true
mapView.padding = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
mapView.isMyLocationEnabled = true
mapView.delegate = self
view.insertSubview(mapView, at: 0)
}
}
Delegate implementation:
extension MapViewController: GMSMapViewDelegate {
func mapView(_ mapView: GMSMapView, markerInfoWindow marker: GMSMarker) -> UIView? {
...
return infoView
}
func mapView(_ mapView: GMSMapView, willMove gesture: Bool) {
...
}
func didTapMyLocationButtonForMapView(mapView: GMSMapView!) -> Bool {
...
return false
}
}
I tried to:
Initialize in viewDidLoad.
Change the order of the methods in viewDidAppear (for example, setting the delegate after inserting the view).
Set the class in storyboard to be GMSMapView rather than adding in programmatically.
But nothing seems to help. Any ideas?
Missing _
func didTapMyLocationButtonForMapView(_ mapView: GMSMapView!) -> Bool {
...
return false
}
Swift 5
func didTapMyLocationButton(for mapView: GMSMapView) -> Bool {
return false //default move map to my location
}
Related
I have a map which loads annotations from the Google API, when the map initially loads all the annotations they are 'placed' as seen through the print in the console, however they won't show up on the map until I move the map once. Does anyone know if I need to call a method to update the map after placing the annotations?
struct ContentView: View {
var locationSearch = LocationSearch()
#State private var mapView = MapView()
#State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: -33.7944, longitude: 151.2649), span: MKCoordinateSpan(latitudeDelta: 0.015, longitudeDelta: 0.015))
#EnvironmentObject var sheetManager: SheetManager
var body: some View {
mapView
.popup(with: SheetManager())
.frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight)
}
}
struct MapView: UIViewRepresentable {
#State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: -33.7944, longitude: 151.2649), span: MKCoordinateSpan(latitudeDelta: 0.015, longitudeDelta: 0.015))
func updateUIView(_ uiView: MKMapView, context: Context) {
print("FLF: MapView updated")
uiView.setNeedsDisplay()
}
var locationManager = CLLocationManager()
let mapView = MKMapView(frame: CGRect(x: 0, y: 0, width: UIScreen.screenWidth, height: UIScreen.screenHeight))
func setupManager() {
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.requestAlwaysAuthorization()
}
func makeUIView(context: Context) -> MKMapView {
setupManager()
mapView.region = ContentView().region
mapView.showsUserLocation = true
mapView.userTrackingMode = .follow
mapView.delegate = context.coordinator // set the delegate to the coordinator
placeMarkersForRegion(region: region)
return mapView
}
func placeMarkersForRegion(region: MKCoordinateRegion) {
var locationSearch = LocationSearch()
locationSearch.performSearch(region: region) { venues in
print("FLF: Placing \(venues.count) marker(s)")
for marker in venues {
let annotation = MKPointAnnotation()
annotation.coordinate = marker.location
annotation.title = marker.name
mapView.addAnnotation(annotation)
}
}
}
func makeCoordinator() -> MapViewCoordinator {
MapViewCoordinator(self) // pass self to the coordinator so it can call `regionDidChangeAnimated`
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// Use the 'coordinate' property to get the current location of the map view
let currentRegion = mapView.region
print("FLF: Map has moved")
self.placeMarkersForRegion(region: currentRegion)
// Do something with the current region (e.g. update a state variable or perform a search)
}
}
class MapViewCoordinator: NSObject, MKMapViewDelegate {
var parent: MapView // add a property to hold a reference to the parent view
init(_ parent: MapView) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// Call the parent's implementation of this method
parent.mapView(mapView, regionDidChangeAnimated: animated)
}
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
view.canShowCallout = true
view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
// Get the tapped annotation
guard let annotation = view.annotation else { return }
// Print the title of the annotation
print(annotation.title ?? "Unknown")
}
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
print("FLF: Marker tapped")
}
}
The UIViewRepresentable and Coordinator aren't implemented correctly. E.g. makeUIView has to init it, but you are initing it as a property on the struct which is immediately lost. Also MapViewCoordinator(self) is a mistake because self, i.e. the struct, is immediately disgarded after SwiftUI has updated.
Another issue is the #State shouldn't hold a View like how your ContentView has a #State for the MapView.
Here is an example of how to use MKMapView with UIViewRepresentable:
struct MKMapViewRepresentable: UIViewRepresentable {
#Binding var userTrackingMode: MapUserTrackingMode
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> MKMapView {
context.coordinator.mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
// MKMapView has a strange design that the delegate is called when setting manually so we need to prevent an infinite loop
context.coordinator.userTrackingModeChanged = nil
uiView.userTrackingMode = userTrackingMode == .follow ? MKUserTrackingMode.follow : MKUserTrackingMode.none
context.coordinator.userTrackingModeChanged = { mode in
userTrackingMode = mode == .follow ? MapUserTrackingMode.follow : MapUserTrackingMode.none
}
}
class Coordinator: NSObject, MKMapViewDelegate {
lazy var mapView: MKMapView = {
let mv = MKMapView()
mv.delegate = self
return mv
}()
var userTrackingModeChanged: ((MKUserTrackingMode) -> Void)?
func mapView(_ mapView: MKMapView, didChange mode: MKUserTrackingMode, animated: Bool) {
userTrackingModeChanged?(mode)
}
}
}
I'm using Google Maps SDK to embed maps on my application. I have also enabled maps recenter icon using:
extension DashboardVC: GMSMapViewDelegate {
func mapView(_ mapView: GMSMapView, willMove gesture: Bool) {
if !change{
googleMapView.settings.myLocationButton = true
ColorLocationButton()
}
change = false
}
func didTapMyLocationButton(for mapView: GMSMapView) -> Bool {
googleMapView.settings.myLocationButton = false
change = true
return false
}
func mapView(_ mapView: GMSMapView, didTapMyLocation location: CLLocationCoordinate2D) {
googleMapView.settings.myLocationButton = false
}
}
How can I add light and dark mode to this button rendering from Google Maps itself?
First Add a View and Add the Image inside a View
Then Make The Outlets on Your Controllers
#IBOutlet weak var recenterView: UIView!
#IBOutlet weak var hamburgerImage: UIImageView!
if Your View On the Google Maps View Must add these Line of Code in Your viewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
googleMapView.settings.myLocationButton = false
self.googleMapView.bringSubviewToFront(self.recenterView)
recenterView.isUserInteractionEnabled = true
let tapOnRecenter = UITapGestureRecognizer(target: self, action: #selector(recenterTheMap(gesture:)))
tapOnRecenter.delegate = self
recenterView.addGestureRecognizer(tapOnRecenter)
// Setup The Recenter View For Corner Radius and Shadow
recenterView.clipsToBounds = true
recenterView.layer.cornerRadius = recenterView.frame.size.width / 2
recenterView.layer.shadowColor = UIColor(ciColor: .gray).cgColor
recenterView.layer.shadowRadius = 12
}
your Recenter Button Code
#objc func recenterTheMap(gesture: UITapGestureRecognizer){
googleMapView.camera = GMSCameraPosition.camera(withTarget: marker.position, zoom: 15)
}
Do not Forget the Add Gestures Delegate
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Detect Light or DarkMode Through This Delegate Method
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *) {
if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
if traitCollection.userInterfaceStyle == .light {
recenterView.tintColor = .white
}
else {
recenterView.tintColor = .black
}
}
} else {
// Fallback on earlier versions
}
}
Here is Your Code For Current Location
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
location: CLLocation = locations.last!
}
I am working on a MapView where on click of any custom annotation pin, I am showing custom callout view (load from xib file).
From this Custom Callout I have an UIButton, I already can detect click on this button but I want to access on the Map like the : view?.rightCalloutAccessoryView in the basic callout.
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView, {
if view.annotation!.isKind(of: MKUserLocation.self){
return
}
let customView = (Bundle.main.loadNibNamed("CustomCalloutView", owner: self, options: nil))?[0] as! CustomCalloutView;
let calloutViewFrame = customView.frame;
customView.frame = CGRect(x: -calloutViewFrame.size.width/2.23, y: -calloutViewFrame.size.height+10, width: 315, height: 170)
view.addSubview(customView)
let region = MKCoordinateRegion(center: pinToZoomOn!.coordinate, span: span)
mapView.setRegion(region, animated: true)
}
The route is correctly calculated from the classic callout but I can't know how to access my map from the button of my custom callout.
My CustomCalloutViewClass :
import UIKit
import MapKit
class CustomCalloutView: MKAnnotationView {
#IBOutlet weak var goButton: UIButton!
#IBAction func goButton(_ sender: AnyObject) {
print("Button clicked sucessfully")
}
// MARK: - Detect taps on callout
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if hitView != nil {
superview?.bringSubview(toFront: self)
}
return hitView
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let rect = self.bounds
var isInside = rect.contains(point)
if !isInside {
for view in subviews {
isInside = view.frame.contains(point)
if isInside {
break
}
}
}
return isInside
}
}
If someone have an idea it will be helpfull I'm stuck on this issue.
Thank you in advance.
Option 1: Capture a MKMapView instance in the closure passed to a CustomCalloutView
Add the closure which will be called on the button action. The closure will capture the MKMapView instance and you will be able to us is inside.
class CustomCalloutView: MKAnnotationView {
var didTapGoButton: (() -> Void)?
#IBAction func goButton(_ sender: AnyObject) {
didTapGoButton?()
}
}
Option 2: Add a weak reference to MKMapView as a property of the callout
This is not a clean solution but it may be an option under some circumstances. You only have to create a weak property storing a reference to MKMapView instance in CustomCalloutView
class CustomCalloutView: MKAnnotationView {
weak var mapView: MKMapView?
}
Configuration
This is how you can wire up the CustomCalloutView for both solutions. Remember to use swift capture list to capture a weak reference to a MKMapView instance. Without it you may create a strong reference cycle.
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
// ...
let customView = (Bundle.main.loadNibNamed("CustomCalloutView", owner: self, options: nil))?[0] as! CustomCalloutView;
// Option 1
customView.didTapGoButton = { [weak mapView ] in
print(mapView?.annotations.count)
}
// Option 2
customView.mapView = mapView
// ...
}
Thank you so much for your help !
I used the first option to capture the MKMapView instance.
If someone is stuck on the same issue you can use the first option and add this in your didSelect MapView function:
let selectedLoc = view.annotation
print("Annotation '\(String(describing: selectedLoc?.title!))' has been selected")
let location = view.annotation as! YourCustomClassType
let launchOptions = [MKLaunchOptionsDirectionsModeKey : MKLaunchOptionsDirectionsModeDriving]
customView.didTapGoButton = { location.mapItem().openInMaps(launchOptions: launchOptions) }
Thank's
var didTapGoButton: (() -> Void)? this types of closures are very helpful for navigation from the view to viewControllers through the button actions in the call layoutviews in the mkmapview.
I know it is possible to capture the taps in the infowindow of a marker. I followed the documentation here.
All are written in Objective C so I tried converting it to Swift, here is my code:
func mapView(_ mapView: GMSMapView, didTap InfoWindowOfMarker: GMSMarker) {
print("You tapped infowindow")
}
But this isn't getting fired at all. What is wrong with the method?
You need to use the delegate of GMSMapView along with some prior setting see below.
Declare the use of GMSMapViewDelegate methods and set the delegate to self:
class yourClassName: UIViewController,GMSMapViewDelegate
mapView?.delegate = self
Method to detect tap on infoWindow:
func mapView(_ mapView: GMSMapView, didTapInfoWindowOf marker: GMSMarker) {
print("infowindow tapped")
}
Method to detect tap on GMSMarker:
func mapView(mapView: GMSMapView, didTapMarker marker: GMSMarker) -> Bool {
print("tapped on marker")
if marker.title == "myMarker"{
print("handle specific marker")
}
return true
}
Method to create custom infoWindow:
func mapView(mapView: GMSMapView!, markerInfoWindow marker: GMSMarker!) -> UIView! {
let infoWindow = Bundle.main.loadNibNamed("nibName", owner: self, options: nil).first as! ClassName
infoWindow.name.text = "title"
infoWindow.address.text = "relevant address"
infoWindow.photo.image = UIImage(named: "imageName")
return infoWindow
}
I want to show a VC as a pop up when the user taps on one of the markers on the Google Map.
The reason that I want to do this is because I want to control the view that pops up when the marker is tapped. I tried using the mapView(mapView: GMSMapView, markerInfoWindow marker: GMSMarker) delegate method. But I don't know how to create a view controller in that method that controls the marker info window's view.
To present a VC as a pop over, I did this:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("MarkerInfoController")
vc.modalInPopover = true
vc.modalPresentationStyle = .Popover
print(marker.iconView)
vc.popoverPresentationController!.sourceView = marker.iconView
self.presentVC(vc) // this is from EZSwiftExtensions. Don't worry about it
The problem arises when I try to set the sourceView of the UIPopoverPresentationController. I thought using the iconView property would work, but no. There is always an error saying that sourceView is not set.
How can I get the UIView instance for the marker, so that I can assign it to sourceView?
P.S. This is how a marker is created:
func mapView(mapView: GMSMapView, didLongPressAtCoordinate coordinate: CLLocationCoordinate2D) {
let marker = GMSMarker(position: coordinate)
marker.map = mapView
}
Output:
Code:
import UIKit
import GoogleMaps
class MapViewController: UIViewController {
#IBOutlet weak var mapView: GMSMapView!
var sourceView: UIView?
}
extension MapViewController: GMSMapViewDelegate {
func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool {
mapCenterPinImage.fadeOut(0.25)
// Adding a delay becuase when click on marker Camera changes it position
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
let location = marker.accessibilityActivationPoint
self.sourceView = UIView(frame: CGRect(x: location.x, y: location.y, width: 1, height: 1))
self.view.addSubview(self.sourceView!)
let popController = MyPopUpViewController()
popController.modalPresentationStyle = UIModalPresentationStyle.popover
popController.preferredContentSize = CGSize(width: 200, height: 200)
popController.popoverPresentationController?.delegate = self
popController.popoverPresentationController?.sourceView = self.sourceView
self.present(popController, animated: true)
}
return false
}
}
extension MapViewController: UIPopoverPresentationControllerDelegate{
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
func popoverPresentationControllerShouldDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) -> Bool {
sourceView?.removeFromSuperview()
sourceView = nil
return true
}
}
What I've done basically created a UIView and added it to ViewController at runtime and set this as a source of Popup and make it nil when it's dismissed.
marker.accessibilityActivationPoint is the source of X and Y according to device's screen
You can just customise the infoView of the GMS Marker instead of showing a viewControlle and perform your desired operations there. You can have a look at this article over here. I faced something similar to your issue and this helped me out. Hope it helps you out as well. :D
https://medium.com/#matschmidy/how-to-implement-custom-and-dynamic-map-marker-info-windows-for-google-maps-ios-e9d993ef46d4
You can do this on below is a gmsMapView delegate mehtod
func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool
here you will get the marker which you tapped , and for source view you can access
marker.iconView
which I think will solve your problem, hope it helps :)