1

I'm using the iOS Google Maps SDK to show maps in my app.

On my maps, I also show some map markers.

This has been working great, however I want the map to ignore the safe areas of the device so that it stretches to the full height (behind any notches and bottom tab bars).

The problem I'm running into is that when I apply .ignoresSafeArea() to the map, all of my map markers get cut off, like so:

enter image description here

This only happens when I add .ignoresSafeArea(). Without it, the markers show normally.

I'm not sure what I'm doing wrong.

Here is my SwiftUI code that shows the map:

import GoogleMaps

struct MapView: View {
    
    @EnvironmentObject var apiManager: ApiManager
    
    private var mapOptions: GMSMapViewOptions {
        var options = GMSMapViewOptions()
        return options
    }
    
    var body: some View {
        ZStack {
            GoogleMapView(options: mapOptions)
                .mapMarkers(MapHelper.convertToGoogleMarkers(markers: apiManager.markers))
                .ignoresSafeArea()
        }
        .task {
            getMapMarkers()
        }
        .navigationBarHidden(true)
    }
    
    func getMapMarkers() {
        Task {
            await apiManager.fetchAllMapMarkers()
        }
    }
}

And here is the UIViewRepresentable for GoogleMapView:

import SwiftUI
import GoogleMaps

/// A SwiftUI wrapper for GMSMapView that displays a map with optional markers and configurable map type
struct GoogleMapView: UIViewRepresentable {
    
   // Configuration properties - set at initialization
   private let options: GMSMapViewOptions
   
   /// Array of markers to display on the map
   private var markers: [GMSMarker]
   
   /// Type of map to display (normal, satellite, hybrid, terrain)
   private let mapType: GMSMapViewType
   
   // Runtime updatable properties
   private var camera: GMSCameraPosition?
   private var backgroundColor: UIColor?
   
   /// Shared delegate instance to handle map interactions across all instances
   /// Using static ensures callbacks work together when chaining modifiers
   private static let mapDelegate = GoogleMapViewDelegate()
   
   init(options: GMSMapViewOptions,
        markers: [GMSMarker] = [],
        mapType: GMSMapViewType = .normal) {
       self.options = options
       self.markers = markers
       self.mapType = mapType
   }
   
   /// Creates the underlying UIKit map view
   func makeUIView(context: Context) -> GMSMapView {
       // Initialize map with current options
       let mapView = GMSMapView(options: options)
       mapView.overrideUserInterfaceStyle = .unspecified
       mapView.mapType = mapType
       
       // Set shared delegate to handle interactions
       mapView.delegate = Self.mapDelegate
             
       return mapView
   }
   
   /// Updates the map view when SwiftUI state changes
   func updateUIView(_ uiView: GMSMapView, context: Context) {
       // Update runtime properties if set
       if let camera = camera {
           uiView.camera = camera
       }
       
       if let backgroundColor = backgroundColor {
           uiView.backgroundColor = backgroundColor
       }
       
       //clears all markers and polylines
       uiView.clear()
       
       // Refresh markers on the map
       markers.forEach { marker in
           marker.map = uiView
       }
       
       uiView.mapType = mapType // Update map type if changed
   }
}

class GoogleMapViewDelegate: NSObject, GMSMapViewDelegate {
    
   var tapHandler: ((CLLocationCoordinate2D) -> Void)?
   var markerTapHandler: ((GMSMarker) -> Bool)?
   
   /// Called by GMSMapView when user taps the map at a specific coordinate
   /// - Parameters:
   ///   - mapView: The GMSMapView that detected the tap
   ///   - coordinate: The geographic coordinate where the tap occurred
   func mapView(_ mapView: GMSMapView, didTapAt coordinate: CLLocationCoordinate2D) {
       tapHandler?(coordinate) // Forward tap to handler if one is set
   }
   
