Swift+Macos Auto close popover window when focus is lost - swift

I am writing a small app that display an icon in the menu bar with a popup view.
I followed https://github.com/twostraws/SwiftOnSundays/tree/master/013%20TextTransformer as an example.
One difference I am seeing in my test app is the popover view controller is not getting dismissed when view loses focus.
this is my app delegate,
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
let popupOver = NSPopover()
#IBOutlet weak var menu: NSMenu!
func applicationDidFinishLaunching(_ aNotification: Notification) {
statusItem.button?.title = "FT";
statusItem.button?.target = self;
statusItem.button?.action = #selector(showMenu)
let storyBoard = NSStoryboard(name: "Main", bundle: nil)
guard let viewController = storyBoard.instantiateController(withIdentifier: "FTViewController") as? ViewController else {
fatalError("Unable to find 'FTViewController'")
}
popupOver.contentViewController = viewController
popupOver.behavior = .semitransient
}
func applicationDidResignActive(_ notification: Notification) {
popupOver.close()
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
#objc func showMenu() {
if(popupOver.isShown) {
popupOver.close()
}
else {
guard let statusButton = statusItem.button else {
fatalError("Unable to find status button")
}
popupOver.show(relativeTo: statusButton.bounds, of: statusButton, preferredEdge: .maxY)
}
}
}
I tried adding applicationDidResignActive but that's getting triggered only when the application loses focus so if i directly click the menu bar item and click else where on screen I am not getting that event. The sampleapp i referenced doesnt seem to hookup for these events but still works as expected.
I am just starting swift ui programming so couldnt figure out what I am missing here.

Not sure if you're still looking for a solution here, but I've just got stuck with the same problem and I found this example which seems to do the job. Hope this helps.

Related

How to reopen an macos App using menubar button

I created an app with a menubar button using swift and storyboard, when I click the close button the window closed and the menubar button still sits on the menubar, that's good. What I want to do next is reopen the window by clicking the menubar button. After searching I figured out I can use this code to bring it to the front, but it only works when the window is still open. How can I bring it back when it is closed or miniatured?
NSApplication.shared.activate(ignoringOtherApps: true)
This the appDelegate
class AppDelegate: NSObject, NSApplicationDelegate {
private var statusItem: NSStatusItem!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
statusItem = NSStatusBar.system.statusItem(withLength: 16.0)
if let button = statusItem.button {
button.image = NSImage(named: "remote-control")
button.image?.size = NSSize(width: 16.0, height: 16.0)
button.image?.isTemplate = true
button.action = #selector(bringToFront(sender:))
}
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
print("terminate")
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
#objc func bringToFront(sender: AnyObject?) {
NSApplication.shared.activate(ignoringOtherApps: true)
NSApp.windows.last?.makeKeyAndOrderFront(nil)
}}
This is the windowcontroller
class MainWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
// Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
window?.title = ""
let styleMask: NSWindow.StyleMask = [.closable, .titled, .miniaturizable]
window?.styleMask = styleMask
}}
Thanks
I got it.
In appdelegate use deminiaturize function does exactly what I want.
for window in NSApp.windows {
window.deminiaturize(nil)
}
Since I only have one window but NSApp.windows has two members, I think I can deminiatureize all the windows.

The initial viewcontroller/windowcontroller/window remains retained after closing window—how do I deinitalize it?

I have an macOS app that opens up multiple tabs. When a new tab is opened, it's retained by the app delegate:
class AppDelegate: NSObject, NSApplicationDelegate {
var tabControllers:[WindowController] = []
}
class WindowController: NSWindowController, NSWindowDelegate {
override func windowDidLoad() {
super.windowDidLoad()
(NSApp.delegate as? AppDelegate)?.tabControllers.append(self)
self.window?.delegate = self
}
#IBAction override func newWindowForTab(_ sender: Any?) {
if let wc = NSStoryboard.main?.instantiateInitialController() as? WindowController,
let window = wc.window {
self.window?.addTabbedWindow(window, ordered: .below)
window.order(.above, relativeTo: self.window.hashValue)
}
}
func windowWillClose(_ notification: Notification) {
if let window = notification.object as? NSWindow,
let wc = window.windowController as? WindowController,
let index = tabControllers.firstIndex(of: wc) {
(NSApp.delegate as? AppDelegate)?.tabControllers.remove(at: index)
}
}
}
For all newly created tabs, this code ensures when the new tab is open, there's a strong reference that keeps it alive. When the user goes to close the tab, it removes the strong reference, which deinitializes the object. Works great except for the initial windowcontroller that the app starts with. If the user adds a new tab and closes the first window/tab, all the above code is called, but the initial window is not deinitialized.
How do I deinitialize that first window/tab?

Xcode NSStatusBar Item not appearing

I'm trying to add an item to the status bar, but when I launch the app the item only appears in the top left for a split second and then quickly disappears.
I've looked through the documentation and I can see things have changed recently e.g. statusItem.title has become statusItem.button?.title. But don't seem to be missing anything else. Any help?
Here's my code:
var statusItem : NSStatusItem? = nil
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem.button?.title = "Connect!"
}
Ah brilliant. That's worked! Thanks Saleh. After playing around with both our codes, mine seemed to work with the var declaration at the top and without NSMenuDelegate instance. My issue seems to be that I was saying :
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
All I had to do to make it work was remove the 'let' and just say:
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
AppDelegate should be an instance of NSMenuDelegate
Define the statusItem at creation
Setup the button title in the applicationDidFinishLaunching callback
class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.variableLength)
func applicationDidFinishLaunching(_ aNotification: Notification) {
if let button = statusItem.button {
//button.image = NSImage(named:NSImage.Name("StatusBarButtonImage"))
button.title = "connect"
//button.action = #selector(doSomething(_:))
}
}

