ZYVOPMulti-Platform Sync
SeriesAI NewsWhy ZyVOPJoin Discord
LoginGet Started
ZYVOPMulti-Platform Sync

The Developer Publishing Hub. Write once, publish everywhere, and make your work citation-ready with built-in SEO, AEO, and GEO discovery support. Zero reader paywalls.

Content

  • Categories
  • Tags
  • Badges
  • Leaderboard
  • Write Article
  • Newsletter

Company

  • About Us
  • Why ZyVOP
  • Developer API & CLI
  • Write for Us
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

ยฉ 2026 ZyVOP. Developer Publishing Hub.

Zero paywalls ยท Full content ownership
All systems operational
HomeThe popover that talks to a window that doesn't exist yet

The popover that talks to a window that doesn't exist yet

Giovambattista Fazioli
Giovambattista Fazioli
Senior Full-stack Engineer, Lead DeveloperSupport
September 4, 2026
3 min read
The popover that talks to a window that doesn't exist yet
#swift#macos#swiftui#menubar

I build Netfox, a macOS network monitor. It lives in two places at once: a full window, and a menu bar popover that shows your network at a glance โ€” devices online, risk level, public IP, a little live traffic chart.

The popover has four status cards. Someone asked the obvious question: "can I click a card and have it open that section in the main window?"

Sure. Easy. Except it wasn't, and the reason why is a nice little lesson about how SwiftUI scenes are isolated from each other.

Why the obvious approach doesn't work

In a single-scene SwiftUI app, the way you'd wire a "menu command triggers an action in a view" is @FocusedValue / .focusedSceneValue. The view publishes a closure; a command elsewhere reads it. Clean.

But a MenuBarExtra is its own scene, separate from your WindowGroup/Window. Focused-scene-values are scoped to a scene tree โ€” they don't cross from the popover scene into the main window scene. So the popover literally cannot reach the main window's selection state that way.

And it gets worse. My app is "dual-mode": closing the main window doesn't quit it (the MenuBarExtra scene keeps it alive). So at the moment a popover card is clicked, the main window might not exist at all. There's no view to send a message to.

You can't message a window that isn't there.

The shape of the fix: a tiny shared router

The trick is to stop thinking "popover โ†’ window" and start thinking "popover โ†’ shared state โ†’ window, whenever it shows up."

A ten-line @Observable singleton both scenes can see:

import Observation

@MainActor@Observablefinal class ShellRouter {
    static let shared = ShellRouter()

    /// Section the shell should switch to at the next opportunity.
    /// `nil` = nothing pending.
    var pendingSection: AppSection?

    private init() {}}

Enter fullscreen mode Exit fullscreen mode

That's the whole bridge. The popover writes to it; the window reads from it.

The popover side: queue, open, dismiss

private func openSection(_ section: AppSection) {
    ShellRouter.shared.pendingSection = section   // queue the destination
    openMainWindow()                              // raise / recreate the window
    dismiss()                                     // close the popover}

Enter fullscreen mode Exit fullscreen mode

openMainWindow() is just openWindow(id: "main") plus NSApp.activate(...). If the window was closed, openWindow recreates it. If it was already open, it comes forward. Either way, a fresh pendingSection is sitting in the router waiting to be consumed.

The window side: the part everyone gets wrong

Here's the subtlety. You need to consume pendingSection in two places, because there are two completely different timelines:

NavigationSplitView { ... } detail: { ... }
    // Timeline A: the window was CLOSED and just got recreated.
    // Its .task runs on mount โ€” and a value set *before* mount
    // can't be observed by .onChange, because .onChange only fires
    // on *changes that happen while mounted*.
    .task { consumePendingSection() }

    // Timeline B: the window was ALREADY OPEN. The popover sets
    // pendingSection while the shell is mounted โ€” .onChange catches it.
    .onChange(of: router.pendingSection) { _, _ in
        consumePendingSection()
    }

Enter fullscreen mode Exit fullscreen mode

private func consumePendingSection() {
    guard let section = router.pendingSection else { return }
    selectedSection = section
    router.pendingSection = nil          // โ† clear it. this matters.}

Enter fullscreen mode Exit fullscreen mode

If you only use .onChange, the window-was-closed case silently does nothing: the value was set before the view existed, so there's no "change" to observe โ€” .task is your only hook there. If you only use .task, the window-already-open case does nothing: the view's already mounted, .task already ran.

You need both. They cover disjoint timelines.

Why the clear is load-bearing

router.pendingSection = nil after consuming isn't tidiness โ€” it's correctness. Without it:

  1. You click "Security" in the popover โ†’ window opens on Security. Good.

  2. You close the window.

  3. A week later you reopen the window normally (from the Dock, not a popover card).

  4. .task fires, finds the stale pendingSection still pointing at Security, and yanks you to Security instead of your default tab.

Clearing on consume makes the destination strictly one-shot.

The takeaway

When two SwiftUI scenes need to talk and one of them might not be alive yet, don't look for a direct channel โ€” there isn't one. Put the intent in shared observable state, and have the receiver drain it both on appearance (for the just-created case) and on change (for the already-alive case). Then clear it, so intent doesn't outlive its moment.

It's the same idea as a message queue with at-most-once delivery, shrunk down to a single @Observable property. Sometimes the smallest abstraction is the right one.

Comments (0)

Login to post a comment.

Giovambattista Fazioli
Giovambattista Fazioli

Senior Full-stack Engineer, Lead Developer

Italian Senior Full-stack Engineer and Lead Developer on the Cloud team at Namecheap. I build developer tools, React/Mantine UI components and native macOS apps โ€” mostly open source. I work across TypeScript, Next.js, Go and SwiftUI, and I've been coding since the Commodore/Assembly days. Creator of WP Bones and 25+ Mantine extensions, and maintainer of the Undolog open-source studio.

Support
Subscribe to Giovambattista Fazioli's Newsletter

More from Giovambattista Fazioli

View profile

FinderGit 0.29.0 โ€” open a commit like a folder

Thereโ€™s a moment, reading a commit list, where you want to know one small thing: which files did that one touch?

3 minSep 4

Netfox 0.18.0 โ€” is it my connection, or the internet?

There is a question that comes up every time something feels slow, and until now Netfox could not...

3 minSep 4

Mantine Book โ€” Grab Any Edge, Turn Any Page

A realistic iBooks-style book for React built on Mantine: stack two-sided pages and turn them by...

5 minSep 4

Netfox โ€” The macOS app that never loses track of your network

I built Netfox because every existing answer to "what's actually on my network?" annoyed me. The...

5 minSep 4

Audio That Sees Itself

A Mantine-native audio player for React with waveform visualisation and a live spectrum analyser,...

5 minSep 4