   /// Called by GMSMapView when user taps a marker on the map
   /// - Parameters:
   ///   - mapView: The GMSMapView that detected the tap
   ///   - marker: The GMSMarker that was tapped
   /// - Returns: true if tap was handled by the app, false to allow default marker behavior
   func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool {
       return markerTapHandler?(marker) ?? false // Forward to handler or use default behavior
   }
    
}

// MARK: - viewModifiers and Markers

extension GoogleMapView {
    /// Updates the camera position of the map view during runtime
    /// - Parameter position: New camera position to apply
    /// - Returns: Updated GoogleMapView instance
    func camera(_ position: GMSCameraPosition?) -> GoogleMapView {
        var view = self
        if let position = position {
            view.camera = position
        }
        return view
    }
    
    /// Updates the background color of the map view during runtime
    /// - Parameter color: New background color to apply
    /// - Returns: Updated GoogleMapView instance
    func backgroundColor(_ color: UIColor) -> GoogleMapView {
        var view = self
        view.backgroundColor = color
        return view
    }
    
    /// Changes the map display type
    /// - Parameter type: GMSMapViewType to use (.normal, .satellite, etc)
    /// - Returns: New GoogleMapView instance with updated map type
    func mapType(_ type: GMSMapViewType) -> GoogleMapView {
        GoogleMapView(options: options, markers: markers, mapType: type)
    }
    
    /// Adds markers to the map
    /// - Parameter markers: Array of GMSMarker objects to display
    /// - Returns: New GoogleMapView instance with updated markers
    func mapMarkers(_ markers: [GMSMarker]) -> GoogleMapView {
        var view = self
        view.markers = markers
        return view
    }

}

// MARK: - View Callbacks

extension GoogleMapView {
   /// Adds handler for map tap events
   /// - Parameter handler: Closure called when map is tapped, providing tap coordinates
   /// - Returns: Same GoogleMapView instance with updated tap handler
   func onMapTapped(_ handler: @escaping (CLLocationCoordinate2D) -> Void) -> GoogleMapView {
       Self.mapDelegate.tapHandler = handler
       return self
   }
   
   /// Adds handler for marker tap events
   /// - Parameter handler: Closure called when marker is tapped
   /// - Returns: Same GoogleMapView instance with updated marker handler
   /// Return true from handler to indicate tap was handled
   func onMarkerTapped(_ handler: @escaping (GMSMarker) -> Bool) -> GoogleMapView {
       Self.mapDelegate.markerTapHandler = handler
       return self
   }
}

extension View {
   /// Configures the view to ignore safe areas except for the top
   /// Useful for map views that should fill the screen below status bar
   /// - Returns: Modified view that extends to screen edges except top
   func ignoresSafeAreaExceptTop() -> some View {
       ignoresSafeArea(.container, edges: [.bottom, .horizontal])
   }
}

Lastly, here is my convertToGoogleMarkers() function:

static func convertToGoogleMarkers(markers: Array<MapMarker>, showOrder: Bool = false) -> Array<GMSMarker> {
    return markers.enumerated().map { (index, mapMarker) in
        let marker = GMSMarker(position: CLLocationCoordinate2D(
            latitude: Double(mapMarker.latitude)!,
            longitude: Double(mapMarker.longitude)!
        ))
        
        // Set the Place as the userData
        marker.userData = mapMarker
        
        let markerView = MarkerUIView(marker: mapMarker, showOrder: showOrder)
        
        let size = markerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
        markerView.frame = CGRect(origin: .zero, size: size)
        
        marker.iconView = markerView
    
        if let order = mapMarker.order {
            if (mapMarker.date != nil) {
                marker.zIndex = Int32(10000 - order)
            } else {
                marker.zIndex = Int32(1000 - order)
            }
        }
        
        return marker
    }
}

Edit:

It seems that if I comment out this code from my convertToGoogleMarkers() function, the default google maps markers show fine. What could be the issue with my custom markers?

