Programmatically creating menus with Swift 3 and Cocoa - macos

I am trying my hand at creating a Cocoa GUI app programmatically (i.e. without a nib file) using Swift 3. I've run into trouble getting the application's menus to show.
I would expect the below code to show a File menu items on the menu bar. Instead, while the window launches and works as expected, the code to set the menu seems to have no effect:
import AppKit
final class ApplicationController: NSObject, NSApplicationDelegate {
var mainWindow: NSWindow?
func applicationDidFinishLaunching(_ aNotification: Notification) {
let mainMenu = NSMenu()
let mainMenuFileItem = NSMenuItem(title: "File", action: nil, keyEquivalent: "")
let fileMenu = NSMenu(title: "File")
fileMenu.addItem(withTitle: "New...", action: nil, keyEquivalent: "n")
mainMenuFileItem.submenu = fileMenu
mainMenu.addItem(mainMenuFileItem)
NSApp.mainMenu = mainMenu
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
styleMask: [.titled, .closable, .resizable, .miniaturizable],
backing: NSBackingStoreType.buffered, defer: false)
window.orderFrontRegardless()
window.title = "Hello World"
self.mainWindow = window
NSApp.activate(ignoringOtherApps: true)
}
func applicationWillTerminate(_ aNotification: Notification) {
print("terminating")
}
func applicationShouldTerminateAfterLastWindowClosed(_ app: NSApplication) -> Bool{
return true
}
}
let app = NSApplication.shared()
let controller = ApplicationController()
app.delegate = controller
app.run()
The closest I've found to a working example is this answer. However, it appears to be for an earlier version of Swift/Cocoa, and I am not able to get that example working.
What am I doing wrong?

One needs to call NSApp.setActivationPolicy(.regular) to make the application a "regular" one. Making that call before app.run() fixes the issue of no menu showing.
The "main" part of the code should thus be:
let app = NSApplication.shared()
NSApp.setActivationPolicy(.regular)
let controller = ApplicationController()
app.delegate = controller
app.run()

Related

How to handle close action of window and have single instance of Window when clicked on menu from cocoa app

I created a cocoa app programatically with menu items. When I click on first menu item it should open a window and I want to have only one instance of window that means even I repeatedly click on first menu item it should activate my current window. I also want to handle close button action of window. When I am closing it and again trying to click on first menu item, my app is crashing.
Below is my code n AppDelegate:
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
var statusItem: NSStatusItem!
lazy var window: NSWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 550, height: 300),
styleMask: [.miniaturizable, .closable, .titled],
backing: .buffered, defer: false)
let myViewController = ViewController()
func applicationDidFinishLaunching(_ aNotification: Notification) {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let button = statusItem.button {
button.image = NSImage(systemSymbolName: "1.circle", accessibilityDescription: "1")
}
setupMenus()
}
func setupMenus() {
// 1
let menu = NSMenu()
// 2
let one = NSMenuItem(title: "Open Network Share Url's", action: #selector(didTapOne) , keyEquivalent: "1")
menu.addItem(one)
let two = NSMenuItem(title: "Reconnect", action: #selector(didTapTwo) , keyEquivalent: "2")
menu.addItem(two)
menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
// 3
statusItem.menu = menu
}
private func changeStatusBarButton(number: Int) {
if let button = statusItem.button {
button.image = NSImage(systemSymbolName: "\(number).circle", accessibilityDescription: number.description)
}
}
#objc func didTapOne() {
changeStatusBarButton(number: 1)
window.center()
window.title = "Open Network Share Url's"
window.makeKeyAndOrderFront(nil)
window.contentViewController = myViewController
NSApp.activate(ignoringOtherApps: true)
}
#objc func didTapTwo() {
changeStatusBarButton(number: 2)
}
}
This solved my problem
if window == nil {
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 550, height: 300),
styleMask: [.miniaturizable, .closable, .titled],
backing: .buffered, defer: false)
window?.center()
window?.title = "Open Network Share Url's"
window?.contentViewController = myViewController
window?.isReleasedWhenClosed = false
window?.makeKeyAndOrderFront(self)
} else {
window?.makeKeyAndOrderFront(self)
}

macOS NSPopover menu bar app how to prioritize order app is displayed

