How do you create a Status Bar icon on macOS with SwiftUI? - swift

I am trying to create an application that starts with a Status Bar icon, and without a application window. It is for a macOS utility that will be invoked from menus displayed from the Status Bar icon.
Can this be done entirely within SwiftUI, or must I use AppKit?
This is what it looks like in AppKit / Cocoa
theItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
let theStatusButton = theItem!.button
theStatusButton?.title = "MyApp"
let menuItemOne = NSMenuItem(title: "Hey MyApp", action: nil, keyEquivalent: "")
statusMenu = NSMenu(title: "This to do in MyApp")
statusMenu!.addItem(menuItemOne)
theItem!.menu = statusMenu!
Thanks in advance for any help.

macOS 13.0+, Xcode 14.0+
Use SwiftUI's MenuBarExtra structure to create a Status Bar icon and menu on macOS Ventura.
struct MenuBarExtra<Label, Content> where Label : View, Content : View
Look at my post for details.

Related

How to Make macOS App Window Hidden When Closed and Reopened With Menu Bar Item?

I am developing a macOS app (using Swift & Storyboard) which window behaves like the Adobe Creative Cloud app. And I could not find the optimal solution after hours of research.
This means:
When the app launches, the main window shows up with various menus on the status bar, an icon appears in the dock, and an icon appears in the status bar.
When the user clicks the red X, the main window and the icon in the dock are hidden.
The main app window can be reopened by clicking the status bar icon. And the dock icon reappears.
My storyboard looks like this:
I have tried the following:
By setting Application is agent (UIElement) to YES, I was able to close the main app window while keeping the app alive. However, the app icon does not show up in the dock, and there are no menus in the left side of the status bar.
I was able to launch a new app window by clicking the status bar icon. But doing so simply opens a whole new window regardless of whether a window is already being presented (I only want one window to show up).
let storyboard = NSStoryboard(name: "Main", bundle: nil)
guard let window = storyboard.instantiateController(withIdentifier: .init(stringLiteral: "main")) as? WindowController else { return }
window.showWindow(self)
Much appreciation for anyone who can help!
Don't use the Application is agent approach, but change the activationPolicy of the NSApp.
To dynamically hide the icon after closing the (last) window use this in your AppDelegate:
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
NSApp.setActivationPolicy(.accessory)
return false
}
And use something simular to this to initialise your menubar icon and activate the window including a dock icon:
class ViewController: NSViewController {
var status: NSStatusItem?
override func viewDidLoad() {
super.viewDidLoad()
status = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
status?.button?.title = "Test"
status?.button?.action = #selector(activateWindow(_:))
status?.button?.target = self
}
#IBAction func activateWindow(_ sender: AnyObject) {
NSApp.setActivationPolicy(.regular)
DispatchQueue.main.async {
NSApp.windows.first?.orderFrontRegardless()
}
}
}

Swift - detect is NSStatusItem hidden

I'm working on MacOS menu bar app which can have variable length:
#IBOutlet weak var statusMenu: NSMenu!
let statusItem: NSStatusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem.title = "Long text..."
statusItem.menu = statusMenu
If the status item title length will be to long to display and the menu bar item will hide I'd like to change the title to the shortened version. So I need:
check if statusItem is visible
if no, check how much free space I have to adjust the shortening
change the title
Point 3 is easy. I have problems with point 1 and 2. None of the stackoverflow answers work for me. Is there any way how this can be done?

How do you show a NSStatusBar item AND hide the dock icon?

