How do I reliably get windowDidBecomeMain and windowDidResignMain events? - cocoa

I have a view controller in which I want to receive windowDidBecomeMain and windowDidResignMain events.
In viewWillApear() I set the window delegate to self.
view.window?.delegate = self
I have added an extension to my view controller that conforms to NSWindowDelegate and have implemented both methods in it thus:
extension CustomerListViewController: NSWindowDelegate
{
func windowDidBecomeMain(_ notification: Notification)
{
print("Customer list did become main")
}
func windowDidResignMain(_ notification: Notification)
{
print("Customer list did resign main")
}
}
This is not the initial window. It is opened via a menu item with a Window controller show segue.
When the window is first opened via the menu item, it does not receive windowDidBecomeMain.
When I click another window it does receive windowDidResignMain.
If I then click back into the newly opened window it does receive windowDidBecomeMain and will from that point on.
I suspect that I need to set my window delegate at a different point but don't have a clue where to do so, if that is the case.

Related

How to get state restoration to restore all windows/tabs on macOS?

I have a non-document based macOS AppKit app. It has one window instantiated automatically by the storyboard. I have sub-classed NSWindowController and added a override func newWindowForTab(_ sender: Any?) to enable the + button on the tab-bar. My main view controller lets the user rename the tab title and the window title is set to the same. This is kind of like how Xcode tab renaming works.
Additionally I have sub-classed NSWindow and added a restorableStateKeyPaths to ensure tab and window titles are automatically restored on app restart.
This all works great.
But only for the first tab. The main window is loaded and it has the tab and window titles set automatically.
The other tabs (windows) are not restored.
Any hints on what I miss to make all tabs restored?
My NSWindowController:
class MyWindowController: NSWindowController {
var subview: MyWindowController?
#IBAction override func newWindowForTab(_ sender: Any?) {
let story = self.storyboard
let windowVC = story?.instantiateInitialController() as! Self
window?.addTabbedWindow(windowVC.window!, ordered: .above)
subview = windowVC
windowVC.window?.orderFront(self.window)
windowVC.window?.makeKey()
}
}
My NSWindow:
class MyWindow: NSWindow {
override class var restorableStateKeyPaths: [String] {
return [ "self.tab.title", "self.title" ]
}
}
First you need to make sure that state restoration is enabled for your user, you can do this by going to Preferences->General and unchecking "Close windows when quitting an app".
Then you should use a restoration class in order to restore all open windows.
Basically if an NSWindow doesn't have a restoration class it won't be preserved across launches, that includes your storyboard loaded window. In this case what is happening is Cocoa is ignoring all window preservation because you haven't defined a restoration class for any of your windows so it resorts to its default behavior which is loading the initial storyboard controller.
Implementing restoration class is easy, just create a restoration class that inherits from NSObject and conforms to NSWindowRestoration, then implement its only required type method restoreWindow(identifier:state:completionHandler) like so:
class MyAppWindowRestoration: NSObject, NSWindowRestoration {
static func restoreWindow(withIdentifier identifier: NSUserInterfaceItemIdentifier,
state: NSCoder,
completionHandler: #escaping (NSWindow?, Error?) -> Void) {
// 1.- Retrieve and show the window
// Retrieve a new instance of the only window
let window = (NSStoryboard.main?.instantiateInitialController() as? NSWindowController)?.window
// Call the completion handler with the window and no errors
completionHandler(window, nil)
}
}
Then just assign this class as the window restoration class on every window you want restored, you can do this everywhere after the window has loaded:
window.restorationClass = MyAppWindowRestoration.self
Unfortunately Apple's documentation on state restoration completely sucks so if you have any more questions let me know ;)

How to close window (NSWindowController) by hitting the ESC key?

