Release a NSWindowController when the window is closed - objective-c

I'm building a Cocoa application and have a question about using window controllers. The idea is that if the user selects New from the menu bar, an instance of MyWindowController which is a subclass of NSWindowController is created and a new window from MyWindow.xib is displayed.
I'm handling the action in the application delegate. From what I have seen after searching around something like the following could be done. Once the window is displayed I don't have any reason to store a pointer to the window controller anymore and since I allocated it I also autorelease it before displaying the window.
[[[[MyWindowController alloc] init] autorelease] showWindow:self];
Since the window is released soon afterwards the window will briefly display on the screen and then go away. I have found a solution where I retain the window controller in the -showWindow: method and let it release itself once it gets a windowWillClose notification.
- (IBAction)showWindow:(id)sender
{
[self retain];
[[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillCloseNotification
object:self.window
queue:nil
usingBlock:^(NSNotification *note) {
[self release];
}];
[super showWindow:sender];
}
Is there a better way to do this? I have searched the Apple documentation and have not found anything on which practices to use. It sounds like something very basic which it should cover so maybe I'm just searching with the wrong terms.

Normally you would hold on to the window controller, and only release it when you are done with it. I'd say that your app delegate would be responsible for that. Just store them in an array if there can be multiple. Whilst your solution may work, it's not very elegant.
If you are working on a document based Cocoa app, you create the window controller in your document subclass method makeWindowControllers and let that class hold a pointer to your window controller.

func windowShouldClose(_ sender: NSWindow) -> Bool {
#if DEBUG
let closingCtl = sender.contentViewController!
let closingCtlClass = closingCtl.className
print("\(closingCtlClass) is closing")
#endif
sender.contentViewController = nil // will force deinit.
return true // allow to close.
}

Related

Opening Modal Sheets with 10.9

I have an app that uses multiple Modal Sheets for data entry. The methods in opening the modal sheets worked fine, and still work fine, but they have been deprecated and I fear they will soon not work with future releases of Xcode. Here, Apple points out how to use modal sheets,
- (void)showCustomSheet: (NSWindow *)window
// User has asked to see the custom display. Display it.
{
if (!myCustomSheet)
//Check the myCustomSheet instance variable to make sure the custom sheet does not already exist.
[NSBundle loadNibNamed: #"MyCustomSheet" owner: self];
[NSApp beginSheet: myCustomSheet
modalForWindow: window
modalDelegate: self
didEndSelector: #selector(didEndSheet:returnCode:contextInfo:)
contextInfo: nil];
// Sheet is up here.
// Return processing to the event loop
}
but with the release of Xcode 5.1, they identify that the loadNibNamed method has been deprecated and that we should use a similar function referencing top-level objects.
The problem I am having, is changing this:
[NSBundle loadNibNamed:#"OrderDetailsWindow" owner:self];
into this.
NSArray *array;
[[NSBundle mainBundle]loadNibNamed:#"OrderDetailsWindow" owner:self topLevelObjects:&array];
This method call does in fact open the modal sheet. However, at the end of my method that opens the modal sheet, Xcode hangs-up with this error.
0x7fff8c33b097: andl 24(%r11), %r10d Thread1: EXC_BAD_ACCESS (code:EXC_I386_GPFLT)
I'm not sure what this is telling me. It doesn't give me any information in the debug area. Could this have to do with the topLevelObjects array not being released properly? Any thoughts on how to make this work a little more smoothly? Apple's out-of-date library is driving me nuts!
Yes, Apple's documentation is a mess. The "Sheet Programming Topics" document has not been updated since 2009.
You don't show the full code after the change but my guess is that your problem is with the memory management of your NIB's objects.
From the documentation of the new loadNibNamed:owner:topLevelObjects:
Unlike legacy methods, the objects adhere to the standard cocoa memory
management rules; it is necessary to keep a strong reference to them
by using IBOutlets or holding a reference to the array to prevent the
nib contents from being deallocated.
Outlets to top-level objects should be strong references to
demonstrate ownership and prevent deallocation.
You have the NSArray that holds the top level objects inside your method. Once the execution leaves this method, the NSArray will be derefernced and released and so are all your top level objects if those are not strongly referenced anywhere else.
You need to either connect your top level objects in the NIB to outlets in your Window Controller or keep the NSArray as a member variable of your Window Controller instance, so it doesn't get released once your sheet showing method exits. And make sure that myCustomSheet properly declared and connected from the sheet's NIB.
Also, [NSApp beginSheet:] is deprecated as well, you now call beginSheet on an instance of NSWindow.
I always use a NSWindowController subclass with a custom delegate for my sheets:
From the window that wants to display the sheet:
_myModalController = [[MyModalController alloc] init];
_myModalController.delegate = self;
[_myModalController beginSheet:self.window];
Then within the modal window controller, I have:
- (id)init {
self = [super initWithWindowNibName:#"MyModalWindow" owner:self];
return self;
}
- (void)beginSheet:(NSWindow *)mainWindow {
[NSApp beginSheet:[self window]
modalForWindow:mainWindow
modalDelegate:self
didEndSelector:#selector(_didEndSheet:returnCode:contextInfo:)
contextInfo:nil];
}
- (void)endSheet:(NSWindow *)mainWindow {
[NSApp endSheet:[self window]];
[[self window] orderOut:mainWindow];
}
This appears to avoid the whole issue of loadNibNamed: becoming deprecated.

EXC_BAD_ACCESS when NSButton is pressed on NSWindowController

I am building a Mac application. I am adding a childWindowController to mainWindow. In my childWindowController, I have several buttons with their actions connected in IB. But when I press the NSButton, the application crashes and I get EXC_BAD_ACCESS message in the terminal. I also tried to perform setTarget:self, but that doesn't help at all.
Here's my code: applicationDidFinishLaunching
HomeWindowController *home_WindowController = [[[HomeWindowController alloc] initWithWindowNibName:#"HomeWindowController"] autorelease];<br/><br/>
[[self window] addChildWindow:home_WindowController.window
ordered:NSWindowAbove];
And in the HomeWindowController:
- (id)initWithWindowNibName:(NSString *)windowNibName
{
self = [super initWithWindowNibName:windowNibName];
if (self) {
// Initialization code here.
}
return self;
}
- (void)windowDidLoad
{
[super windowDidLoad];
}
-(IBAction)action:(id)sender
{
NSLog(#"------------------ ");
}
What is wrong here? I am binding the NSButton to FileOwner and its action as well. Normally same as for iOS for IB. When I don't bind the IBAction, I don't get EXC_BAD_ACCESS.
It finally got resolved, I was releasing the childWindowController on appDelegate method after adding it on main window.But I dont understand why cant I release it …
Because you own the window controller, and the window controller owns its window. The window isn't keeping its controller alive; you are. And when you're not, it dies out from under anything that might want to talk to it, such as a button that has it as its target.
More generally, trying to shrug off your ownership responsibilities onto other objects—e.g., expecting a window to own its WC for you—is asking for memory-management bugs.
(My only exception to that is indirectly owning objects through collections: if I own, say, an array full of Things, I don't retain and release each Thing individually on its way in and out. Anything else, I expect to outsmart me.)
since it is a local instance of childWindowController on AppDelegate (just for adding it above)?
I don't understand what you meant by that.

ViewController respondsToSelector: message sent to deallocated instance (CRASH)

Ok, here is the deal, I hate putting out questions about my debugging and crashes. Because I usually handle them myself, but I just cannot get my way around this, even after viewing multiple questions already.
Ok so here is the problem, I find my app randomly on and off crashing with this stack trace:
*** -[ViewController respondsToSelector:]: message sent to deallocated instance 0x1e5d2ef0
Where ViewController can vary, sometimes the place where my code crashes, has NO relevance to that particular ViewController and doesn't own or call it.
Also, to get that console trace, I have enabled Zombies, otherwise I would get no console print at all, I would only get: objc_msgSend, which I know means I am messaging something that is released. But I cannot find where that is... I am really stuck! Usually I always debug my crashes, so I am really stuck on this.
Again, this crashes in different places at different times, on and off. And the place it crashes has almost no relevance to the ViewController. And I find this very confusing.
Do you need any of my code? I have a lot of files and since it is crashing in different places, distributing my code will be a mess!
I have tried to add symbolic breakpoints with no luck, and Zombies is not available on the Instruments application for iOS. I cannot run my app on the simulator as it has unsupportive architecture frameworks for it.
Thanks everyone...
Use Instruments to track down deallocated instance errors. Profile your application (Cmd ⌘+I) and choose Zombies template. After your application is running, try to crash it. You should get something like that:
Click on the arrow next to address in the popover to show object that was called after it was deallocated.
You should see now every call that has changed retain count of this object. This could be because sending directly retain/release messages as well as draining autorelease pools or inserting into NSArrays.
RefCt column shows retainCount after action was invoked and Responsible Caller shows class name and method in which it was performed. When you double click on any retain/release, instruments will show you line of code where this was performed (If this isn't working, you can examine call by selecting it and choosing its counterpart in Extended Detail pane):
This will let you examine all the retainCount lifecycle of object and probably you'll find your problem right away. All you got to do is find missing retain for latest release.
had a similar problem. In my case a viewController needed to get navigationController events, so it was registering as the navigation controller delegate:
self.navigationController.delegate = self;
The crash occurs when that controller was dealloc'ed but was still the delegate for the view controller. Adding this code in dealloc had no effect:
-(void) dealloc
{
if (self.navigationController.delegate == self)
{
self.navigationController.delegate = nil;
}
because at the point that dealloc is called, the view controller has already been removed from the view hierarchy, so self.navigationController is nil, so the comparison is guaranteed to fail! :-(
The solution was to add this code to detect the VC leaving the view hierarchy just before it actually does so. It uses a method introduced in iOS 5 to determine when the view is being pop'ed and not pushed
-(void) viewWillDisappear:(BOOL) animated
{
[super viewWillDisappear:animated];
if ([self isMovingFromParentViewController])
{
if (self.navigationController.delegate == self)
{
self.navigationController.delegate = nil;
}
}
}
No more crashes!
For anyone who can't solve it, here are some other techniques:
https://stackoverflow.com/a/12264647/539149
https://stackoverflow.com/a/5698635/539149
https://stackoverflow.com/a/9359792/539149
https://stackoverflow.com/a/15270549/539149
https://stackoverflow.com/a/12098735/539149
You can run Instruments in Xcode 5 by clicking the project popup->Edit Scheme...Profile ->Instrument and choose Allocations or Leaks, then profile your app, then stop Instruments, click the info button in Allocations and "Enable NSZombie Detection".
However, for the messages that come directly from the com.apple.main-thread, this probably won't reveal anything.
I banged my head on this for over two hours and the answer turned out to be an over-release, which I discovered by commenting out a copy of my project by brute force until I found the culprit:
[viewController release];
viewController = NULL;
The problem is that release doesn't set the variable to NULL.
That means that setting it to NULL calls release again, decrementing the refcount and freeing the memory immediately until later when the variables that reference viewController are finished with it.
So either enable ARC or make sure your project consistently uses release or NULL but not both. My preference is to use NULL because then there is no chance of referencing a zombie but it makes finding where objects are released more difficult.
I had met the same problem in iOS yesterday. I have made IAP in App "About" subview, and I have added Transaction Observer in "About" viewDidLoad. When I purchase for the first time, no problem, but after I back to main window and enter about subview to purchase again, the problem "message sent to deallocated instance" happened, and the App crashed.
- (void)viewDidLoad
{
[[SKPaymentQueue defaultQueue] addTransactionObserver:self]; object:nil];
}
After I remove Transaction Observer in dealloc, the problem is solved.
- (void)dealloc
{
// Even though we are using ARC, we still need to manually stop observing any
// NSNotificationCenter notifications. Otherwise we could get "zombie" crashes when
// NSNotificationCenter tries to notify us after our -dealloc finished.
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
I had a very similar issue and I figured out it was due to navigation controller delegates set.
The below solved my issue,
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (self.navigationController.delegate != self) {
self.navigationController.delegate = self;
}
}
-(void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.navigationController.delegate == self) {
self.navigationController.delegate = nil;
}
}
Had the same problem in OS X.
To solve this not enough - (void)dealloc method as #SoftwareEvolved already said. But unfortunately - (void)viewWillDisappear is available only on version 10.10 and later.
I introduced custom method in my NSViewController subclass where set all the zombie-dangerous references to nil. In my case that was NSTableView properties (delegate and dataSource).
- (void)shutdown
{
self.tableView.delegate = nil;
self.tableView.dataSource = nil;
}
That's all. Each time I'm about to remove view from the superview need call this method.
I had the same Problem.It was difficult to find which delegate cause issue, because it does not indicate any line or code statement So I have try some way, Maybe it becomes helpful to you.
Open xib file and from file's owner, Select "show the connections inspector" right hand side menu. Delegates are listed, set them to nil which are suspected.
(Same as my case)Property Object like Textfield can create issue, So set its delegates to nil.
-(void) viewWillDisappear:(BOOL) animated{
[super viewWillDisappear:animated];
if ([self isMovingFromParentViewController]){
self.countryTextField.delegate = nil;
self.stateTextField.delegate = nil;
}
}

How to get Main Window (App Delegate) from other class (subclass of NSViewController)?

I'm trying to change my windows content, from other class , that is subclass of NSViewController.I'm trying code below, but it doesn't do anything.
[NSApplication sharedApplication]mainWindow]setContentView:[self view]]; //code in NSViewController
[NSApplication sharedApplication]mainWindow] // returns null
I tried to add
[window makeMainWindow];
in App Delegate class, but it won't help.
Did I miss something?
P.S. Also I'm using code below to call any delegate function in my class,
[(appDelegate *) [[NSApplication sharedApplication]delegate]MyMethod];
but I wonder is there something better, wihtout importing delegate class. Something like this
[[NSApplication sharedApplication]delegate]MyMethod];
(it gives warning)
For the mainWindow method the docs say:
This method might return nil if the application’s nib file hasn’t finished loading, if the receiver is not active, or if the application is hidden.
I just created a quick test application and I placed the following code:
NSLog(#"%#", [[NSApplication sharedApplication] mainWindow]);
into my applicationDidFinishLaunching:aNotification method, and into an action method which I connected to a button in the main window of my application.
On startup, the mainWindow was nil, but when I click the button (after everything is up and running and displayed), the mainWindow was no longer nil.
NSApplication provides other methods which you may be useful to you:
- windows - an array of all the windows;
– keyWindow - gives the window that is receiving keyboard input (or nil);
– windowWithWindowNumber: - returns a window corresponding to the window number - if you know the number of the window whose contents you wish to replace you could use this;
– makeWindowsPerform:inOrder: - sends a message to each window - you could use this to test each window to see if it's the one you are interested in.
With regard to calling methods on the delegate, what you say gives a warning works fine for me. For example, this works with no warnings:
NSLog(#"%#", [[[NSApplication sharedApplication]delegate] description]);
What exactly is the warning you receive? Are you trying to call a method that doesn't exist?
Fighting with MacOS just figured this out.
Apple's quote:
mainWindow
Property
The app’s main window. (read-only)
Discussion
The value in this property is nil when the app’s storyboard or nib file has not yet finished loading. It might also be nil when the app is inactive or hidden.
If you have only one window in your application (which is the most used case) use next code:
NSWindow *mainWindow = [[[NSApplication sharedApplication] windows] objectAtIndex:0];
Promise it won't be nil, if application has windows.
Swift flavored approaches for getting the main window (if present)
Application Main Window
guard let window = NSApplication.shared.mainWindow,
else {
// handle no main window present
}
// ... access window here
Application Window Array
let windowArray: [NSWindow] = NSApplication.shared.windows
guard windowArray.count > 0 else {
// hand case where no windows are present
}
let window = windowArray[0]
// ... access window here
If the window property isn't set yet, try delaying things until the app has finished loading, like so:
[myObject performSelector:#selector(theSelector) withObject:nil afterDelay:0.1];

Getting EXC_BAD_ACCESS, can't figure out how to fix it

Currently I'm learning Obj-C for Mac developing, with Cocoa. I made a simple file browser with an inspector, to see a file's icon an some info. So far, so good. Now I made it document based, so I could have more than one open windows.
To tell the inspector which file it should inspect, I use the NSWindowDidBecomeMainNotification. Works fine for switching between windows, but it gives an EXC_BAD_ACCESS when I close all windows and then open a new one.
This is the method that handles the notification:
- (void)windowChanged: (NSNotification *)notification
{
NSWindow *window = [notification object];
BrowserWindow *doc = [[window windowController] document];
if (currentDocument != doc) {
[currentDocument.arrayController removeObserver: self
forKeyPath: #"selectionIndex"];
[icon setImage:nil];
[size setStringValue:#"-"];
[owner setStringValue:#"-"];
[filename setStringValue:#"(none selected)"];
[doc.arrayController addObserver: self
forKeyPath: #"selectionIndex"
options: NSKeyValueObservingOptionNew
context: NULL];
currentDocument = doc;
}
}
The error occurs where it calls removeObserver:forkeyPath: on the currentDocument.arrayController. It kinda makes sense, it tries to remove the observer for something that doesn't exist anymore, 'cause the window is closed. But how to fix it? I just can't think of anything else..
Could someone point me in the right directions?
I appreciate the help! :)
--
It's getting weirder.. Just checked the example that was downloadable from the website of the book I've got, and they're using the same approach, but it works all fine. Can't find any differences, it's driving me crazy.
--
Solved! More details later.
Daniel is probably right: You probably don't retain currentDocument. Make currentDocument a property:
#property (retain) BrowserWindow *currentDocument;
And synthesize it in the implementation section:
#synthesize currentDocument;
And change your code to:
- (void) windowChanged: (NSNotification *) notification
{
NSWindow *window = [notification object];
BrowserWindow *doc = [[window windowController] document];
if (self.currentDocument != doc)
{
[self.currentDocument.arrayController removeObserver: self
forKeyPath: #"selectionIndex"];
[icon setImage: nil];
[size setStringValue: #"-"];
[owner setStringValue: #"-"];
[filename setStringValue: #"(none selected)"];
[doc.arrayController addObserver: self
forKeyPath: #"selectionIndex"
options: NSKeyValueObservingOptionNew
context: NULL];
self.currentDocument = doc;
}
}
You might want to do the same for icon, size, owner and filename.
And heed the warning of the message: you probably don't register self as observer to start with.
To tell the inspector which file it should inspect, I use the NSWindowDidBecomeMainNotification. Works fine for switching between windows, but it gives an EXC_BAD_ACCESS when I close all windows and then open a new one.
This is part of the problem right there. When the last window closes, no window will become main. So, you also need to handle the case where a window resigns main, as happens when it closes (and when another window becomes main).
Your inspector probably should both retain the document and switch documents after a delay, using a timer (whose fire date you postpone every time another did become/resign main notification comes in) or delayed perform (which you cancel and re-perform every time). When the timer/perform fires, find out what document, if any, is the active document, and update the inspector accordingly.
Also note that you can have no active document (no document window is the main window) even when there are documents open. The About panel and your Preferences panel are two good ways to achieve (and test) this.