I've tried setting "Application is Agent" to 1 but the status bar item disappears. When set to 0 both the status bar item and dock icon are showing. How do I show the status bar item but hide the dock icon?
I've tried the following in both awakeFromNib() and applicationDidFinishLaunching() in AppDelegate.swift:
//class scope
let statusItem = NSStatusBar.system().statusItem(withLength: NSVariableStatusItemLength)
//function scope
self.statusItem.image = NSImage(named: "myImage")
let menu = NSMenu(title: "MyApp-Menu")
let menuItem = NSMenuItem(title: "title", action: nil, keyEquivalent: "")
menu.addItem(menuItem)
self.statusItem.menu = menu
Updated for Mac OS 10.14
in AppDelegate:
let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength)
func applicationDidFinishLaunching(_ aNotification: Notification) {
if let button = statusItem.button {
button.image = NSImage(named:NSImage.Name("StatusBarButtonImage"))
button.action = #selector(launchFromTray)
}
constructMenu()
}
func constructMenu() {
let menu = NSMenu()
menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Show", action: #selector(launchFromTray), keyEquivalent: "w"))
menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
statusItem.menu = menu
}
And you should set the Application is agent (UIElement) to YES now. Should work fine, hope this will help someone
You want hide the dock icon, you must set "Application is Agent" to "YES" or you can set app to UIElement Application:
ProcessSerialNumber psn = { 0, kCurrentProcess };
OSStatus rt = TransformProcessType(&psn, kProcessTransformToUIElementApplication);
According to the parts of codes you're offered I think that you say "the status bar item disappears" is not really disappear, it is just the menu disappears. The status bar can't disappear unless you write the wrong code, but your codes look fine.
If what I think is right, the menu disappears because the app is an agent app. What you need to do is just make it be a UI Element app again:
ProcessSerialNumber psn = { 0, kCurrentProcess };
OSStatus returnCode = TransformProcessType(&psn, kProcessTransformToForegroundApplication);
You can't show a menu bar when you run an agent app.Only one can be selected between the agent app and the menu bar.
Agents include background-only applications, faceless background-only applications, and UI elements, but is not a full blown application with a menu bar

How to show two row of text in a menu bar app in mac os

can you explain to me how to make an app on osx (with swift 3) showing only (without icon on the dock) two row of text in the menu bar like in the picture below ? or at least how to show text on the menu bar ?
OSX Menu bar picture:
PS: I am new on osx app dev so don't hesitate to be very verbose ;)
This kind of app is called menu bar app or menulet.
The dockless appearance is set by the key LSUIElement (1) in Info.plist
Use a custom NSView
Assign the view as a subview to the view of the button of the NSStatusItem
Draw the text in drawRect
Important: Using a custom view requires to implement all methods to handle the highlighting, mouse clicks and NSMenu delegate.
Here is an article how to do it, it's in Objective-C but this might be a starting point:
Adding a Custom View to an NSStatusItem
var statusBarItem: NSStatusItem?
func applicationDidFinishLaunching(_ notification: Notification) {
statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let button = self.statusBarItem?.button {
let style = NSMutableParagraphStyle()
style.maximumLineHeight = 10
style.alignment = NSTextAlignment.left
let attributes = [NSAttributedString.Key.paragraphStyle: style, NSAttributedString.Key.font: NSFont.systemFont(ofSize: 10.0), NSAttributedString.Key.baselineOffset: -5] as [NSAttributedString.Key : Any]
let textString = "Line1\nLine2"
let attributedTitle = NSAttributedString(string: textString, attributes: attributes)
button.attributedTitle = attributedTitle
}
}

OS X Application Pop Up Menu

I have a custom image button. I want to display a custom menu when clicked on it.
I am using
settingMenu.popUpMenuPositioningItem(settingMenu.itemAtIndex(0), atLocation: NSEvent.mouseLocation(), inView: nil )
I created a menu and created an outlet for it. Still I am not able to see the menu
Any Suggestions?
In AppDelegate.swift:
let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-2)
if let button = statusItem.button {
button.image = NSImage(named: "ButtonImageHere")
button.action = Selector("actionForClickingButtonHere:")
}
func actionForClickingButtonHere(sender: AnyObject) {
//Present view, show menu list, whatever
}
If you want to hide the dock icon do this in Info.plist:
For full example see this tutorial.