Swift Protip: Hiding Your App's Icon From the Dock Properly
How to not have duplicate icons all over the place
I’ve been working on this feature request for Cork: Hide dock icon if shown in menu bar. Basically, what it boils down to:
If Cork has an item in the menu bar, and the last window is closed, remove it from the Dock so it’s not useless there.
Restore the icon in the Dock when Cork is opened again
If Cork doesn’t have an item in the menu and the last window is closed, kill the app
TL;DR
class AppDelegate: NSObject, NSApplicationDelegate
{
func applicationWillBecomeActive(_ notification: Notification)
{
NSApp.setActivationPolicy(.regular)
}
func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool
{
NSApp.setActivationPolicy(.accessory)
return false
}
}
The Problem
Cork can have an optional item in the Menu Bar, the presence of which is controlled by a variable @AppStorage("showInMenuBar") var showInMenuBar = false
This variable is then modified in the Settings:
LabeledContent
{
Toggle(isOn: $showInMenuBar)
{
Text("settings.general.menubar.toggle")
}
} label: {
Text("settings.general.menubar")
}
And then the Menu Bar Extra is shown depending on whether this variable is true or false:
MenuBarExtra("app-name", (…), isInserted: $showInMenuBar)
{
…
}
Until now, when the Menu Bar item was enabled, Cork remained in the Dock and did pretty much nothing there, since you should reactivate Cork by using a button in the Menu Bar item.
The Solution
First Attempts
I went through multiple ideas. I tried .onDisappear
on ContentView()
, but in true Apple fashion, this doesn’t do anything on macOS. Then went to using NotificationCenter to get the notification about the window closing (check out that sweet use of structured concurrency to react to NotificationCenter):
.task
{
for await _ in NotificationCenter.default.notifications(named: .init(NSWindow.willCloseNotification.rawValue))
{
let windowNumbers = NSApplication.shared.windows.filter
{ window in
return window.title != String(localized: "app-name")
}.count
print(windowNumbers)
}
}
Unfortunately, this solution didn’t work too well, since it gave me pretty much random numbers each time a window was closed AND opened (for some reason), so this wasn’t the way. Also, it was giving me concurrency warnings because I’m a masochist that has strict concurrency checking enabled.
Getting Warm…
I completely randomly started wondering… applicationShouldTerminateAfterLastWindowClosed
controls when the last window closes, so it must know when the last window closes somehow. What if this function gets triggered when the last window closes? And this turned out to be the case.
I ended up with this code that works perfectly:
func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool
{
if showInMenuBar
{
NSApp.setActivationPolicy(.accessory)
return false
}
else
{
NSApp.setActivationPolicy(.regular)
return true
}
}
It does exactly what I need it to! When the last window closes, check if the Menu Bar item is shown. If it is, use NSApp.setActivationPolicy(.accessory)
to remove the icon from the Dock, and prevent the app from terminating; otherwise, keep the item in the menu bar through NSApp.setActivationPolicy(.regular)
and terminate the app.
One Last Problem: Re-Adding the Dock Icon When the User Reactivates Cork
That was all fine and good, but now, when Cork got reactivated, it didn’t have a Dock icon, or a Menu Bar.
This was, luckily, much easier to figure out: Use applicationWillBecomeActive
to re-add the icon to Dock:
func applicationWillBecomeActive(_ notification: Notification)
{
NSApp.setActivationPolicy(.regular)
}
The Complete Solution
class AppDelegate: NSObject, NSApplicationDelegate
{
@AppStorage("showInMenuBar") var showInMenuBar = false
func applicationWillBecomeActive(_ notification: Notification) {
NSApp.setActivationPolicy(.regular)
}
func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool
{
if showInMenuBar
{
NSApp.setActivationPolicy(.accessory)
return false
}
else
{
NSApp.setActivationPolicy(.regular)
return true
}
}
}
And that’s it for today! Follow me on Mastodon to get the latest Cork, Swift and SwiftUI on the Mac news.