What is the correct way to close a popover?

In my NSDocument subclass I instantiate an NSPopover, with .semitransient behaviour, and show it:
popover.show(relativeTo: rect, of: sender, preferredEdge: .maxX)
popover is declared locally. A button method in the popover controller calls:
view.window?.close()
The popover closes, but I have become aware that it remains in memory, deinit() is never called and the NSApp.windows count increases, whereas if I dismiss it by pressing escape or clicking outside it, deinit is called and the windows count doesn't increase.
If I set the window's .isReleasedWhenClosed to true, the windows count doesn't increase, but deinit is still not called.
(Swift 3, Xcode 8)
You have to call performClose (or close) on the popover, not the window.
Thanks -DrummerB for your interest. It has taken me some time to get around to making a simple test application I might send you, and of course it wasn't a document-based one as mine was, and that seemed to be clouding the issue. My way of opening the popover was based on an example I'd recently read, but can't now find or I'd warn people. It went like this:
let popover = NSPopover
let controller = MyPopover(...)! // my convenience init for NSViewController descendant
popover.controller = controller
popover.behaviour = .semitransient // and setting other properties
popover.show(relativeTo: rect, of: sender, preferredEdge: .maxX)
Here's the improved way I've come across:
let controller = MyPopover(...)! // descendant of NSViewController
controller.presentViewController(controller,
asPopoverRelativeTo: rect, of: sender, preferredEdge: .maxX,
behavior: .semitransient) // sender was a NSTable
In the view controller, the 'Done' button's action simply does:
dismissViewController(self)
which never worked before. And now I find the app's windows list doesn't grow, and the controller's deinit happens reliably.
I would suggest doing the following:
Define a protocol like this
protocol PopoverManager {
func dismissPopover(_ sender: Any)
}
In your popoverViewController (in this example we are displaying a filter view controller as a popover) add a variable for the popoverManager like this
/// Filter shown as a NSPopover()
class FilterViewController: NSViewController {
// Delegate
var popoverManager: PopoverManager?
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
}
// Bind this to the close button or action on your popover view controller
#IBAction func closeAction(_ sender: Any) {
self.popoverManager?.dismissPopover(sender)
}
...
}
Now in your viewController that you show the popover from add an extension like this
extension MainViewController: NSPopoverDelegate, PopoverManager {
#IBAction func setFilter(_ sender: AnyObject) {
self.showFilterPopover(sender)
}
func showFilterPopover(_ sender: AnyObject) {
let storyboard = NSStoryboard(name: "Filter", bundle: nil)
guard let controller = storyboard.instantiateController(withIdentifier: "FilterViewController") as? FilterViewController else {
return
}
// Set the delegate to self so we can dismiss the popover from the popover view controller itself.
controller.popoverManager = self
self.popover = NSPopover()
self.popover.delegate = self
self.popover.contentViewController = controller
self.popover.contentSize = controller.view.frame.size
self.popover.behavior = .applicationDefined
self.popover.animates = true
self.popover.show(relativeTo: sender.bounds, of: sender as! NSView, preferredEdge: NSRectEdge.maxY)
}
func dismissPopover(_ sender: Any) {
self.popover?.performClose(sender)
// If you don't want to reuse it
self.popover = nil
}
}

Adding Items to the Dock Menu from my View Controller in my Cocoa App

I have implemented a dock menu in my Mac app via the Application delegate method:
func applicationDockMenu(sender: NSApplication) -> NSMenu? {
let newMenu = NSMenu(title: "MyMenu")
let newMenuItem = NSMenuItem(title: "Common Items", action: "selectDockMenuItem:", keyEquivalent: "")
newMenuItem.tag = 1
newMenu.addItem(newMenuItem)
return newMenu
Is there a way I can add items to the menu from within my View Controller - I can't seem to find a method in my NSApplication object. Is there another place I should look?
Since applicationDockMenu: is a delegate method, having an instance method add menu items would conflict with the delegate return.
What you could do is make the dock menu a property/instance variable in your application delegate class. This way, your view controller could modify the menu either by passing the reference to the menu from your application delegate to your view controller (which you would have a dockMenu property) or referencing it globally (less recommended).
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
var dockMenu = NSMenu(title: "MyMenu")
func applicationDidFinishLaunching(aNotification: NSNotification) {
if let viewController = ViewController(nibName: "ViewController", bundle: nil) {
viewController.dockMenu = self.dockMenu
self.window.contentViewController = viewController
}
}
func applicationDockMenu(sender: NSApplication) -> NSMenu? {
return self.dockMenu
}
class ViewController: NSViewController {
var dockMenu: NSMenu?
// Button action
#IBAction func updateDockMenu(sender: AnyObject) {
self.dockMenu?.addItem(NSMenuItem(title: "An Item", action: nil, keyEquivalent: ""))
}
}