Toolbar Bugs

Update: Found a workaround. Minutes after publishing this, I was reading Chris Eidhof’s presentation on SwiftUI, and noticed he uses ZStack to wrap his conditional SwiftUI statements where I’ve been using Group. On a whim I tried changing Group to ZStack in my repro code below… and it solved the problem of the disappearing toolbar buttons. I’m relieved there’s a workaround to this bug but it’s still been a bit of a maddening experience.

Original post:

I really want to like SwiftUI. I like the way it manages state, and when it works, it feels magical. The problem is it doesn’t work reliably. For example, I spent way too much time banging my head against the wall managing toolbars in a SwiftUI app that targets the Mac. I finally distilled things down to this: If you have a NavigationSplitView, and have toolbar items associated with the detail: view, and that detail: view is conditional… then you’ll lose the toolbar items when the condition changes, and they never come back.

For example, here’s what my simple app looks like when I launch it:

D735457D 32EB 4151 BC39 E74A4DA693A1

See those toolbar items over the detail view? That’s what we’re going for.

But then click the Toggle Globe button and this happens:

89C3F695 3CD8 4F2F A9AD 77676BF72B72

The toolbar buttons have gone away and they’re not coming back. I haven’t figured out a great way to work around this. This bug only happens for SwiftUI apps that target MacOS. It doesn’t repro on iOS or in the “Scaled to match iPad” Catalyst mode. (All is not great on iOS though. In my “real” app instead of my toy app I wrote to isolate the disappearing-toolbar bug, the iOS app crashes when I try to install the toolbar. I haven’t yet isolated why it crashes. I don’t have time for this.)

Every year I think, “Maybe this is the year I can rely on SwiftUI,” and every year I walk away disappointed.

rdar://FB13106004

Repro code

import SwiftUI

struct ContentView: View {
  @State private var showGlobe = true
  var body: some View {
    NavigationSplitView {
      Text("sidebar")
    } content: {
      VStack {
        Text("content")
        Button("Toggle Globe", systemImage: "globe") {
          showGlobe.toggle()
        }
      }
      .padding()
      .toolbar(id: "my-toolbar") {
        ToolbarItem(id: "trash") {
          Button("Test", systemImage: "trash") {
            print("Tapped test")
          }
        }
      }
      .navigationTitle("Content")
    } detail: {
      Group {
        if showGlobe {
          VStack {
            Image(systemName: "globe")
              .imageScale(.large)
              .foregroundStyle(.tint)
            Text("Hello, world!")
          }
          .padding()
        } else {
          Text("There is no globe")
        }
      }
      .toolbarRole(.editor)
      .toolbar(id: "my-toolbar") {
        ToolbarItem(id: "toggle-globe", placement: .automatic) {
          Button("Toggle Globe", systemImage: "globe") {
            showGlobe.toggle()
          }
        }
        ToolbarItem(id: "bold", placement: .automatic) {
          ControlGroup {
            Button("Bold", systemImage: "bold") {
              print("tapped bold")
            }
            Button("Italic", systemImage: "italic") {
              print("tapped italic")
            }
          }
        }
      }
    }
  }
}