In my SwiftUI detail view I have this code (part of it)
import SwiftUI
import MapKit
import Kingfisher
import BottomSheet
struct VehiclePositionView: View {
@Environment(\.isPresented) var isPresented
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var themeProvider: ThemeProvider
@State var vehicle: VehicleModel
@State private var bottomSheetPosition: BottomSheetPosition = .dynamicBottom
@State private var cameraPosition = MapCameraPosition.region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 0, longitude: 0),
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
)
)
let span: (CGFloat, CGFloat) = (0.005, 0.005)
let imgUrl = URL(string: "https://clientes.gesfrota.pt/Gesfrota_Images/1/Viaturas/viatura_14563.jpg")!
var body: some View {
Map(position: $cameraPosition) {
Annotation(vehicle.plate, coordinate: location) {
Image(systemName: "car")
.foregroundColor(themeProvider.accentColor)
.background(
Circle()
.fill(Color.white)
.frame(width: 30, height: 30)
.shadow(color: getVehicleStatusColor(for: vehicle), radius: 3)
)
}
}
.mapStyle(.standard(elevation: .realistic, showsTraffic: true))
.onAppear {
NotificationCenter.notify(.toggleBottomBar)
cameraPosition = .region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: vehicle.latitude, longitude: vehicle.longitude),
span: MKCoordinateSpan(latitudeDelta: span.0, longitudeDelta: span.1)
)
)
}
.onChange(of: isPresented) {
if !isPresented {
NotificationCenter.notify(.toggleBottomBar)
}
}
.toolbar {
ToolbarItem(placement: .principal) {
VStack(spacing: 3) {
ZStack {
KFImage(imgUrl)
.fade(duration: 0.25)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 50, height: 50)
.clipShape(Circle())
.shadow(
color: colorScheme == .dark ? Color.white.opacity(0.7) : Color.black.opacity(0.3),
radius: 5
)
Circle()
.fill(getVehicleStatusColor(for: vehicle))
.frame(width: 12, height: 12)
.offset(x: 18, y: 20)
}
Text(vehicle.plate)
Text("\(vehicle.brand) \(vehicle.model)")
.font(.footnote)
.foregroundStyle(themeProvider.secondary)
}
//.padding(.top, 50)
}
}
.toolbarBackground(.thinMaterial, for: .navigationBar)
.toolbarRole(.editor)
.toolbarVisibility(.visible, for: .navigationBar)
.bottomSheet(bottomSheetPosition: $bottomSheetPosition, switchablePositions: [.dynamicTop, .dynamicBottom]) {
VStack(alignment: .leading, spacing: 8) {
Text(vehicle.plate)
.font(.title2)
.bold()
.foregroundStyle(themeProvider.primary)
Text("\(vehicle.brand) \(vehicle.model)")
.font(.callout)
.foregroundStyle(themeProvider.secondary)
}
.padding(.horizontal)
} mainContent: {
}
}
private var location: CLLocationCoordinate2D {
return CLLocationCoordinate2D(latitude: vehicle.latitude, longitude: vehicle.longitude)
}
}
I was expecting to get the following output, and this is exactly what I get in the Xcode preview
But on a real device/simulator I get this weird behaviour
As you can see, when the transition starts it does appear as expected but when the animation ends it clips the toolbar item
On the parent view I'm using a NavigationStack, no special configuration. On the detail (this view) the configuration for the toolbar/navbar is provided in this post. I'm targeting iOS 18
UPDATE
After some debugging I was able to narrow down the issue to this part of the code in the parent view
.navigationTitle("vehicles")
.searchable(text: $searchField, isPresented: $searchVisible, placement: .navigationBarDrawer, prompt: "search")
.toolbar {
ToolbarItemGroup(placement: .topBarTrailing) {
Button() { searchVisible = true } label: { Image(systemName: "magnifyingglass") }
Menu {
Picker(selection: $vehicleState) {
ForEach(VehicleState.allCases, id:\.self) { choice in
Text(LocalizedStringKey(choice.rawValue))
}
} label: {
Text("Select a state")
}
} label: {
if vehicleState == .all {
Image(systemName: "line.3.horizontal.decrease.circle")
} else {
Image(systemName: "line.3.horizontal.decrease.circle.fill")
}
}
}
}
Full RootView code
NavigationStack {
List(appState.vehicles.filter {
(
$0.model.lowercased().contains(searchField.lowercased()) ||
$0.plate.lowercased().contains(searchField.lowercased()) ||
$0.brand.lowercased().contains(searchField.lowercased()) ||
$0.driverName.lowercased().contains(searchField.lowercased()) ||
(vehicleLocations[$0.id]?.lowercased().contains(searchField.lowercased()) ?? false) ||
searchField.isEmpty
) && (
vehicleState == .all ||
(vehicleState == .off && !$0.ignition) ||
(vehicleState == .running && $0.ignition && $0.speed >= 5) ||
(vehicleState == .idle && $0.ignition && $0.speed < 5)
)
}) { vehicle in
NavigationLink(destination: VehiclePositionView(vehicle: vehicle)) {
VehicleCell(vehicle: vehicle, locatedCallback: {
vehicleLocations[vehicle.id] = $0
})
}
}
.safeAreaInset(edge: .bottom) {
Spacer()
.frame(height: navbarHeight + 10)
}
.navigationBarTitleDisplayMode(.large)
.navigationTitle("vehicles")
.searchable(text: $searchField, isPresented: $searchVisible, placement: .navigationBarDrawer, prompt: "search")
.toolbar {
ToolbarItemGroup(placement: .topBarTrailing) {
Button() { searchVisible = true } label: { Image(systemName: "magnifyingglass") }
Menu {
Picker(selection: $vehicleState) {
ForEach(VehicleState.allCases, id:\.self) { choice in
Text(LocalizedStringKey(choice.rawValue))
}
} label: {
Text("Select a state")
}
} label: {
if vehicleState == .all {
Image(systemName: "line.3.horizontal.decrease.circle")
} else {
Image(systemName: "line.3.horizontal.decrease.circle.fill")
}
}
}
}
}
I was able to reproduce the problem by reducing the detail view to just Map()
.
It helps if you set the navigation bar title display mode to .large
:
var body: some View {
Map()
.navigationBarTitleDisplayMode(.large) // 👈 here
.toolbar {
// ...
}
// + other modifiers
}
With a few small tweaks to sizes and spacing, you should be able to fit your header into the larger space that this gives you. Example:
Here is the full code for reproducing the example above:
import MapKit
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink("Go to next") {
VehiclePositionView()
}
}
}
}
struct VehiclePositionView: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
Map()
.navigationBarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .principal) {
VStack(spacing: 4) {
ZStack {
Image(systemName: "car.fill")
.resizable()
.foregroundStyle(.red)
.scaledToFit()
.frame(width: 50, height: 50)
.clipShape(Circle())
.shadow(
color: colorScheme == .dark ? Color.white.opacity(0.7) : Color.black.opacity(0.3),
radius: 5
)
Circle()
.fill(.gray)
.frame(width: 12, height: 12)
.offset(x: 18, y: 20)
}
Text("ABC 123") // vehicle.plate
Text("Apple Car") // "\(vehicle.brand) \(vehicle.model)"
.font(.caption)
.offset(y: -6)
}
.padding(.top, 50)
}
}
.toolbarBackground(.thinMaterial, for: .navigationBar)
.toolbarRole(.editor)
.toolbarVisibility(.visible, for: .navigationBar)
}
}