How to know when a Cocoa app is about to quit? - xcode

I have a NSDocument based application. I'd like to know when the application is about to quit to validate some things. I'd hoped there might be a method such as a applicationWillQuit, but looking through the docs for both NSDocument and NSApplication I can't find anything similar.

There is a notification you can use coming from your NSApplication:
NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:#selector(appWillTerminate:)
name:NSApplicationWillTerminateNotification
object:nil];
This is documented here: https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Classes/nsapplication_Class/Reference/Reference.html
By passing the object as nil your method is being called whenever an object fires the notification.

We have a delegate method in AppDelegate.swift or AppDelegate.m class. You can use it and add functionality to you application before closing it.
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}

Related

OS X storyboard calls viewDidLoad before applicationDidFinishLaunching

I created a Mac OS X application in Xcode using storyboards. For some reason the applicationDidFinishLaunching method in the AppDelegate is being called after viewDidLoad in the NSViewControllers. As with iOS apps, I thought viewDidLoad is supposed to be called before applicationDidFinishLaunching? Do storyboards in OS X apps initialize the view controllers before the app has finished launching?
I am using the applicationDidFinishLaunching method to register default settings into NSUserDefaults. Unfortunately, registering the default values is happening after the views in the storyboard are loaded. Therefore, when I set up the view in each view controller using viewDidLoad, the defaults data in NSUserDefaults has not been set. If I can't use applicationDidFinishLaunching to register NSUserDefaults in OS X storyboard apps, then how I set the defaults before viewDidLoad is called?
To fix this issue, in the Main.storyboard in Xcode, I turned off "Is Initial Controller" for the main window. I assigned a storyboard ID to the main window as "MainWindow". Then in the AppDelegate I entered the following code:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(aNotification: NSNotification) {
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let mainWindow = storyboard.instantiateControllerWithIdentifier("MainWindow") as! NSWindowController
mainWindow.showWindow(nil)
mainWindow.window?.makeKeyAndOrderFront(nil)
}
}
The app does not crash but now the window never appears. The following image displays the storyboard I'm working with:
Correct, the lifecycle is a wee bit different in OS X.
Instead of letting the storyboard be your initial interface (this is defined in the General settings of your project), you can instead set up a MainMenu xib file and designate that as your main interface, then in your applicationDidFinishLaunching method in your AppDelegate you can programmatically instantiate your storyboard after you have completed your other initialization code.
I recommend checking out Cocoa Programming for OS X: The Big Nerd Ranch Guide if you haven't already; one nice thing they do in their book is actually have you get rid of some of the default Xcode template stuff and instead they have you set up your initial view controller the "right" way by doing it explicitly.
You might put something like this in your applicationDidFinishLaunching:
NSStoryboard *mainStoryboard = [NSStoryboard storyboardWithName:#"Main" bundle:nil];
MyWindowController *initialController = (MyWindowController *)[mainStoryboard instantiateControllerWithIdentifier:#"myWindowController"];
[initialController showWindow:self];
[initialController.window makeKeyAndOrderFront:self];
This assumes that you've already changed "Main Interface" to something like MainMenu.xib.
As the question's author mentioned:
The app does not crash but now the window never appears.
Despite Nicolas and Aaron both helpful answers (I already adquire the book you recommended, Aaron, and will start implementing the two-storyboard pattern your way, Nicolas), neither solve the specific problem of the window not showing.
Solution
You need make your WindowController instance live outside applicationDidFinishLaunching(_:)'s scope. To accomplish it, you could declare a NSWindowController propriety for your AppDelegate class and persist your created WindowController instance there.
Implementation
In Swift 4.0
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var mainWindowController: NSWindowController?
func applicationDidFinishLaunching(_ aNotification: NSNotification) {
let storyboard = NSStoryboard(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil)
let mainWindow = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: "MainWindow")) as! NSWindowController
mainWindow.showWindow(self)
mainWindow.window?.makeKeyAndOrderFront(self)
// After applicationDidFinishLaunching(_:) finished, the WindowController instance will persist.
self.mainWindowController = mainWindow
}
}
In addition to Aaron's answer, I found out that the separate Interface Builder file containing the menu alone (separate from the window and view controller) does not need to be a xib; it too can be a storyboard. This enables us to easily fix as follows:
Duplicate your original storyboard (Main.storyboard) and rename it to "Menu.storyboard"
Delete the window controller and view controller from Menu.storyboard
Delete the app menu bar from Main.storyboard.
Change your app target's "Main Interface" from "Main.storyboard" to "Menu.storyboard".
(I tried it in my app and it works)
This avoids having to re-create the app's main menu from scratch, or to figure out how to transplant your existing one from "Main.stroyboard" to "Menu.xib".
The other solution is registering NSApplicationDidFinishLaunchingNotification in your view controllers and move your code from viewDidLoad to applicationDidFinishLaunchingNotification:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(applicationDidFinishLaunchingNotification:)
name:NSApplicationDidFinishLaunchingNotification
object:nil];
Based on my previous answer here
If you want a piece of code to be run before everything, you can override the AppDelegate's init() like this:
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
override init() {
DoSomethingBeforeEverthing() // You code goes here
super.init()
}
...
}
Few points to remember:
You might want to check, what's allowed/can be done here, i.e. before app delegate is initialized
Also cleaner way would be, subclass the AppDelegate, and add new delegate method that you call from init, say applicationWillInitialize and if needed applicationDidInitialize

