Ordering Menu Bar Status Items When AppKit Won't Let You
Jake MarshIn this post
Multimeter puts one status item in the menu bar for each meter you add. In its Settings window you can drag meters into whatever order you like, and the menu bar is supposed to follow. That turned out to need a small trick, because AppKit gives you no way to move a status item once it exists.
What NSStatusBar Gives You#
The whole of NSStatusBar’s item API is two methods:
func statusItem(withLength length: CGFloat) -> NSStatusItem
func removeStatusItem(_ item: NSStatusItem)NSStatusItem has properties like length, menu, button, isVisible and autosaveName, and none of them is an index or a position. There’s no “insert after this one” either. You create an item and macOS decides where it goes.
Where it goes is to the left of the items already there. I checked with a tiny test app on macOS 27 that creates three items titled A, B and C, in that order. The menu bar reads C B A. Remove B and create it again, and it lands on the far left: B C A.
Rebuild In Reverse#
So the order you create items in is the only lever you have. If the items should read A B C, create C first and A last. Every time the order changes, Multimeter throws the whole row away and builds it again backwards:
private func updateStatusItems() {
let resolved = meterManager.resolvedActiveMeters
let layout = resolved.map { "\($0.meter.id)|\($0.template.id)" }
if layout != currentLayout {
currentLayout = layout
for item in statusItems.values {
NSStatusBar.system.removeStatusItem(item)
}
statusItems.removeAll()
meterViews.removeAll()
meterConfigs.removeAll()
for entry in resolved.reversed() {
createStatusItem(for: entry.meter, template: entry.template)
}
}
for entry in resolved {
let width = entry.meter.customWidth ?? entry.template.width
if let item = statusItems[entry.meter.id], item.length != width { item.length = width }
let config = entry.meter.effectiveConfig(for: entry.template)
if meterConfigs[entry.meter.id] != config {
meterConfigs[entry.meter.id] = config
meterViews[entry.meter.id]?.setSnapshots(meterManager.snapshots(for: entry.meter))
}
}
}The layout key is each meter’s id plus its face’s id, so adding, removing or reordering meters triggers a rebuild, and so does switching a meter to a different face (that needs a new view anyway). Everything else goes through the second loop.
Update In Place Otherwise#
Most changes aren’t order changes. Someone gives a meter a new width or a new color, and that shouldn’t make every item in the row disappear and come back. length is settable on a live item, so a new width is one assignment. Settings changes get compared against the last config the item was built with and pushed into the existing view only when they differ. A rebuild only happens when the sequence of meters and faces changes.
What autosaveName Does#
Here’s what the AppKit header says about it:
Specifies a unique name for persisting visibility information. If none is specified, one is automatically chosen. Apps with multiple status bar items should set an autosave after creation.
In practice it also keys the item’s position. On macOS 27, positions live in ~/Library/Group Containers/com.apple.MenuBar/Library/Preferences/com.apple.MenuBar.plist, under TrailingItemPreferredPositions, with keys like status:com.greatwork.Multimeter::Item-0. The number stored for each one grows the further left the item sits.
The automatic names are Item-0, Item-1 and so on, handed out in creation order. Remove an item and create a new one, and the names can repeat: in the remove-B test above, the new B got Item-2, the same name C already had.
Three things I saw in the test app:
- Positions get recorded without anyone dragging anything. A fresh app that only created and rebuilt its items had an entry for each one afterward.
- On a later launch, a recorded position beats creation order. With names
meter-A,meter-Bandmeter-C, a later launch created A, B, C in that order and they came outA B C, right where they’d been last time, instead of theC B Ayou’d get from a fresh app. - Cmd-dragging an item rewrites its recorded position. I Cmd-dragged A from the right end to the left end, quit and relaunched: still on the left. Rebuilding the row in a different creation order didn’t move it either.
Why The Automatic Names Fit A Rebuild#
Put those together and a stable per-meter autosaveName works against the rebuild trick. Once each meter has a recorded position under its own name, the next launch puts it back there no matter what order you create things in, and the order in Settings stops mattering.
With the automatic names, the recorded positions belong to slots. The first item created is always Item-0, and after a rebuild that’s always the last meter in Settings, which sits on the right. In the two relaunches I ran with no dragging, the rebuilt row came out in exactly the order the code asked for.
The trade-off is what a Cmd-drag means. It moves a slot. I dragged Item-0 (A, on the right) to the far left, then rebuilt as C, B, A. C was created first, got Item-0, and appeared on the far left: C A B. From then on, whichever meter is last in Settings shows up wherever the person dragged that slot.
If your items are separate things the person arranges by hand, give each one a stable autosaveName right after you create it, as the header suggests, and a Cmd-drag will stick to that item across launches. If your app owns the order and has its own UI for changing it, leave autosaveName alone, create the items in reverse, and change length and content in place for everything else.