I have a macOS menu bar app. I like to know can we prioritize the order that the app shows in the status bar.
//
// AppDelegate.swift
// elmc_midi_deck-macOS
//
// Created by Jerry Seigle on 6/23/22.
//
import Foundation
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var popover: NSPopover!
var window: NSWindow!
var statusBarItem: NSStatusItem!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let jsCodeLocation: URL
#if DEBUG
jsCodeLocation = RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index", fallbackResource:nil)
#else
jsCodeLocation = Bundle.main.url(forResource: "main", withExtension: "jsbundle")!
#endif
let rootView = RCTRootView(bundleURL: jsCodeLocation, moduleName: "elmc_midi_deck", initialProperties: nil, launchOptions: nil)
let rootViewController = NSViewController()
rootViewController.view = rootView
popover = NSPopover()
popover.contentSize = NSSize(width: 400, height: 600)
popover.animates = true
popover.behavior = .transient
popover.contentViewController = rootViewController
statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(60))
if let button = self.statusBarItem.button {
button.action = #selector(togglePopover(_:))
button.title = "elmc_midi_deck"
}
#if DEBUG
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 1, height: 1),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered,
defer: false)
window.contentViewController = rootViewController
window.center()
window.setFrameAutosaveName("ELMC Midi Deck")
window.isReleasedWhenClosed = false
window.makeKeyAndOrderFront(self)
let screen: NSScreen = NSScreen.main!
let midScreenX = screen.frame.midX
let posScreenY = 200
let origin = CGPoint(x: Int(midScreenX), y: posScreenY)
let size = CGSize(width: 400, height: 600)
let frame = NSRect(origin: origin, size: size)
window.setFrame(frame, display: true)
#endif
}
#objc func togglePopover(_ sender: AnyObject?) {
if let button = self.statusBarItem.button {
if self.popover.isShown {
self.popover.performClose(sender)
} else {
self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
self.popover.contentViewController?.view.window?.becomeKey()
}
}
}
}
Currently the app will show at the far left once started. Of course you can command click and drag left to right but I like to make the app show after Apple's default apps like show in the photo
As mentioned in this post, you can use this library to do this. The library allows defining the position for an NSStatusBarItem inside the NSStatusBar. It is written in Objective-C and available via CocoaPods, you probably have to create a bridging header or manually convert it in order to use it in Swift. Refer to this post for details on how to do this.

macOS statusBarApp without Storyboard create and close settingsWindow causing EXC_BAD_ACCESS

I have a MacOS cocoa statusBarApp without any Storyboard with main.swift file.
The statusBarIcon shows up a Menu which presents a custom view with a button, which should open a settingsWindow - which it does. If I close the settingsWindow and reopen it and close it again, I got a EXC_BAD_ACCESS Error. It seems, that the window is deallocate but the reference is still present. I don't know how to fix this.
Edit the question like Willeke´s advice:
Thx, to your answer. Ok, hier is a minimal reproducible example:
create a new Xcode project, with storyboard and swift for macOS app.
Under Project-Infos / General / Deployment Info: Delete the main entry to the storyboard. Then delete the storyboard file itself.
Under Info set the "application is agent" flag to yes, so the app is statusBarApp only.
then you only need the code below.
The Exception Breakpoint leads to this line:
settingsWindow = NSWindow(
To reproduce the error: start the app, click on statusItem, click on menuItem, a window opens, close the window, click again all first steps and reopen the window. sometimes that's the point of crash. sometimes a few more attempts of closing the window are necessary, but not more then three times.
main.swift
import Cocoa
let delegate = AppDelegate()
NSApplication.shared.delegate = delegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
AppDelegate.swift
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
var settingsWindow: NSWindow!
var statusItemMain: NSStatusItem?
var menuMain = NSMenu()
var menuItemMain = NSMenuItem()
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
statusItemMain = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
let itemImage = NSImage(systemSymbolName: "power", accessibilityDescription: nil)
itemImage?.isTemplate = true
statusItemMain?.button?.image = itemImage
menuItemMain.target = self
menuItemMain.isEnabled = true
menuItemMain.action = #selector(createWindow)
menuMain.addItem(menuItemMain)
menuMain.addItem(.separator())
statusItemMain?.menu = menuMain
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
#objc func createWindow() {
settingsWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 750, height: 500),
styleMask: [.miniaturizable, .closable, .resizable, .titled],
backing: .buffered, defer: false)
settingsWindow.center()
settingsWindow.title = "No Storyboard Window"
settingsWindow.makeKeyAndOrderFront(nil)
settingsWindow?.contentViewController = ViewController()
}
}
ViewController.swift
import Cocoa
class ViewController: NSViewController {
override func loadView() {
self.view = NSView(frame: NSRect(x: 0, y: 0, width: 750, height: 500))
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
NSWindow is released when it is closed. Before ARC this was a usefull feature. It can be switched off by setting the isReleasedWhenClosed property to false. But then the window stays in memory when it is closed because the settingsWindow property is holding on to it. Implement delegate method windowWillClose and set settingsWindow to nil so window is released.
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
var settingsWindow: NSWindow!
// other methods
#objc func createWindow() {
settingsWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 750, height: 500),
styleMask: [.miniaturizable, .closable, .resizable, .titled],
backing: .buffered, defer: false)
settingsWindow.isReleasedWhenClosed = false
settingsWindow.delegate = self
settingsWindow.center()
settingsWindow.title = "No Storyboard Window"
settingsWindow?.contentViewController = ViewController()
settingsWindow.makeKeyAndOrderFront(nil)
}
func windowWillClose(_ notification: Notification) {
settingsWindow = nil
}
}

