I'm trying to get a NavigationSplitView working where on the iPhone in compact/portrait mode when you go to the detail view there is a '.sheet' that is available only on that view, and when you navigate back it goes away. I'm coming across a few issues and I'm not sure what I'm doing wrong.
1 - preferredCompactColumn seems to be ignored at the start in my minimal reproducible sample, it always starts into the detail view. fixed thanks to link provided by sweeper - set to nil
2 - the sheet initially appears over the sidebar view at first launch despite being attached to the detail view. I get an error message... "swiftui sheet Presenting view controller from detached view controller is not supported, and may result in incorrect safe area insets and a corrupt root presentation. Make sure is in the view controller hierarchy before presenting from it. Will become a hard exception in a future release."
Minimal Reproducible Code below.
import SwiftUI
// simple sidebar for testing
struct SideBarView: View {
@State private var selectedInt: Int? = nil
var body: some View {
List(1...3, id: \.self, selection: $selectedInt) { int in
NavigationLink("Row \(int)", value: int)
}
}
}
// Detail View with a permanent bottom sheet
struct DetailView: View {
@State var sheetVisible: Bool = false
// fill the space
var body: some View {
VStack {
Spacer()
Text("detail view")
Spacer()
}
.onAppear(perform: {
sheetVisible = true
})
.onDisappear(perform: {
sheetVisible = false
})
// show a bottom sheet that is for this view
// in this case I have dismiss disable but it seems to break regardless
.sheet(isPresented: $sheetVisible) {
Text("Bottom Sheet")
.presentationDetents([.fraction(0.15), .medium, .large])
.presentationDragIndicator(.visible)
.interactiveDismissDisabled()
.presentationBackgroundInteraction(
.enabled(upThrough: .medium)
)
}
}
}
struct ContentView: View {
// bug? - this seems to be ignored at the start. Initial view is the detail
@State private var preferredColumn = NavigationSplitViewColumn.sidebar
var body: some View {
NavigationSplitView(
preferredCompactColumn: $preferredColumn)
{
// simple sidebar
SideBarView()
.navigationTitle("Sidebar")
} detail: {
// bug? - this detail view has a sheet displayed when active
// but it's causing issues with ehe split view and I'm not sure
// what I've done wrong with the setup
DetailView()
.navigationTitle("Details")
}
}
}
Tthe sheet appears even in the sidebar, because apparently the detail view's onAppear
is called as soon as the sidebar appears, even when the detail view hasn't appeared. I think this might not be intentional.
To work around this, I thought of a way where you would need to shift the ownership of selectedInt
from SideBarView
to ContentView
. The idea is to let the detail view know about whether anything is selected in the sidebar.
struct SideBarView: View {
@Binding var selectedInt: Int?
var body: some View {
List(1...3, id: \.self, selection: $selectedInt) { int in
NavigationLink("Row \(int)", value: int)
}
}
}
struct DetailView: View {
@State var sheetVisible: Bool = false
let hasSelected: Bool
var body: some View {
VStack {
Spacer()
Text("detail view")
Spacer()
}
.onChange(of: hasSelected, { oldValue, newValue in
if !oldValue && newValue {
sheetVisible = true
}
})
.sheet(isPresented: $sheetVisible) {
Text("Bottom Sheet")
.presentationDetents([.fraction(0.15), .medium, .large])
.presentationDragIndicator(.visible)
.interactiveDismissDisabled()
.presentationBackgroundInteraction(
.enabled(upThrough: .medium)
)
}
}
}
struct ContentView: View {
@State private var selectedInt: Int?
@State private var preferredColumn = NavigationSplitViewColumn.sidebar
var body: some View {
NavigationSplitView(
preferredCompactColumn: $preferredColumn)
{
SideBarView(selectedInt: $selectedInt)
.navigationTitle("Sidebar")
} detail: {
DetailView(hasSelected: selectedInt != nil)
.navigationTitle("Details")
}
}
}
DetailView
now knows whether itself is presented by checking hasSelected
. Only when hasSelected
changes from false to true (this means the user tapped a row in the sidebar), does it set sheetVisible = true
.