Issue
I would like the user being able to close a window by hitting the ESC key but I can't get it to work in this specific case, hitting ESC triggers an error sound (the "no you can't do that" macOS bloop) and nothing happens.
Context
I'm making a subclass of NSWindowController which itself creates an instance of a subclass of NSViewController and sets it in a view. Both controllers have their own xib file.
NSWindowController:
final class MyWindowController: NSWindowController, NSWindowDelegate {
#IBOutlet weak var targetView: MainView!
let myVC: MyViewController!
var params: SomeParams!
override func windowDidLoad() {
super.windowDidLoad()
myVC = MyViewController(someParams: params)
myVC.view.setFrameSize(targetView.frame.size)
myVC.view.setBoundsSize(targetView.bounds.size)
targetView.addSubview(myVC.view)
}
override var windowNibName: String! {
return "MyWindowController"
}
convenience init(someParams params: SomeType) {
self.init(window: nil)
self.params = params
}
}
NSViewController:
final class MyViewController: NSViewController {
convenience init(someParams params: SomeType) {
// do stuff with the params
}
override func viewDidLoad() {
super.viewDidLoad()
// configure stuff for the window
}
}
What I've tried
I suppose that my issue is that the MyWindowController NSWindow is the .initialFirstResponder when I would want the content of the targetView (an NSTableView) to be the first responder - this way I could use keyDown, I guess, and send the close command to the window from there. This doesn't seem optimal, though.
I've tried forcing the view controller views into being the first responder by using window?.makeFirstResponder(theView) in the windowDidLoad of MyWindowController but nothing ever changes.
I've also tried adding this to MyWindowController:
override func cancelOperation(_ sender: Any?) {
print("yeah, let's close!")
}
But this only works if the user clicks first on the background of the window then hits ESC, and it still emits the error sound anyway. Which is actually what made me think that the issue was about the first responder being on the window.
Question
How would you achieve that? Of course, I know that the user can already close the window with CMD+W, but I'd really like to sort out this issue nonetheless.
Note that the code example is in Swift but I can also accept explanations using Objective-C.
The documentation of cancelOperation explains how cancelOperation should work:
This method is bound to the Escape and Command-. (period) keys. The key window first searches the view hierarchy for a view whose key equivalent is Escape or Command-., whichever was entered. If none of these views handles the key equivalent, the window sends a default action message of cancelOperation: to the first responder and from there the message travels up the responder chain.
If no responder in the responder chain implements cancelOperation:, the key window searches the view hierarchy for a view whose key equivalent is Escape (note that this may be redundant if the original key equivalent was Escape). If no such responder is found, then a cancel: action message is sent to the first responder in the responder chain that implements it.
NSResponder declares but does not implement this method.
NSWindow implements cancelOperation: and the next responder, the window controller, isn't checked for an implementation of cancelOperation:. The cancel: message does arrive at the window controller. Implementing
- (void)cancel:(id)sender
{
NSLog(#"cancel");
}
will work. The cancel: message isn't inherited from a superclass so autocompletion doesn't suggest it.
This worked for me in Xcode 10 and Swift 4.2:
#objc func cancel(_ sender: Any?) {
close()
}
I tried it before but without the #objc part and it didn't work. So don't omit it.
When I needed such behavior I implemented it by overriding keyDown: of the NSWindow object.
I.e. something like the following:
- (void)keyDown:(NSEvent *)theEvent
{
int k = [theEvent keyCode];
if (k == kVK_Escape)
{
[self close];
return;
}
[super keyDown:theEvent];
}

NSProgressIndicator on Modal Sheet Doesn't Animate

My main window controller has a toolbar item that triggers the presentation of a modal sheet. The sheet is supposed to display the progress of a lengthy, asynchronous process (e.g., sync local data with a server).
However, I can not get the (indeterminate) progress indicator to animate.
This is the action that triggers the modal sheet:
var syncProgressWindowController: SyncProgressWindowController!
// ...
#IBAction func syncWithServer(_ sender: AnyObject) {
// (Actual HTTP code not implemented)
syncProgressWindowController = SyncProgressWindowController()
syncProgressWindowController.loadWindow()
guard let modalWindow = syncProgressWindowController.window else {
return
}
self.window?.beginSheet(modalWindow, completionHandler: { (response) in
// THIS GETS EXECUTED.
// However, the code below has no effect:
self.syncProgressWindowController.progressIndicator.startAnimation(self)
// self.syncProgressWindowController.progressIndicator is
// NOT nil, despite windowDidLoad() not being called
// (see below)
})
}
The modal sheet window controller (class SyncProgressWindowController above) is defined like this:
#IBOutlet weak var progressIndicator: NSProgressIndicator!
convenience init() {
self.init(windowNibName: "SyncProgressWindow")
}
override func windowDidLoad() {
super.windowDidLoad()
// Breakpoints here don't work, logs don't print to the console.
// Not called? But outlet _is_ set (see above).
}
The xib file (SyncProgressWindow.xib) has:
File's Owner Identity/Class set to "SyncProgressWindowController"
Window has New Referencing Outlet to File's Owner's window
Window has delegate outlet wired to "File's Owner" (just in case - but delegate methods don't seem to get called either).
Window has "Visible at Launch" unchecked (and is therefore displayed modally with no problems).
Progress has New Referencing Outlet wired to File's Owner's progressIndicator.
However:
SyncProgressWindowController's windowDidLoad() does not get called (Execution does not stop at breakpoints there and logs aren't printed).
Despite that, the property/outlet progressIndicator is set somehow, because the app does not crash when I attempt to animate it, with code like this:
self.syncProgressWindowController.progressIndicator.startAnimation(self)
What am I missing?
completionHandler will be fired when you close sheet by endSheet(_:returnCode:) So you start indicator before sheet will be closed.
I'm not good in xib files, but when i disabled row with loadWindow, windowDidLoad was called. I'm not sure it's right way.

