Search code examples
swiftswiftuimapkitswiftui-listswiftui-navigationstack

SwiftUI List/ScrollView x MapKit - Map adds gradient/material toolbar (navigation bar) and overrides its design


I'm working on iOS 17+ app in SwiftUI, which uses MapKit. MapKit's Map view adds a gradient/material view on Toolbar, which is presented on second and every next navigation.

Unexpected bahavior

I would like to get the first's navigation behavior, which is no background and no shadow image (navbar divider).

This issue is 100% related to the Map view - commenting it out resolves the issue, but also removes the functionality, which I need.

Code:

struct HomeView: View {
    var body: some View {
        NavigationStack {
            Content(/* properties */)
        }
    }
}

struct Content: View {
    // Properties ...

    var body: some View {
        Group {
            if array.isEmpty {
                EmptyStateView()
            } else {
                ListView(data: array, onDelete: onDelete)
            }
        }
        .accentBackground(strong: true) // << Gradient background
        .navigationTitle("Title")
        .toolbar {
            ToolbarItem(placement: .topBarTrailing) {
                AddButton()
            }
        }
    }
}

struct ListView: View {
    // Properties ...

    var body: some View {
        List {
            ForEach(data) { list in
                Row(/* properties */)
            }
            .onDelete(perform: onDelete)
        }
        .scrollContentBackground(.hidden)
    }
}

struct Row: View {
    // Properties ...

    var body: some View {
        NavigationLink(destination: DetailsView(list: list)) {
            VStack(alignment: .leading) {
                Text(list.title)
                    .font(.headline)
                Text(/* sublabel */)
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }
        }
    }
}

struct DetailsView: View {
    // Properties ...

    var body: some View {
        List {
            // Some conditional subviews ...

            if let location = list.location {
                Section {
                    // Edit mode conditional remove button ...

                    PresentableMap(location: location)
                        .frame(height: 300)
                }
            }
        }
        .scrollContentBackground(.hidden)
        .accentBackground(strong: true)
        .navigationTitle(list.title)
    }
}

struct PresentableMap: View {
    let location: ListLocation

    private var coordinates: CLLocationCoordinate2D {
        .init(latitude: location.latitude, longitude: location.longitude)
    }

    var body: some View {
        // MapKit View ⬇️
        Map(position: .constant(.camera(.init(centerCoordinate: coordinates, distance: 200))), selection: .constant(location)) {
            Marker(coordinate: coordinates) {
                Image(systemName: "mappin")
            }
        }
        .clipShape(RoundedRectangle(cornerRadius: 8))
        .allowsHitTesting(false)
    }
}

What I tried:

  • .toolbarBackground(.hidden, for: .navigationBar) and other SwiftUI modifiers for hidden navbar background - resolves the issue, but also removes the toolbar background for inline navbar, which is presented during scroll. I would like to keep the large title style and present inline navbar only during scroll. Screenshot of the bugged scroll:
  • Overriding UINavigationBarAppearance - breaks whole app.
  • Overriding UINavigationBar by its properties using SwiftUIIntrospect - can be used only on the NavigationStack, which leds to the breaking Home and Details views, similar behavior to the first point. Can't Introspect navigation controller on Map view - I can only Introspect Map, which is MKMapView, which is UIView, which doesn't have access to NavigationController and its NavigationBar. inputViewController or inputAccessoryViewController are nil.
  • Changing the toolbar color to red (which I don't like, but shows how the app behaves; I don't want to use any color, even matching one, because it results in the ugly design, where the space is anyway divided and doesn't match the linear gradient background):
    Buggy toolbar appearance override

On the gif above you can see that overriding the background respects the safe area, however, the toolbar material added by Map view ignores the safe area.

What do I expect:

  • If that's possible, I would like to keep the List (+its current style) and native NavigationBar with large title.
  • Such behavior:
    Correct behavior example

Solution

  • It seems to help to add a modifier for .toolbarBackgroundVisibility that sets the visibility to .automatic, even though you would expect this to be the default:

    // DetailsView
    
    List {
        // Some conditional subviews ...
    
        if let location = list.location {
            Section {
                // Edit mode conditional remove button ...
    
                PresentableMap(location: location)
                    .frame(height: 300)
            }
        }
    }
    .scrollContentBackground(.hidden)
    .accentBackground(strong: true)
    .navigationTitle(list.title)
    .toolbarBackgroundVisibility(.automatic, for: .navigationBar) // 👈 here
    

    The modifier .toolbarBackgroundVisibility was introduced in iOS 18. For iOS 16 and 17, use .toolbarBackground instead:

    .toolbarBackground(.automatic, for: .navigationBar)