Cocoa: NSViewController.viewWillAppear() and viewDidAppear() not called when window was minimized to dock - cocoa

This question is about a macOS app, not iOS.
I have a basic Cocoa app that supports multiple windows. I noticed that NSViewController.viewWillAppear() and viewDidAppear() are not called when the app launches and the window was minimized to the dock previously.
To reproduce:
Launch the app and open a new window
Minimize the window to the dock (Window → Minimize)
Quit and relaunch the app
→ The window should still be minimized in the dock
Click on the window in the dock to bring it back on screen
Expected:
NSViewController.viewWillAppear() and viewDidAppear() should be called
Actual:
they are not called
This is a problem, because I need a reference to the NSWindow in my view controller to finish some setup so I can't simply hook into viewDidLoad().
Minimal, reproducible example
System Preferences → General → Uncheck "Close windows when quitting an app"
Xcode → New Project → macOS App:
User Interface: Storyboard
☑︎ Create Document-Based Application
Open ViewController.swift and add:
.
override func viewWillAppear() {
super.viewWillAppear() [PUT BREAKPOINT HERE]
}
Open Document.swift and:
Have func data(ofType typeName: String) simply return Data()
Have func read(from data: Data, ofType typeName: String) do nothing (remove the exception)
Build & run
Cmd-S to save the document that opened by default
Window → Minimize
Document window now minimized in the dock
Cmd-Q to quit the app
Relaunch the app
Document window still sits minimized in the dock
Click on the document window icon in the dock to bring it back
viewWillAppear() and viewDidAppear() will not be called as the window is unminimized.

Why not using NSWindowDidDeminiaturizeNotification on the window controller and, in that notification method, send a message to the view(s) needing this kind of update ? You could have a flag on the window controller to differentiate your case from a standard case and only send that message in the first context. Something like this :
-(void) NSWindowDidDeminiaturize:(NSNotification *)notification {
if (_justLaunched) [_myView windowDidDeminatiurizeAtLaunchTime];
_justLaunched = NO;
}

Related

MacOS SwiftUI Menu Bar App Settings opening in background

I'm building a macOS menu bar app with SwiftUI's new MenuBarExtra API and running into a bit of an odd issue.
I've implemented a settings window that I can open with the following call:
if #available(macOS 13, *) {
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
} else {
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
}
I've also set the Application is agent flag to YES in info my project's properties.
Unfortunately, whenever I open the settings window via the MenuBar, it opens in the background and isn't visible at all. I'm really not sure how to proceed from here. I've thought about the following:
Programmatically change focus (Doesn't appear to exist)
Open a separate window (This seems to not work due to the Agent setting)
Has anyone come across this issue and implemented a solution to it?
You can change the activation policy programatically. Before you open your preferences, run the following code:
NSApp.setActivationPolicy(.regular) //makes the app a "normal" app again with a menu bar and a visible dock icon
NSApp.activate(ignoringOtherApps: ignoringOtherApps) // Activates the app in order to bring the window to front. Otherwise your window may be hidden behind windows of other apps
As soon as your last window closes, make the app an agent/accessory app again:
NSApp.hide(self)
NSApp.setActivationPolicy(.accessory)
You may want to use this extension:
extension NSApplication {
static func show(ignoringOtherApps: Bool = true) {
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: ignoringOtherApps)
}
static func hide() {
NSApp.hide(self)
NSApp.setActivationPolicy(.accessory)
}

Menu access from NSStatusItem (Menu Bar) Application in Swift

I'm creating an application where a menubar seems to be the most convenient way to have the user's desktop clean without a window. I've seen many tutorials online and on stack overflow but they seem to be only for Objective-C. I only use Swift. If you don't know what a menubar is, they're these icons:
I would like my app to have one of these instead of a constant full window. And if I can, how can I have a button on my menubar that brings up the window. Lastly, how can I have my icon not show, but I still have the finder advantages. (Like File, Edit..). For example,
I have already tried to put
Application is Agent (UIElement) to False
in my Info.plist but that also takes away my finder advantages.
Presumably what you're saying is that you want the text editing items (like Undo, Cut, Copy, Paste, Select All) from the Edit menu to work in your app's window.
Those menu items are part of another application, and only send messages in that application. They aren't available to your application, regardless of whether it's an “agent” (with no visible menu bar of its own). If one of your agent app's windows is the key window and the user clicks on a menu title (like File or Edit) that belongs to another app, then that app will activate and your app's window will “resign” the key window status.
You can make the usual shortcut keys (like ⌘X for Cut) work for your app, and it's easy. When one of your app's windows is the user's key window, your app receives keyboard events, and your NSApplication object (created for you automatically) will check its mainMenu for keyboard shortcuts even though the main menu is not displayed on the screen.
The OS X “Cocoa Application” project template sets up a main menu bar for you in MainMenu.xib (or in Main.storyboard), with all of the menu items wired up to the appropriate actions. So if you keep that main menu bar and the Edit menu and the menu items in the Edit menu and leave the shortcuts set on those items, then the keyboard shortcuts will work even if you set LSUIElement to YES in your Info.plist, when one of your app's windows is the key window. In other words, the shortcut keys will work by default, and you have to change things to make them stop working.
Text fields in your app's windows will also still get the default right-click menu with the usual items like Cut, Copy, and Paste, so you don't need to do anything else to make that work either.
Here's the contents of my test app's MainMenu.xib:
I've left the main menu bar alone. I've created a separate menu with two items, “Show Window” and “Quit”. I've set the shortcut for “Quit” to ⌘Q, but this shortcut has no effect. The StatusItem > Quit menu item (not visible in my screen shot) off the main menu bar has the same shortcut set, and that's the setting that matters. I've set the shortcut on this other Quit item because it's visible to the user, and the main menu bar won't be visible to the user.
I've wired this Quit item to the terminate: action of First Responder. (The StatusItem > Quit menu item is connected the same way by default.)
Here's my AppDelegate:
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet var window: NSWindow!
#IBOutlet var statusItemMenu: NSMenu!
var statusItem: NSStatusItem?
func applicationDidFinishLaunching(aNotification: NSNotification) {
self.statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSVariableStatusItemLength)
let statusItem = self.statusItem!
let button = statusItem.button!
button.title = "Hello"
statusItem.menu = statusItemMenu
}
#IBAction func showWindow(sender: AnyObject) {
NSApp.activateIgnoringOtherApps(true)
window.makeKeyAndOrderFront(sender)
}
}
I've wired the “Show Window” menu item to the showWindow(_:) action, and I've connected the statusItemMenu outlet to that standalone menu in the XIB.
I also set “Application is Agent (UIElement)” to “YES” in Info.plist.
When I run this app, it creates the status item in the menu bar. I can choose “Show Window” from the item and my window comes to the front and becomes key. I can right-click the text field to get its context menu. I can use the standard shortcuts to cut/copy/paste/etc., to close the window, and even to quit the app.