Open and bring new window to front when pressing NSMenuItem

I have a macOS menu application that is running as an agent (LSUIElement).
I need it to have a companion settings window. There is a "settings" NSMenuItem in the NSMenu, and the requirement is to open an actual window and bring it to the front when pressed.
The window is SwiftUI driven. Here's how it's working:
// main is an NSMenu
main.addItem(
withTitle: "Settings",
action: #selector(AppDelegate.openSettings),
keyEquivalent: "")
#objc func openSettings() {
let detailView = SettingsWindow(); // Swift UI view
let controller = DetailWindowController(rootView: detailView) // See below
controller.window?.title = "Settings";
controller.showWindow(nil)
NSApp.activate(ignoringOtherApps: true)
}
class DetailWindowController<RootView : View>: NSWindowController {
convenience init(rootView: RootView) {
let hostingController = NSHostingController(rootView: rootView.frame(width: 400, height: 500))
let window = NSWindow(contentViewController: hostingController)
window.setContentSize(NSSize(width: 400, height: 500))
self.init(window: window)
}
}
What actually happens
The current behaviour is that the window opens, however it's always behind whatever other windows are currently in the foreground.
I need it to be in the foreground.
NSApp.activate(ignoringOtherApps: true) in the code above is an attempt to achieve this, but that didn't work.
Any help would be amazing. Many thanks.
Figured it out in the end. I needed these 3. The activation policy should be better managed, but for what it's worth here's what works.
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
settingsWindow!.window?.orderFrontRegardless()
In context
#objc func openSettings() {
if(settingsWindow == nil) {
let detailView = ActionWindow();
settingsWindow = DetailWindowController(rootView: detailView)
settingsWindow!.window?.title = "Cloud Brains - Settings";
settingsWindow!.showWindow(nil)
}
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
settingsWindow!.window?.orderFrontRegardless()
}

OS X storyboard: how to show a window programmatically?

I am creating an OS X status bar application.
I am trying to achieve the following:
app starts invisible, with menu bar item
click on menu bar item shows the main window
on deactivate, the window is hidden
So I am trying to programmatically show the main window when the menu item is clicked, but with no success.
My main window has "Hide on deactivate" checked. Once hidden, I cannot make it visible again using code.
Here is the code I have for now, but it doesn't work:
#IBAction func menuClick(sender: AnyObject) {
var mainWindow = NSStoryboard(name: "Main", bundle: nil)?.instantiateInitialController()
mainWindow?.makeKeyAndOrderFront(self)
}
This is how you have to do to show your Windows programmatically:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
let mainWindow = NSWindow(contentRect: NSMakeRect(0, 0, NSScreen.mainScreen()!.frame.width/2, NSScreen.mainScreen()!.frame.height/2), styleMask: NSTitledWindowMask|NSResizableWindowMask|NSMiniaturizableWindowMask|NSClosableWindowMask, backing: NSBackingStoreType.Buffered, defer: false)
func createNewWindow(){
mainWindow.title = "Main Window"
mainWindow.opaque = false
mainWindow.center()
mainWindow.hidesOnDeactivate = true
mainWindow.movableByWindowBackground = true
mainWindow.backgroundColor = NSColor(calibratedHue: 0, saturation: 0, brightness: 1, alpha: 1)
mainWindow.makeKeyAndOrderFront(nil)
}
func applicationDidFinishLaunching(aNotification: NSNotification) {
// lets get rid of the main window just closing it as soon as the app launches
NSApplication.sharedApplication().windows.first!.close()
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
#IBAction func menuClick(sender: AnyObject) {
createNewWindow()
}
}
or you can create an optional NSWindow var to store your window before you close it as follow
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var defaultWindow:NSWindow?
func applicationDidFinishLaunching(aNotification: NSNotification) {
// lets get rid of the main window just closing it as soon as the app launches
defaultWindow = NSApplication.sharedApplication().windows.first as? NSWindow
if let defaultWindow = defaultWindow {
defaultWindow.close()
}
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
#IBAction func menuClick(sender: AnyObject) {
if let defaultWindow = defaultWindow {
defaultWindow.makeKeyAndOrderFront(nil)
}
}
}
The makeKeyAndOrderFront method is a NSWindow method, but instantiateInitialController returns the window controller, not its window.
Also, if the window is hidden on deactivate, you wouldn't want to instantiate another copy. Keep a reference to the window and re-show that.
Finally, you may need to bring the app to the front too. Call [NSApp activateIgnoringOtherApps:YES] (or the Swift equivalent).

Resources