Handle close event of the window in Swift

How to handle close event of the window using swift, for example, to ask "Are you sure you want to close the form?"
The form will be closed in the case "yes" and not closed in the case "no". Showing message box is not a problem for me.
viewWillDisappear() works for minimizing also, but I need only close event.
Thanks.
Like said above, you should make the ViewController an NSWindowDelegate, but you should handle windowWillClose, not windowShouldClose. windowShouldClose is to determine if the window is able to close or not, not an event that the window is actually closing.
I also found that you need to set up the delegate in viewDidAppear, not viewDidLoad. For me self.view.window wasn't defined yet in viewDidLoad.
override func viewDidAppear() {
self.view.window?.delegate = self
}
I was having the same query too, solved it using the method explained in detail here: Quit Cocoa App when Window Close using XCode Swift 3
It needs three steps:
Conform toNSWindowDelegate in your ViewController class
Override viewDidAppear method
Add windowShouldClose method
The added code should look like this:
class ViewController: NSViewController, NSWindowDelegate {
// ... rest of the code goes here
override func viewDidAppear() {
self.view.window?.delegate = self
}
func windowShouldClose(_ sender: Any) {
NSApplication.shared().terminate(self)
}
}
You can use the NSWindowDelegate protocol in your ViewController class. (See the documentation here)
To make your class conform to the protocol:
class ViewController: NSObject, NSWindowDelegate
To detect when the window's close button has been clicked, use windowShouldClose:
From the doc:
Tells the delegate that the user has attempted to close a window [...]
In this method, you can use NSAlert to prompt the user on whether or not they really want to close the window.
EDIT (in response to #Mr Beardsley's comment)
To make your ViewController the delegate, use:
window.delegate = self
Where self is the ViewController and window is the window you're using. You can put this in viewDidLoad:.
Just add this function to AppDelegate ...
func applicationShouldTerminateAfterLastWindowClosed (_ theApplication: NSApplication) -> Bool {
return true
}

How do you re-open a closed window created in the storyboard in OS X

My question is essential this question, but the answer doesn't seem to work with Swift/Storyboards.
Cocoa: programmatically show the main window after closing it with X
Basically, I have a more or less default application with a menu, a window, and a ViewController. If the user closes the window while the application is running, how do I reopen it?
I have created an action in the app delegate the connects to the "Open" Menu Item. Within this function, I would like to ensure that the window is visible. So if the user has closed it, it should reappear. But I cannot figure out how to access the closed window. Storyboard does not seem to allow me to create an outlet for my Window in my app delegate.
This is quite simple to archive, even it is not an elegant solution. Add a new property to your app delegate for your main window controller. In the following example, I call the controller MainWindowController.
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var mainWindowController: MainWindowController? = nil
func applicationShouldHandleReopen(sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
mainWindowController?.window?.makeKeyAndOrderFront(self)
return false
}
}
In the initialisation of the main window controller I register the controller in the app delegate:
class MainWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
// ...initialisation...
// Register the controller in the app delegate
let appDelegate = NSApp.delegate as! AppDelegate
appDelegate.mainWindowController = self
}
}
That is all, works perfectly for me.

Resources