SwiftUI NavigationSplitView: Persisting a Floating Bar through Navigation

I'm currently working with NavigationSplitView introduced in iOS 16. My aim is to maintain a floating bar at the bottom of the detail view, kind of like the playback bar in the Apple Music app which remains visible no matter how the user navigates through the app.

To recreate this functionality, my initial approach involved using a safe area inset on the NavigationStack in the detail view. This worked until I needed to navigate to another view. When using a NavigationLink to push a destination view, the floating bar was obscured by the pushed view.

I tried moving the safe area inset to the navigation split view itself just to see what would happen. The floating bar did end up staying visible as I navigated but it stretched across both the detail and sidebar views. I solved this problem by using a geometry reader and preference key to set the width according to the detail view, but challenges arose when the sidebar was in its slide-over presentation in portrait mode on iPad.

I ended up using SwiftUI Introspect to add the floating bar view to the navigation split view. This method provided the desired results, but the necessity to use the hosting view controller and manage the subview within the introspect closure complicated data passage and UI updates. I used an environment object to get data to the floating bar which works perfectly on the iPad, but crashes consistently on the iPhone with the following error in the floating bar view:

Thread 1: Fatal error: No ObservableObject of type AppState found. A View.environmentObject(_:) for AppState may be missing as an ancestor of this view.

final class AppState: ObservableObject {
    @Published var number: Int = 0
    @Published var showFloatingBar = true
}

struct ContentView: View {
    @StateObject private var appState = AppState()

    var body: some View {
        
        NavigationSplitView {
            // ...
        } detail: {
            NavigationStack {
                // ...
            }
        }
        .introspect(.navigationSplitView, on: .iOS(.v17)) { entity in
            guard let detailView = entity.viewController(for: .secondary)?.view else { return }
            guard detailView.viewWithTag(123) == nil else { return }
            
            let hosting = UIHostingController(rootView: FloatingBar())
            detailView.addSubview(hosting.view)
            hosting.view.tag = 123
            hosting.view.backgroundColor = .clear
            
            hosting.view.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                hosting.view.leadingAnchor.constraint(equalTo: detailView.leadingAnchor),
                hosting.view.trailingAnchor.constraint(equalTo: detailView.trailingAnchor),
                hosting.view.bottomAnchor.constraint(equalTo: detailView.bottomAnchor)
            ])
        }
        .environmentObject(appState)
    }
  
}

struct FloatingBar: View {
    @EnvironmentObject private var appState: AppState
    
    var body: some View {
        if appState.showFloatingBar {
            VStack {
                ZStack {
                    RoundedRectangle(cornerRadius: 12)
                        .fill(.thinMaterial)
                        .frame(height: 50)
                    
                    Text(appState.number.description)
                }
            }
            .padding()
        }
    }
}

I feel like I need to either fix the way I'm passing data to the floating bar view or change my approach from using introspect completely. I would ideally like to use a "SwiftUI Native" solution for this, but I've had no luck in finding one so far. I'm developing this on iOS 17 beta, mainly testing on iPad but ensuring compatibility with iPhone as well.

I appreciate any insights, advice, or potential solutions to help me achieve the desired behavior. Thank you for your time!

I would still prefer a more SwiftUI-centric approach if anyone has any ideas, but I did figure out the error:

The error was actually also happening on iPad when the horizontal size class switched to compact. I didn't realize that the navigation split view uses a different view controller when this happens. I changed the code to select the view controller based on the size class:

let viewController = entity.viewController(
    for: horizontalSizeClass == .compact ? .compact : .secondary
)
guard let detailView = viewController?.view else { return }
SwiftUI NavigationSplitView: Persisting a Floating Bar through Navigation
 
 
Q