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

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.

Related

NSWindow closing and controlling issues in Mac OS X

I met the issues of NSWindow regarding closing it when the application starts. There are plenty of examples, however, I can not get the proper effect, perhaps I missing something.
Firstly, in many examples there is the recommendation to use "[self window]" but I get the error like "No visible #interface for 'ViewController' declares the selector 'window'".
Then I use the round way: "[[self view] window]". Anyway, the window is not closed after the execution of the code:
NSWindow *win = [[self view] window];
[win performClose:self];
or
NSWindow *win = [[self view] window];
[win close];
The next one also does not give any results as I expect, according to the documentation:
[win orderOut:self];
The code compiles but I can see the window. Of course, I tried:
NSLog(#"%#", [win.windowController windowShouldClose:self] ? #"YES" : #"NO" );
It outputs "NO", so, it means that the window, which appear when I run my application, cannot be closed? Is there any way how to work around it? Why I cannot control that main window following the way the documentation suggests?
I checked for the import "#import AppKit/AppKit.h;" as well.
All of this suggests that win is nil. The view of your view controller is not in a window.

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.

Closing Window and Releasing NSWindowController

I have a relatively-lengthy task. So I bring up a separate window (NSWindowController) from AppDelegate to show progress. It goes like
//AppDelegate.m
if (self.progresswindow == nil) {
self.progresswindow = [[ProgressController alloc] initWithWindowNibName:#"ProgressController"];
}
[progresswindow showWindow:self];
//[[progresswindow window] setReleasedWhenClosed:NO];
[NSApp runModalForWindow:progresswindow.window];
When a task is complete, the progress window will close itself.
//ProgressController.m
[NSApp stopModal];
[self close];
It works fine. But when I click on a button to start another session of a task with the same window, the application won't run a task although it opens. It appears that the last instance hasn't be released. The progress window has the following lines.
- (void)windowDidLoad {
NSLog(#"Hey!");
}
And NSLog won't be called for the 2nd time. I wonder what I'm doing wrong? Calling setReleasedWhenClosed from AppDelegate has no effect. I have the Release When Closed checkbox enabled, anyway. I read something like I need to observe NSWindowWillCloseNotification the progress window in a different topic so that I can release it when it closes. But I'm using ARC. So I can't manually release it, can I? Meanwhile, if I open Apple's sample (TableViewPlayground), it seems that they use this notification. Furthermore, I've read this topic and this topic. But I don't know what the problem is.
I appreciate any advice. Thank you for your time.
Release the Progress-Window-Controller.

iCloud enabled - Stop the open file displaying on application launch?

I've just added iCloud support to an app that I am working on. Its working great, except that when I open the application without a document in focus the iCloud open file dialog appears and I don't want it to!
In my app delegate I have:
- (BOOL) applicationShouldOpenUntitledFile:(NSApplication *)sender
{
[mainWindowController.window makeKeyAndOrderFront:self];
return NO;
}
Which I use to show my own custom window. However now, both the iCloud open file dialog and my own dialog are displayed. Any ideas on how I can get rid of the iCloud dialog?
https://developer.apple.com/library/prerelease/content/releasenotes/AppKit/RN-AppKitOlderNotes/index.html
NSDocument Support for iCloud
In 10.8, NSDocument-based applications with a ubiquity-container-identifiers entitlement gain new functionality and UI to facilitate iCloud document management.
When iCloud is enabled and an application is first launched or re-activated and no windows are visible or being restored, instead of creating a new Untitled document, NSDocumentController will display a non-modal open panel showing the user's iCloud library.
...
Applications that do not wish to use these features for any or all of their NSDocument subclasses can override +[NSDocument usesUbiquitousStorage] and return NO. If all of the application's declared NSDocument subclasses return NO from this method, then NSDocumentController will never show the new non-modal open panel.
So if you can give up using the features listed in this release note, return NO at +[NSDocument usesUbiquitousStorage].
I confirmed you can still open/save your file into iCloud storage from the normal dialog.
Putting below codes in your App Delegate lets you bypass that iCloud pop up New Document screen. Tested for High Sierra.
-(void)applicationDidFinishLaunching:(NSNotification *)notification
{
// Schedule "Checking whether document exists." into next UI Loop.
// Because document is not restored yet.
// So we don't know what do we have to create new one.
// Opened document can be identified here. (double click document file)
NSInvocationOperation* op = [[NSInvocationOperation alloc]initWithTarget:self selector:#selector(openNewDocumentIfNeeded) object:nil];
[[NSOperationQueue mainQueue] addOperation: op];
}
-(void)openNewDocumentIfNeeded
{
NSUInteger documentCount = [[[NSDocumentController sharedDocumentController] documents]count];
// Open an untitled document what if there is no document. (restored, opened).
if(documentCount == 0){
[[NSDocumentController sharedDocumentController]openUntitledDocumentAndDisplay:YES error: nil];
}
}
- (BOOL) applicationShouldOpenUntitledFile:(NSApplication *)sender
{
[mainWindowController.window makeKeyAndOrderFront:self];
return NO;
}
This part is correct. I've just tested it.
Just make sure your that this class is really your app delegate.
Make a new class called prefixAppDelegate
In your MainMenu.xib, drag a new object to the side and set it's custom class to the app delegate class
Right click Application and drag from Delegate down to your app delegate object.
Now just paste the code above into your app delegate class
If that still doesn't help, try logging something in applicationShouldOpenUntitledFile:.
Also, I recommend not to set [mainWindowController.window makeKeyAndOrderFront:self]; in this method. You should rather use the app delegate method applicationDidFinishLaunching: method.
My observation and fix:
[applicationShouldOpenUntitledFile:] won't be executed except you remove Key NSDocumentClass from *-info.plist. But this is harmful if your app is document based application, it won't open the document type you linked.
My fix is open my customised window directly in -(void)applicationWillFinishLaunching:(NSNotification *)notification method (Application delegate)
ETDocumentWindowController *windowController = (ETDocumentWindowController*)get your own window controller here...;
[windowController.window makeKeyAndOrderFront:nil];
I thought I would share my solution to this issue as I see others still looking for an answer. Its not a great solution but it does the trick.
Subclass NSDocumentController and add the following:
+ (void) setCanOpenUntitledDocument: (BOOL) _canOpenUntitledDocument
{
canOpenUntitledDocument = _canOpenUntitledDocument;
} // End of setCanOpenUntitledDocument:
- (void) openDocument: (id) sender
{
// With iCloud enabled, the app keeps trying to run openDocument: on first launch (before apphasfinishedlaunching gets set.
// This method lets us check and see if the app has finished launching or not. If we try to open a document before
// its finished, then don't let it.
if(!canOpenUntitledDocument)
{
return;
} // End of appHasFinishedLaunching not set
[super openDocument: sender];
} // End of openDocument:
Add the following to your app delegate:
- (void) applicationDidFinishLaunching: (NSNotification *) aNotification
{
// Finished launching. Let us open untitled documents.
[SQLProDocumentController setCanOpenUntitledDocument: true];
...
}
And the reasoning -- By setting a breakpoint in openDocument I've found that its called before applicationDidFinishLaunching, applicationShouldOpenUntitledFile or applicationShouldHandleReopen:hasVisibleWindows: get called, meaning adding those methods is useless. Again, it's not great code but it works and does the trick. (None of the other solutions have worked for me).
I ran into a similar problem -- it turned out that in my case, I had to remove the NSDocumentClass key and value from my Info.plist in the CFBundleDocumentTypes array. Only then would the applicationShouldOpenUntitledFile: method get called and thus allow me to prevent the iCloud/Document window from opening.

Release a NSWindowController when the window is closed

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.
}