Skip to main content

Overview

MenuBarManager manages a persistent NSStatusItem and NSPopover for the r2Vault menu bar widget. The popover uses .applicationDefined behavior to remain open even when the app loses focus, requiring explicit user interaction to dismiss.
Located at: Fiaxe/Services/MenuBarManager.swift:17

Type Definition

Must run on the main actor since it manages AppKit UI components.

Initialization

AppViewModel
required
The app’s view model, injected into the SwiftUI environment for the popover content.
The initializer sets up both the status item and popover:

Properties

statusItem

The menu bar status item displayed in the system status bar (menu bar).

popover

The popover shown when the status item is clicked.

viewModel

The app’s view model passed to the SwiftUI popover content.

Setup Methods

setupStatusItem()

Configures the menu bar status item.

Implementation Details

The method:
  1. Creates a status item with square dimensions (typically 22x22 points)
  2. Sets an SF Symbol icon: arrow.up.to.line.compact (upload icon)
  3. Configures accessibility description for VoiceOver
  4. Connects the button’s action to togglePopover

setupPopover()

Configures the popover with SwiftUI content.

Implementation Details

Key configuration:
  • Content size: 300x400 points
  • Behavior: .applicationDefined - popover won’t auto-dismiss when focus is lost
  • Animation: Enabled for smooth show/hide transitions
  • Content: SwiftUI MenuBarView wrapped in AlwaysActiveHostingController
The .applicationDefined behavior means the popover only dismisses when:
  • User clicks the status bar icon again
  • User explicitly closes it via UI
  • App calls popover.performClose(nil)
It will NOT dismiss when:
  • User clicks outside the popover
  • App loses focus
  • User switches to another app

Toggle Method

togglePopover()

Toggles the popover visibility when the status bar icon is clicked.

Implementation Details

When showing the popover:
  1. Shows it relative to the status bar button’s bounds
  2. Positions it below the button (.minY edge)
  3. Activates the app, bringing it to the front
  4. Forces the popover window to become key window
When hiding:
  • Calls performClose(nil) to dismiss the popover
The NSApp.activate(ignoringOtherApps: true) and makeKey() calls ensure the popover appears active and fully saturated even when other apps have focus.

AlwaysActiveHostingController

A custom NSHostingController subclass that prevents content desaturation:

Purpose

By default, AppKit windows become “inactive” when they lose key status, causing:
  • Colors to desaturate (appear washed out)
  • Controls to appear disabled
  • Reduced visual prominence
This controller overrides viewDidAppear() to force the window to remain key, maintaining:
  • Full color saturation
  • Active appearance
  • Proper visual hierarchy
This is particularly important for menu bar apps that should maintain an “active” appearance even when the user interacts with other applications.

Popover Behavior Modes

AppKit provides several popover behaviors: r2Vault uses .applicationDefined for maximum control.

SwiftUI Integration

The popover hosts SwiftUI content via NSHostingController:
The view model is injected into the SwiftUI environment, making it accessible to all child views:

Usage Example

Lifecycle Management

Keep a strong reference to MenuBarManager for the app’s lifetime:
If the manager deallocates, the status item will disappear from the menu bar.
macOS Menu Bar Icon Best Practices:
  • Use SF Symbols when possible (automatic dark mode support)
  • Target size: 22x22 points (44x44 pixels @2x)
  • Use template images (monochrome, system adjusts for light/dark mode)
  • Keep designs simple and recognizable at small sizes
  • Provide accessibility descriptions

Popover Positioning

The popover is positioned relative to the status bar button:
Edge options:
  • .minY - Below (most common for menu bar)
  • .maxY - Above
  • .minX - To the left
  • .maxX - To the right
The system automatically adjusts position if the popover would go off-screen.