Toolbar buttons disappearing when showing a navigation split view as a sheet

When displaying a view using a navigation Split View as a sheet, the toolbar button will disappear if you leave the app and resume it.

import SwiftUI

struct Folder: Hashable, Identifiable {
    let id = UUID()
    let name: String
}

struct ContentView: View {
    @State private var showingSettings = false
    
    var body: some View {
        VStack {
            Button {
                showingSettings.toggle()
            } label: {
                Text("Settings")
            }
        }
        .sheet(isPresented: $showingSettings, content: {
            SettingsView()
        })
        .padding()
    }
}


struct SettingsView: View {
    @Environment(\.dismiss) private var dismiss
    
    @State private var selectedFolder: Folder? = nil
    
    private var folders = [Folder(name: "Recents"), Folder(name: "Deleted"), Folder(name: "Custom")]
    
    var body: some View {
        NavigationSplitView {
            SidebarView(selectedFolder: $selectedFolder, folders: folders)
        } detail: {
            VStack {
                if let folder = selectedFolder {
                    Text(folder.name)
                }
                else {
                    Text("No selection")
                }
            }
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button {
                        dismiss()
                    } label: {
                        Text("Cancel")
                    }
                }
                
                ToolbarItem(placement: .confirmationAction) {
                    Button {
                        dismiss()
                    } label: {
                        Text("Save")
                    }
                }
            }
        }
    }
}

struct SidebarView: View {
    @Binding var selectedFolder: Folder?
    
    var folders: [Folder]
    
    var body: some View {
        List(selection: $selectedFolder) {
            ForEach(folders) { folder in
                NavigationLink(value: folder) {
                    Text(folder.name)
                }
            }
        }
    }
}

Steps to reproduce the issue:

  • Launch the attached project on an iPad or iPad simulator
  • Tap the Settings button
  • Select one item in the sidebar
  • Use the app switcher to open an other app or just leave the app
  • Bring back the app

Result:

Both Cancel and Save buttons are gone.

Note:

This will not occur if no item is selected in the sidebar.

FB12991687

Accepted Reply

Even better workaround:

struct DetailsView: View {
    @Environment(\.dismiss) private var dismiss
    
    @Binding var selectedFolder: Folder?
    
    var body: some View {
        VStack {
            if let folder = selectedFolder {
                Text(folder.name)
            }
            else {
                Text("No selection")
            }
        }
        .toolbar {
            ToolbarItem(placement: .cancellationAction) {
                Button {
                    dismiss()
                } label: {
                    Text("Cancel")
                }
                .id(UUID())
            }
            
            ToolbarItem(placement: .confirmationAction) {
                Button {
                    dismiss()
                } label: {
                    Text("Save")
                }
                .id(UUID())
            }
        }
    }
}

Replies

Found a workaround:

struct DetailsView: View {
    @Environment(\.dismiss) private var dismiss
    
    @Binding var selectedFolder: Folder?
    
    @Environment(\.scenePhase) private var scenePhase
    @State var refreshID = UUID()
    
    var body: some View {
        VStack {
            if let folder = selectedFolder {
                Text(folder.name)
            }
            else {
                Text("No selection")
            }
        }
        .onChange(of: scenePhase) { _, newPhase in
            refreshID = UUID()
        }
        .toolbar {
            ToolbarItem(placement: .cancellationAction) {
                Button {
                    dismiss()
                } label: {
                    Text("Cancel")
                }
                .id(refreshID)
            }
            
            ToolbarItem(placement: .confirmationAction) {
                Button {
                    dismiss()
                } label: {
                    Text("Save")
                }
                .id(refreshID)
            }
        }
    }
}

Forcing an ID refresh seems to work but still, this should be fixed.

Even better workaround:

struct DetailsView: View {
    @Environment(\.dismiss) private var dismiss
    
    @Binding var selectedFolder: Folder?
    
    var body: some View {
        VStack {
            if let folder = selectedFolder {
                Text(folder.name)
            }
            else {
                Text("No selection")
            }
        }
        .toolbar {
            ToolbarItem(placement: .cancellationAction) {
                Button {
                    dismiss()
                } label: {
                    Text("Cancel")
                }
                .id(UUID())
            }
            
            ToolbarItem(placement: .confirmationAction) {
                Button {
                    dismiss()
                } label: {
                    Text("Save")
                }
                .id(UUID())
            }
        }
    }
}

This has been fixed in the latest iPadOS 17.4 beta, BTW.