//            let markerView = MarkerUIView(marker: mapMarker, showOrder: showOrder)
//
//            let size = markerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
//            markerView.frame = CGRect(origin: .zero, size: size)
//
//            marker.iconView = markerView

Here is my MapMarkerView:

import SwiftUI
import MapKit

struct MapMarkerView: View {
    var marker: MapMarker2
    var showOrder: Bool = false
    
    let circleBorderWidth: CGFloat = 4
    
    var body: some View {
        VStack(spacing: 0) {
            ZStack {
                let color = marker.hexColor != nil ? Color(hex: marker.hexColor!) : Color.wireframe
                
                if (showOrder && marker.date != nil) {
                    Circle()
                        .fill(color)
                        .frame(width: 40, height: 40)
                        .shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 4)
                        .overlay(
                            Circle()
                                .inset(by: circleBorderWidth / 2)
                                .stroke(Color.mapMarkerBackground, lineWidth: circleBorderWidth)
                        )
                    
                    if let order = marker.order {
                        Text("\(order)")
                            .font(.custom("Inter-SemiBold", size: 18))
                            .foregroundColor(Color.white)
                    }
                } else {
                    Circle()
                        .fill(Color.mapMarkerBackground)
                        .frame(width: 40, height: 40)
                        .shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 4)
                        .overlay(
                            ZStack {
                                Circle()
                                    .fill(color)
                                    .opacity(0.33)
                                
                                Circle()
                                    .inset(by: circleBorderWidth / 2)
                                    .stroke(Color.mapMarkerBackground, lineWidth: circleBorderWidth)
                            }
                        )
                
                    AsyncImage(url: URL(string: marker.iconUrl ?? "")) { image in
                        image.resizable()
                            .frame(width: 24, height: 24)
                    } placeholder: {
                        Circle()
                            .fill(Color.mapMarkerBackground)
                            .frame(width: 40, height: 40)
                    }
                }
            }
            
            Triangle()
                .rotationEffect(.degrees(-180))
                .foregroundColor(Color.mapMarkerBackground)
                .frame(width: 12, height: 10)
                .offset(y: -4)
        }
    }
}

class MarkerUIView: UIView {
    private var hostingController: UIHostingController<AnyView>?
    private var onTapAction: (() -> Void)?
    
    init(marker: MapMarker2, showOrder: Bool = false) {
        super.init(frame: .zero)
        
        // Use AnyView so we can capture the binding
        self.hostingController = UIHostingController(
            rootView: AnyView(MapMarkerView(marker: marker, showOrder: showOrder))
        )
        
        guard let hostingView = hostingController?.view else { return }
        
        // Make background transparent
        hostingView.backgroundColor = .clear
        
        // Add the SwiftUI view to the UIView hierarchy
        addSubview(hostingView)
        
        // Size the hosting controller's view
        hostingView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            hostingView.leadingAnchor.constraint(equalTo: leadingAnchor),
            hostingView.trailingAnchor.constraint(equalTo: trailingAnchor),
            hostingView.topAnchor.constraint(equalTo: topAnchor),
            hostingView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
        
        sizeToFit()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // Forward tap to the SwiftUI view
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        onTapAction?()
        super.touchesEnded(touches, with: event)
    }
}
7
  • Unrelated, but note you could just use: .task { await apiManager.fetchAllMapMarkers() } Commented Oct 23 at 1:27
  • 1
    Have you tried using the paddingAdjustmentBehavior parameter on the makeUIView and setting the value to .never? Commented Oct 24 at 5:29
  • @Lanceslide Yeah, I've tried that and still had no luck unfortunately :( Commented Oct 24 at 14:55
  • Can you try ignoresSafeAreaExceptTop() that you have... Commented Oct 28 at 6:12
  • 1
    check github.com/kasimok/GoogleMapStatckOverflow, the issue is not reproducible. Commented Nov 3 at 1:28

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.