SwiftUI Map menu / chrome placement — three approaches (overlay, ZStack + safeAreaPadding, safeAreaInset): which one is best practice?

Hi everyone,

I’m building a full-screen Map (MapKit + SwiftUI) with persistent top/bottom chrome (menu buttons on top, session stats + map controls on bottom). I have three working implementations and I’d like guidance on which pattern Apple recommends long-term (gesture correctness, safe areas, Dynamic Island/home indicator, and future compatibility).

Version 1 — overlay(alignment:) on Map

Idea: Draw chrome using .overlay(alignment:) directly on the map and manage padding manually.

Map(position: $viewModel.previewMapCameraPosition, scope: mapScope) {
    UserAnnotation {
        UserLocationCourseMarkerView(angle: viewModel.userCourse - mapHeading)
    }
}
.mapStyle(viewModel.mapType.mapStyle)
.mapControls {
    MapUserLocationButton().mapControlVisibility(.hidden)
    MapCompass().mapControlVisibility(.hidden)
    MapPitchToggle().mapControlVisibility(.hidden)
    MapScaleView().mapControlVisibility(.hidden)
}
.overlay(alignment: .top) { mapMenu }         // manual padding inside
.overlay(alignment: .bottom) { bottomChrome }  // manual padding inside

Version 2 — ZStack + .safeAreaPadding

Idea: Place the map at the back, then lay out top/bottom chrome in a VStack inside a ZStack, and use .safeAreaPadding(.all) so content respects safe areas.

ZStack(alignment: .top) {
    Map(...).ignoresSafeArea()
    VStack {
        mapMenu
        Spacer()
        bottomChrome
    }
    .safeAreaPadding(.all)
}

Version 3 — .safeAreaInset on the Map

Idea: Make the map full-bleed and then reserve top/bottom space with safeAreaInset, letting SwiftUI manage insets

Map(...).ignoresSafeArea()
    .mapStyle(viewModel.mapType.mapStyle)
    .mapControls {
        MapUserLocationButton().mapControlVisibility(.hidden)
        MapCompass().mapControlVisibility(.hidden)
        MapPitchToggle().mapControlVisibility(.hidden)
        MapScaleView().mapControlVisibility(.hidden)
    }
    .safeAreaInset(edge: .top) { mapMenu } // manual padding inside
    .safeAreaInset(edge: .bottom) { bottomChrome } // manual padding inside

Question

I noticed:

Safe-area / padding behavior – Version 2 requires the least extra padding and seems to create a small but partial safe-area spacing automatically. – Version 3 still needs roughly the same manual padding as Version 1, even though it uses safeAreaInset. Why doesn’t safeAreaInset fully handle that spacing?

Rotation crash (Metal) When using Version 3 (safeAreaInset + ignoresSafeArea), rotating the device portrait↔landscape several times triggers a Metal crash: failed assertion 'The following Metal object is being destroyed while still required… CAMetalLayer Display Drawable' The same crash can happen with Version 1, though less often. I haven’t tested it much with Version 2. Is this a known issue or race condition between Map’s internal Metal rendering and view layout changes?

Expected behavior What’s the intended or supported interaction between safeAreaInset, safeAreaPadding, and overlay when embedding persistent chrome inside a SwiftUI Map? Should safeAreaInset normally remove the need for manual padding, or is that by design?

SwiftUI Map menu / chrome placement — three approaches (overlay, ZStack + safeAreaPadding, safeAreaInset): which one is best practice?
 
 
Q