Pushing a window into focus

I'm developing a small menubar application and I want to display the settings window when the corresponding NSMenuItem is pressed.
I currently have the following IBAction assigned to the menu item:
#IBAction func settingsButtonPressed(sender: NSMenuItem) {
settingsView.makeKeyAndOrderFront(sender)
}
This displays the window, but doesn't push it into focus, so it's displayed behind the currently active window, which is not the behaviour I'm looking for.
I had a suspicion that this might have been due to the fact that the Application is agent target property is set to YES, but this actually has no effect on the outcome.
Could there be anything to be done with the window in the XIB file?
Probably your app is not the active app. It should work to call [NSApp activateIgnoringOtherApps:YES] in addition to making the window key and ordering it front.

Showing Cocoa app's preferences on clicking app icon again

For a background app (LSUIElement=1), what's the most elegant way to make its 'preferences' or 'configuration' window pop up if a user double-clicks the app icon while it's already running?
This is assuming the user cannot access the app's prefs from anywhere else (such as menu bar status item menus).
I would suppose the ideal method would prevent the prefs window from showing on initial start, but is smart enough to show it on subsequent double-clicks on the app's icon.
Thanks
You just have to implement an NSApplicationDelegate protocol method applicationShouldHandleReopen:hasVisibleWindows:.
- (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag
{
// open pref pane
return NO;
}
would suffice; this delegate method is only called when the app is re-opened.

How do you make the Application window open when the dock icon is clicked?

I'm surprised this doesn't happen automatically, but I would like my applications window to open automatically when the Dock icon is clicked.
Just to clarify, when i open the app, the window automatically opens, but when I click the cross for the window but leave the App running, the window won't open when i click the dock icon.
Implement - (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag in your app delegate. Check the documentation for the details of the return value.
Document based apps and non-document based apps behave slightly differently. If there are no open windows when the dock icon of a document based app is clicked then it will create a new document. If there are no open windows when the dock icon of a non-document based app is clicked then it will do nothing.
- (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag
{
if (flag) {
return NO;
}
else
{
[YourWindow makeKeyAndOrderFront:self];// Window that you want open while click on dock app icon
return YES;
}
}
This is what I'm doing to get the main window of a non-document based app back to screen once it has been closed. I know this might not be the right way to do it but It's working for me so far.
Implemented this on the AppDelegate, window is defined as instance variable of the same object.
- (BOOL) applicationShouldOpenUntitledFile:(NSApplication *)sender
{
[window makeKeyAndOrderFront:self];
return NO;
}
If anyone has a better solution please reply. Thanks!
As others pointed, using applicationShouldHandleReopen method for reopening closed windows in non-document apps is the right way. The only change I want to add is a more flexible way to check what window must be re-displayed, by iterating through the NSApplication's list of visible and invisible .windows and checking for the required window.
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
if flag == false {
for window in sender.windows {
if (window.delegate?.isKind(of: WelcomeWindowController.self)) == true {
window.makeKeyAndOrderFront(self)
}
}
}
return true
}
Appendix
a) If window was hidden then it will be showed automatically when user will click on app's Dock icon, so no need to implement applicationShouldHandleReopen method.
b) Checked "Release when closed" option doesn't affect in any way the above behaviour.
A document based application will automatically open a new untitled document when the app becomes active, so I am assuming you are referring to a non-document based app.
Implement the applicationDidBecomeActive: method in your application delegate and open/show the window.
Edit:
Some information on Delegates.
Some information on Opening and Closing Windows and the NSWindow API
A solutions to add to the accepted answer:
With the accepted answer the reopened window did not react to mouse events anymore.
When using the accepted answer you also have to make sure to uncheck "Release when closed" in the Attributes Inspector of the window in IB. This fixes the unresponsive window problem.
https://developer.apple.com/forums/thread/706772?answerId=715063022#715063022
func applicationDidFinishLaunching(_ aNotification: Notification)
{
NSApplication.shared.delegate = self
}
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
//Now is working
return true
}

Resources