Hello,
After a fair amount of time of trial and error, I have seemingly discovered a bug in iOS 18 with .toolBar with placement:. keyboard)
Feedback: FB15205988
Xcode Version: 16.0 (16A242d)
iOS Version: 18.0 (22A3351)
I'd appreciate help to see if I'm missing something here.
What's happening?
When using .toolBar with (placement: .keyboard), within a NavigationSplitView detail:, encompassed in a TabView, the .toolBar view does not appear above the keyboard.
Within the code, the usage of .toolBar is within ShakeDetail.swift.
When commenting out the TabView within ShakeTabView.swift, or running on iOS 17.5, the .toolbar appears.
Would greatly appreciate help if someone knows a workaround, or a misunderstanding here of course!
Code
Shake.swift
import Foundation
import SwiftUI
struct Shake: Equatable, Identifiable, Hashable {
var name: String
let id = UUID()
static var example = Shake(name: "Banana")
static var many: [Shake] {
return [
Shake(name: "Apple"),
Shake(name: "Banana"),
Shake(name: "Cherry"),
Shake(name: "Berry"),
]
}
}
ShakeDetail.swift
import SwiftUI
struct ShakeDetail: View {
@State var shake: Shake = Shake(name: "")
var body: some View {
List {
TextField("dsf", text: $shake.name, prompt: Text("Enter your favorite shake"))
}
.scrollDismissesKeyboard(.interactively)
.toolbar {
ToolbarItem(placement: .keyboard) {
HStack(alignment: .center) {
Text(shake.name)
.font(.title3)
.foregroundColor(.red)
}
}
}
.toolbarColorScheme(.dark, for: .navigationBar)
.toolbarBackground(
Color.purple,
for: .navigationBar
)
.toolbarBackground(.visible, for: .navigationBar)
}
}
#Preview {
ShakeDetail()
}
ShakeListView.swift
struct ShakeListView: View {
@Binding var shakes: [Shake]
@Binding var selectedShake: Shake?
var body: some View {
List(selection: $selectedShake) {
ForEach(shakes) { shake in
NavigationLink(value: shake) {
Text(shake.name)
.font(.system(.title2, design: .rounded))
.foregroundStyle(.primary)
}
}
}
.navigationTitle("Shakes")
}
}
#Preview {
@Previewable @State var shakes: [Shake] = Shake.many
@Previewable @State var selectedShake: Shake?
ZStack {
ShakeListView(shakes: $shakes, selectedShake: $selectedShake)
}
}
ShakeSplitView.swift
import SwiftUI
import OSLog
public struct ShakeSplitView: View {
@State var selectedShake: Shake?
@State var shakes = Shake.many
@State private var columnVisibility = NavigationSplitViewVisibility.doubleColumn
public init(isShowingFavorites: Bool = false, columnVisibility: SwiftUI.NavigationSplitViewVisibility = NavigationSplitViewVisibility.doubleColumn) {
self.columnVisibility = columnVisibility
}
public var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
ShakeListView(shakes: $shakes, selectedShake: $selectedShake)
} detail: {
if let selectedShake {
// Add shake detail
ShakeDetail(shake: selectedShake)
} else {
Label("Please select a shake", systemImage: "arrow.backward.circle")
.foregroundColor(.purple)
.font(.system(.title2, design: .rounded))
}
}
.navigationSplitViewStyle(.balanced)
}
}
struct FormulaSplitView_Previews: PreviewProvider {
static var previews: some View {
ShakeSplitView()
}
}
ShakeTabView.swift
import SwiftUI
struct ShakeTabView: View {
var body: some View {
/* TabView breaks the ToolbarItem(placement: .keyboard) */
TabView {
ShakeSplitView()
.tabItem {
Image(systemName: "carrot.fill")
Text("Shakes")
}
}
}
}
#Preview {
ShakeTabView()
}
Post
Replies
Boosts
Views
Activity
Hi All!
I've encountered a SwiftUI NavigationSplitView bug in my app that I'd appreciate help with, I've spent quite some time trying to work out a solution to this problem.
I have a main list, which has either a Location or LocationGroup. When selecting a Location, the detail view is presented.
When selecting a LocationGroup, a subsequent list of Locations are presented (from the group).
When tapping on any of these Locations, the NavigationStack does not animate (push the view), and when tapping back from it, it jumps to the main selection.
The overall goal is to be able to present within the detail: of a NavigationSplitView a NavigationStack, and to be able to push onto that stack from both the main NavigationSplitView's list, and from the detail.
I created the following sample code that reproduces the issue:
ContentView.swift
public enum LocationSplitViewNavigationItem: Identifiable, Hashable {
case location(Location)
case locationGroup(LocationGroup)
public var id: UUID? {
switch self {
case .location(let location):
return location.id
case .locationGroup(let locationGroup):
return locationGroup.id
}
}
}
struct ContentView: View {
@State private var columnVisibility = NavigationSplitViewVisibility.doubleColumn
@State private var selectedNavigationItem: LocationSplitViewNavigationItem?
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
LocationListView(selectedItem: $selectedNavigationItem, locationData: LocationSampleData(locations: LocationSampleData.sampleLocations, locationGroups: LocationSampleData.sampleLocationGroups))
} detail: {
if let selectedLocation = selectedNavigationItem {
switch selectedLocation {
case .location(let location):
LocationDetailView(selectedLocation: location)
case .locationGroup(let locationGroup):
LocationListView(selectedItem: $selectedNavigationItem, locationData: LocationSampleData(locations: locationGroup.locations))
}
}
}
.navigationSplitViewStyle(.balanced)
}
}
LocationListView.swift
struct LocationListView: View {
@Binding public var selectedItem: LocationSplitViewNavigationItem?
var locationData: LocationSampleData
var body: some View {
List(selection: $selectedItem) {
if let locations = locationData.locations {
ForEach(locations) { location in
NavigationLink(value: LocationSplitViewNavigationItem.location(location)) {
Text(location.name)
.bold()
}
}
}
if let locationGroups = locationData.locationGroups {
ForEach(locationGroups) { locationGroup in
NavigationLink(value: LocationSplitViewNavigationItem.locationGroup(locationGroup)) {
Text(locationGroup.name)
.bold()
.foregroundStyle(.red)
}
}
}
}
.navigationTitle("Saturday Spots")
.navigationBarTitleDisplayMode(.large)
}
}
LocationDetailView.swift
struct LocationDetailView: View {
var selectedLocation: Location
var body: some View {
Text(selectedLocation.name)
.font(.largeTitle)
.bold()
.foregroundStyle(LinearGradient(
colors: [.teal, .indigo],
startPoint: .top,
endPoint: .bottom
))
.toolbarBackground(
Color.orange,
for: .navigationBar
)
.toolbarBackground(.visible, for: .navigationBar)
}
}
Location.swift
import CoreLocation
struct LocationSampleData {
var locations: [Location]?
var locationGroups: [LocationGroup]?
static let sampleLocations: [Location]? = Location.sample
static let sampleLocationGroups: [LocationGroup]? = [LocationGroup.sample]
}
public struct Location: Hashable, Identifiable {
var name: String
var coordinates: CLLocationCoordinate2D
var photo: String
public var id = UUID()
static public let sample: [Location] = [
Location(name: "Best Bagel & Coffee", coordinates: CLLocationCoordinate2D(latitude: 123, longitude: 121), photo: "asdf"),
Location(name: "Absolute Bagels", coordinates: CLLocationCoordinate2D(latitude: 123, longitude: 121), photo: "asdf"),
Location(name: "Tompkins Square Bagels", coordinates: CLLocationCoordinate2D(latitude: 123, longitude: 121), photo: "asdf"),
Location(name: "Zabar's", coordinates: CLLocationCoordinate2D(latitude: 123, longitude: 121), photo: "asdf"),
]
static public let oneSample = Location(name: "Absolute Bagels", coordinates: CLLocationCoordinate2D(latitude: 123, longitude: 121), photo: "asdf")
}
public struct LocationGroup: Identifiable, Hashable {
var name: String
var locations: [Location]
public var id = UUID()
static public let sample: LocationGroup = LocationGroup(name: "Bowling", locations: [
Location(name: "Frames Bowling Lounge", coordinates: CLLocationCoordinate2D(latitude: 123, longitude: 121), photo: "asdf"),
Location(name: "Bowlero Chelsea Piers", coordinates: CLLocationCoordinate2D(latitude: 123, longitude: 121), photo: "asdf")
])
}
extension CLLocationCoordinate2D: Hashable {
public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
}
public func hash(into hasher: inout Hasher) {
hasher.combine(latitude)
hasher.combine(longitude)
}
}