macOS app StatusBar icon only appears when Xcode is focussed - swift

I'm building a macOS app with a StatusBar icon in Xcode (13.2, Swift 5) for personal use.
When I build in the dev environment it works as expected and the StatusBar icon appears.
To create the production app I go to Product > Archive > Distribute App > Copy App, and it creates a .app file with no errors (this method works for other apps without signing).
When I run the .app file, I see the app running in my processes, but no StatusBar icon appears.
Then if I switch to Xcode while the app is running, the StatusBar icon for the production app shows up. I know it's the distribution app because if I run the development build again I see two icons.
And if I kill the process one goes away.
Relevant code -
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength)
let popover = NSPopover()
var eventMonitor: EventMonitor?
func applicationDidFinishLaunching(_ aNotification: Notification) {
if let button = self.statusItem.button {
button.image = NSImage(named: NSImage.Name("MenuBarIcons"))
button.action = #selector(AppDelegate.togglePopover(_:))
}
self.popover.contentViewController = ViewController.newInstance()
self.popover.animates = false
self.eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in
if let strongSelf = self, strongSelf.popover.isShown {
strongSelf.closePopover(sender: event)
}
}
}
Questions are:
Why does the distribution app StatusBar icon only appear when I'm focused on Xcode?
What can I do to get the icon to always show up?

Related

Open instance of another Mac app in a subview in Swift?

I I'm trying to open the photos app inside a subview in a macOS app. I have the following code that lets me open the Photos app using NSWorkspace but I wish to know how I can open this app inside a subview in another Swift app rather than opening the app in its original form.
import Cocoa
func openPhotos() -> Bool {
if let photosApp = FileManager.default.urls(
for: .applicationDirectory,
in: .systemDomainMask
).first?.appendingPathComponent("Photos.app") {
return NSWorkspace.shared.open(photosApp)
}
return false
}

Request key for TextField in macOS status bar app

I am writing a little status bar app, which includes a text input.
The app is added to the status bar as a NSMenuItem which holds a NSHostingViewController with the SwiftUI-View. The status bar menu is added through an App delegate inside SwiftUIs #main scene. I've set "Application is agent" in the info.plist to true and I have an empty scene as #main in SwiftUI.
Problem: When I try to edit text, in the status bar app, the text field is not clickable and does not receive text input. When I add a window, the text field in the status bar app works as expected as long as the window is in foreground.
From my understanding, this is caused by the text field not being inside a window marked as key window. Is there any workaround to make the text field work as expected without the need for an additional app window?
Minimal example for the app delegate:
class AppDelegate: NSObject, NSApplicationDelegate {
private var statusItem: NSStatusItem?
func applicationDidFinishLaunching(_ notification: Notification) {
self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let statusBarButton = statusItem?.button {
statusBarButton.image = NSImage(systemSymbolName: "6.circle", accessibilityDescription: "")
statusBarButton.image?.isTemplate = true
}
let menuItem = NSMenuItem()
menuItem.view = createView()
let menu = NSMenu()
menu.addItem(menuItem)
statusItem?.menu = menu
}
private func createView() -> NSView {
let view = HostingView(rootView: ContentView())
view.frame = NSRect(x: 0, y: 0, width: 520, height: 260)
return view
}
}
SwiftUI #main
import SwiftUI
import AppKit
#main
struct MacApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
Settings {
EmptyView()
}
}
}
Edit: I figured out, that there are two windows registered for my app. One for the status bar icon and one for the menu. However, the code below does not result in the menu window to become the key window. (although it is in fact the only window which returns true for .canBecomeKey)
.onAppear {
let window = NSApp.windows.first { $0.canBecomeKey }
window?.makeKey()
}
Second Edit: Apparently, the problem is not, that the window is not able to become key, but that the app is not set as the active app when the status bar menu is opened. I came up with this rather ugly hack, which causes the menu to close and re-open when another app was in focus before, but at least buttons and textfields work as expected with this:
func applicationDidFinishLaunching(_ notification: Notification) {
...
// Also let AppDelegate implement NSMenuDelegate
menu.delegate = self
...
}
func menuWillOpen(_ menu: NSMenu) {
#warning("This is a hack to activate the app when the menu is shown. This will cause the menu to close and re-open when another window was focused before.")
if !NSApp.isActive {
NSApp.activate(ignoringOtherApps: true)
DispatchQueue.main.async {
self.statusItem?.button?.performClick(nil)
}
}
}
I am still looking for a proper solution.

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()
}
}
}

Set menubar icon from ViewController

How to change the menubar icon of a MacOS app from another ViewController?
AppDelegate.swift (inits menubar icon)
ViewController.swift (tries to set menubar icon ❌)
I found this but this isn't changing the menubar icon for me:
Mac: How to save alternate app icon in dock OSX xcode
let image = NSImage.init(named: NSImage.Name(rawValue: "AltAppIcon"))
NSApp.applicationIconImage = image
See how the BOINC icon has the little custom pause symbol/badge in the bottom right of it's menubar? This app's icon changes. Are they writing over the name of that file and changing it to the "paused icon" image maybe?
✅UPDATE*
A AppDelegate.swift function that set the menubar icon worked:
AppDelegate.swift
func setIcon() {
let onIcon = NSImage(named: "fv-mini-icon-green")
statusItem.button?.image = onIcon
}
ViewController.swift
func taskOnIcon() {
DispatchQueue.main.async(execute: {
let appDele = NSApplication.shared.delegate as! AppDelegate
appDele.setIcon()
})
}
Here is a way...
class AppDelegate: NSObject, NSApplicationDelegate {
var statusBarItem: NSStatusItem!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let statusBar = NSStatusBar.system
statusBarItem = statusBar.statusItem(withLength: 16)
let button = statusBarItem.button
button?.image = NSImage(named: "fv-mini-icon-green")
// .. other code here

How to make sure that a new view controller is visible to the user?

First of all the native macOS application is made into an accessory type application 3 seconds after launching (to show an info screen first, before the app goes into the system menu bar):
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
NSApplication.shared.setActivationPolicy(.accessory)
}
It has a status bar menu created with:
class func createMenu(color: Bool) -> Void {
let statusBar = NSStatusBar.system
self.sharedInstance.storedStatusItem = statusBar.statusItem(withLength: NSStatusItem.variableLength)
self.sharedInstance.storedStatusItem.menu = ABCMenu.statusBarMenu()
}
The user has several options in the status bar menu to open different screens with controls. One of them is shown with:
class func showServiceViewController() -> Void {
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: Bundle.main)
guard let vc = storyboard.instantiateController(withIdentifier: "ABCServiceViewController") as? ABCServiceViewController, let checkedWindow = ABCUIManager.sharedInstance.window else {
return
}
checkedWindow.contentViewController = vc
checkedWindow.setIsVisible(true)
checkedWindow.orderFrontRegardless()
}
The problem is that sometimes the selected view controller is not brought to the background and has to be found underneath many other already opened applications and windows. Most of the time it works fine, but not always.
Are there any better ways to assure that the new view controller is always brought up to the highest level and shown to the user?
Thank you for any suggestions.
I would activate application before showing window (as you have accessory application it is not activated by default and must be done programmatically)
...
NSApplication.shared.activate(ignoringOtherApps: true)
checkedWindow.contentViewController = vc
checkedWindow.setIsVisible(true)
checkedWindow.orderFrontRegardless()
}