How to use NSViewController in an NSDocument-based Cocoa app

I've got plenty of experience with iOS, but Cocoa has me a bit confused. I read through several Apple docs on Cocoa but there are still details that I could not find anywhere. It seems the documentation was written before the NSDocument-based Xcode template was updated to use NSViewController, so I am not clear on how exactly I should organize my application. The template creates a storyboard with an NSWindow, NSViewController.
My understanding is that I should probably subclass NSWindowController or NSWindow to have a reference to my model object, and set that in makeWindowControllers(). But if I'd like to make use of the NSViewController instead of just putting everything in the window, I would also need to access my model there somehow too. I notice there is something called a representedObject in my view controller which seems like it's meant to hold some model object (to then be cast), but it's always nil. How does this get set?
I'm finding it hard to properly formulate this question, but I guess what I'm asking is:how do I properly use NSViewController in my document-based application?
PS: I understand that NSWindowController is generally meant to managing multiple windows that act on one document, so presumably if I only need one window then I don't need an NSWindowController. However, requirements might change and having using NSWindowController may be better in the long run, right?
I haven't dived into storyboards but here is how it works:
If your app has to support 10.9 and lower create custom of subclass NSWindowController
Put code like this into NSDocument subclass
- (void)makeWindowControllers
{
CustomWindowController *controller = [[CustomWindowController alloc] init];
[self addWindowController:controller];
}
If your app has multiple windows than add them here or somewhere else (loaded on demand) but do not forget to add it to array of document windowscontroller (addWindowController:)
If you create them but you don't want to show all the windows then override
- (void)showWindows
{
[controller showWindow:nil]
}
You can anytime access you model in your window controller
- (CustomDocument *)document
{
return [self document];
}
Use bindings in your window controller (windowcontroller subclass + document in the keypath which is a property of window controller)
[self.textView bind:#"editable"
toObject:self withKeyPath:#"document.readOnly"
options:#{NSValueTransformerNameBindingOption : NSNegateBooleanTransformerName}];
In contrast to iOS most of the views are on screen so you have to rely on patterns: Delegation, Notification, Events (responder chain) and of course MVC.
10.10 Yosemite Changes:
NSViewController starting from 10.10 is automatically added to responder chain (generally target of the action is unknown | NSApp sendAction:to:from:)
and all the delegates such as viewDidLoad... familiar from iOS are finally implemented. This means that I don't see big benefit of subclassing NSWindowCotroller anymore.
NSDocument subclass is mandatory and NSViewController is sufficient.
You can anytime access you data in your view controller
- (CustomDocument *)document
{
return (CustomDocument *)[[NSDocumentController sharedDocumentController] documentForWindow:[[self view] window]];
//doesn't work if you do template approach
//NSWindowController *controller = [[[self view] window] windowController];
//CustomDocument *document = [controller document];
}
If you do like this (conforming to KVC/KVO) you can do binding as written above.
Tips:
Correctly implement UNDO for your model objects in Document e.g. or shamefully call updateChangeCount:
[[self.undoManager prepareWithInvocationTarget:self] deleteRowsAtIndexes:insertedIndexes];
Do not put code related to views/windows into your Document
Split your app into multiple NSViewControllers e.g.
- (void)prepareForSegue:(NSStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:AAPLListWindowControllerShowAddItemViewControllerSegueIdentifier]) {
AAPLListViewController *listViewController = (AAPLListViewController *)self.window.contentViewController;
AAPLAddItemViewController *addItemViewController = segue.destinationController;
addItemViewController.delegate = listViewController;
}
}
Previous code is called on windowcontroller with viewcontroller as delegate (again possible only after 10.10)
I always prefer to use multiple XIBs rather than one giant storyboard/XIB. Use following subclass of NSViewController and always inherit from it:
#import <Cocoa/Cocoa.h>
#interface MyViewController : NSViewController
#property(strong) IBOutlet NSView *viewToSubstitute;
#end
#import "MyViewController.h"
#interface MyViewController ()
#end
#implementation MyViewController
- (void)awakeFromNib
{
NSView *view = [self viewToSubstitute];
if (view) {
[self setViewToSubstitute:nil];
[[self view] setFrame:[view frame]];
[[self view] setAutoresizingMask:[view autoresizingMask]];
[[view superview] replaceSubview:view with:[self view]];
}
}
#end
Add a subclass of MyViewController to the project with XIB. Rename the XIB
Add NSViewController Object to the XIB and change its subclass name
Change the loading XIB name to name from step 1
Link view to substitute to the view you want to replace
Check example project Example Multi XIB project
Inspire yourself by shapeart or lister or TextEdit
And a real guide is to use Hopper and see how other apps are done.
PS: You can add your views/viewcontroller into responder chain manually.
PS2: If you are beginner don't over-architect. Be happy with the fact that your app works.
I'm relatively new to this myself but hopefully I can add a little insight.
You can use the view controllers much as you would in ios. You can set outlets and targets and such. For NSDocument-based apps you can use a view controller or the window controller but I think for most applications you'll end up using both with most of the logic being in the view controller. Put the logic wherever it makes the most sense. For example, if your nsdocument can have multiple window types then use the view controller for logic specific to each type and the window controller for logic that applies to all the types.
The representedObject property is primarily associated with Cocoa bindings. While I am beginning to become familiar with bindings I don't have enough background to go into detail here. But a search through the bindings programming guide might be helpful. In general bindings can take the place of a lot of data source code you would need to write on ios. When it works it's magical. When it doesn't work it's like debugging magic. It can be a challenge to see where things went wrong.
Let me add a simple copy-pastable sample for the short answer category;
In your NSDocument subclass, send self to the represented object of your view controller when you are called to makeWindowControllers:
- (void) makeWindowControllers
{
NSStoryboard* storyboard = [NSStoryboard storyboardWithName: #"My Story Board" bundle: nil];
NSWindowController* windowController = [storyboard instantiateControllerWithIdentifier: #"My Document Window Controller"];
MyViewController* myController = (id) windowController.contentViewController;
[self addWindowController: windowController];
myController.representedObject = self;
}
In you MyViewController subclass of NSViewController, overwrite setRepresentedObject to trap it's value, send it to super and then make a call to refresh your view:
- (void) setRepresentedObject: (id) representedObject
{
super.representedObject = representedObject;
[self myUpdateWindowUIFromContent];
}
Merci, bonsoir, you're done.

Forcing a delegate method to be called

I have a delegate method of an NSSplitView like this:
- (void)splitViewWillResizeSubviews:(NSNotification *)aNotification
{
NSLog(#"RESIZE!");
}
This method is called whenever I drag a divider, so it registered properly. I would like to call this from another object, and was thinking to use this:
[[NSNotificationCenter defaultCenter] postNotificationName:NSSplitViewWillResizeSubviewsNotification object:self];
According to the Apple docs, this is the notification that should be sent to call the delegate method. However, it does not work. Does anyone have an idea what I am doing wrong?
You can just invoke the method manually
NSSplitView * yourSplitView; //Get reference to your splitview
id yourSplitViewDelegate = [yourSplitView delegate];
[yourSplitViewDelegate splitViewWillResizeSubviews:nil];//Optionally create the NSNotification with relevant data
If you really want to go through notification center, make sure self in your question is the NSSplitView.
NSSplitView * yourSplitView; //Get reference to your splitview
[[NSNotificationCenter defaultCenter] postNotificationName:NSSplitViewWillResizeSubviewsNotification object:yourSplitView];
Turns out that I needed to manually register the delegate class for the NSSplitViewWillResizeSubviewsNotification notifications!
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(splitViewWillResizeSubviews:)
name:NSSplitViewWillResizeSubviewsNotification
object:vc];
where vc is the viewcontroller that should be sending the notifications.
This is unexpected behavior (to me), since an <NSSplitViewDelegate> is expected to register automatically for NSSplitView... notifications.

How to use NSWindowDidExposeNotification

I am trying to update another windows when the one becomes visible. So I found the NSWindowDidExposeNotification and tried to work with it, so I wrote in my awakeFromNib:
// MyClass.m
- (void)awakeFromNib {
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:#selector(mentionsWindowDidExpose:)
name:NSWindowDidExposeNotification
object:nil];
}
and implemented the method
// MyClass.h
- (void)mentionsWindowDidExpose:(id)sender;
// MyClass.m
- (void)mentionsWindowDidExpose:(id)sender {
NSLog(#"test");
}
But it never gets called which is odd. What do I do wrong here?
Generally speaking, you would set up your controller as the window's delegate in order to receive these notifications, like so:
// MyClass.m
- (void)awakeFromNib {
// note: this step can also be done in IB by dragging a connection
// from the window's "delegate" property to your `MyClass` object
[window setDelegate:self];
}
- (void)windowDidExpose:(NSNotification *)notification {
NSLog(#"test");
}
Although, after reading here and here, windowDidExpose may not be your best bet. I would recommend trying the windowDidBecomeKey delegate method instead. That one is posted whenever your window gains "focus" (starts responding to user input) which may be the right time to show your second window.
Update: (in response to comments)
Apple's documentation (quoted below) indicates that NSWindowDidExposeNotification is only valid for nonretained windows, which, according to the posts that I linked above, are quite uncommon.
NSWindowDidExposeNotification
Posted whenever a portion of a nonretained NSWindow object is exposed, whether by being ordered in front of other windows or by other windows being removed from in front of it.
The notification object is the NSWindow object that has been exposed. The userInfo dictionary contains ... the rectangle that has been exposed.
On a higher level, NSNotification objects are simply packages of data that get passed around between Cocoa classes and NSNotificationCenter objects. NSNotificationCenter objects are controllers that manage these packages of data and send them out to observers as required. There is usually no need to trap notifications directly. You can simply use KVC/KVO or pre-defined delegates in your classes and Cocoa handles all of the dirty details behind the scenes.
See Notification Programming Topics and Key Value Coding Programming Guide if you want to know more.

How can I get notified when the user finishes editing a cell in an NSTableView?

I need to know when the user finishes editing a cell in an NSTableView. The table contains all of the user's calendars (obtained from the CalCalendarStore), so in order for the user's changes to be saved I need to inform the CalCalendarStore of the changes. However, I can't find anything that gets called after the user finishes their editing - I would guess that there would be a method in the table's delegate, but I only saw one that gets called when editing starts, not when editing ends.
You can achieve the same result without subclassing NSTableView by using NSNotificationCenter or using the NSControl methods. See the Apple documentation here:
http://developer.apple.com/library/mac/#qa/qa1551/_index.html
It's only a couple of lines of code and worked perfectly for me.
If you can be the delegate of the NSTableView you just need to implement the method
- (void)controlTextDidEndEditing:(NSNotification *)obj { ... }
In fact, NSTableView is the delegate of the NSControl elements it contains, and forwards those method calls to its delegate (There are other methods that are useful)
Otherwise, use the NSNotificationCenter:
// where you instantiate the table view
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(editingDidEnd:)
name:NSControlTextDidEndEditingNotification object:nil];
// somewhere else in the .m file
- (void)editingDidEnd:(NSNotification *)notification { ... }
// remove the observer in the dealloc
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSControlTextDidEndEditingNotification object:nil];
[super dealloc]
}
Subclass NSTableView and override textDidEndEditing: (be sure to call super's implementation).
This will only be invoked by text fields NSTextFieldCell or NSComboBoxCell (but only when changing the value by typing it, not by selecting the value from the combo's menu).
Set up observers for each item in the content array using addObserver:toObjectsAtIndexes:forKeyPath:options:context:
You will also need to set an observer for the array itself, so that you will be notified about objects that are added to or removed from the array.
For an example look at the iSpend project.
Look into the NSTableDataSource protocol. The message you are looking for is called: tableView:setObjectValue:forTableColumn:row:
Translating #Milly's answer into Swift 3:
// Setup editing completion notifications
NotificationCenter.default.addObserver(self, selector: #selector(editingDidEnd(_:)), name: NSNotification.Name.NSControlTextDidEndEditing, object: nil)
Function to handle notification:
func editingDidEnd(_ obj: Notification) {
guard let newName = (obj.object as? NSTextField)?.stringValue else {
return
}
// post editing logic goes here
}
Subclass NSArrayController and override objectDidEndEditing: (be sure to call super's implementation).
This will mostly only be invoked by text fields NSTextFieldCell or NSComboBoxCell (but only when changing the value by typing it, not by selecting the value from the combo's menu). There may be a few other cells that will invoke it, but I'm not sure which ones. If you have a custom cell then consider implementing the NSEditor and NSEditorRegistration informal protocols.

Resources