I have a NSDocument based macOS app with Core Data which by its nature can only ever have one document open at a time. Therefore, when a new document is opened I close the currently open one.
All document related UI is in a separate window controller and everything works well.
But I also have a menu bar item that toggles a separate window which displays some information about the document. The UI is a simple NSTableView bound to an NSArrayController. The array controller's managagedObjectContext property is set when the current document changes. This always leads to a crash with EXC_BAD_INSTRUCTION.
To narrow down the issue I completely removed all bindings and any other manipulation of the array controller. The crash is gone.
I also created a new testArrayController in code to see what happens there and sure enough I could reproduce the crash:
let testArrayController = NSArrayController()
var document: Document? {
didSet {
if document != nil {
testArrayController.managedObjectContext = document?.managedObjectContext
testArrayController.prepareContent() // <---- this causes the crash later on
} else {
testArrayController.managedObjectContext = nil
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
testArrayController.entityName = "MyEntity"
...
}
It seems that calling prepareContent() somehow locks the array controller to a specific managedObjectContext and causes the crash when it is set to nil.
How can I safely "deactivate" an NSArrayController, or change its managedObjectContext?
After lots of experimenting I think I found out that NSArrayController produces a memory leak as soon as you call fetch(_:) or preopareContent(). It seems that it retains its managedObjectContext and never releases it. Even though all other references to the controller have been released I could see the leaked instance in the memory debugger.
I worked around the issue by replacing bindings with a regular NSTableViewDataSource implementation.
Related
NSWindows can be made restorable so that their configuration is preserved between application launches.
https://developer.apple.com/documentation/appkit/nswindow/1526255-restorable
Windows should be preserved between launch cycles to maintain interface continuity for the user. During subsequent launch cycles, the system tries to recreate the window and restore its configuration to the preserved state. Configuration data is updated as needed and saved automatically by the system.
In a new macOS project, the NSWindow on a Storyboard is restorable by default:
My problem comes when embedding an NSTabViewController in the NSWindow.
The NSTabView is inheriting the window's restorable state automatically, with no added code.
This makes the selected tab persist between app launches. I don't want that. I want it to always default to index 0. If the selected tab is restored, attempting to select a tab programmatically in viewDidLoad has unexpected results.
How can I force certain AppKit UI elements to be excluded from NSWindow state restoration?
I want the Tab View to be un-restorable.
But I would like to keep other restorable benefits, such as restoring the previously-set window size.
How can single views be excluded from NSWindow state restoration?
AFAIK, you cannot exclude a certain part of the UI from being restorable. It is an either ON or OFF thing for all elements. That's why I rarely use Apple's own restorability APIs, as more often than not, they are unreliable. I always do the restoration myself to get that fine control that you need. For simpler windows, however, I let the system do the restoration.
After this preamble, and to really answer your question, I rarely use viewDidLoad() to set up any windows, because as you found out that has some nasty consequences (e.g., the window might not exist yet!). I always do that in viewWillAppear(). For that to happen, you need to set up the following:
You need to have an ivar (let's call it tabViewController) to your NSTabViewController instance in your parent NSViewController (let's call it NSViewMainController)
Override prepare(for segue: NSStoryboardSegue, sender: Any?) in NSViewMainController and set up the NSTabViewController and its NSViewController children like this:
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
// set up the tabViewController ivar
self.tabViewController = segue.destinationController as? NSTabViewController
// set up the child NSViewControllers if you need to access them via their parent (otherwise this step is not needed)
if let childControllers = tabViewController?.children {
for controller in childControllers {
if let controller = controller as? NSViewController1 {
childController1 = controller
}
else if let controller = controller as? NSViewController2 {
childController2 = controller
}
else if let controller = controller as? NSViewController3 {
childController3 = controller
}
}
}
}
Override viewWillAppear() of NSViewMainController and then set up the desired tabView:
guard let controller = tabViewController else { return }
controller.selectedTabViewItemIndex = 0
Major caveat: Beware of viewWillAppear(), though... Unlike viewDidLoad(), this override can be called multiple times, and thus you need to take that into account in your code and react appropriately.
The key to state restoration is the NSResponder method restoreStateWithCoder:
This method is part of the window restoration system and is called at launch time to restore the visual state of your responder object. The default implementation does nothing but specific subclasses (such as NSView and NSWindow) override it and save important state information. Therefore, if you override this method, you should always call super at some point in your implementation.
https://developer.apple.com/documentation/appkit/nsresponder/1526253-restorestate
So, to not restore a certain control, make this method a no-op.
It says that you "should always call super", but that restores the window state. So if you don't want window restoration, don't call super.
In the case of a Tab View, it evidently must be done in the NSTabView (subclass) itself. In other views, overriding this method on the View Controller may work.
class SomeTabView: NSTabView {
override func restoreState(with coder: NSCoder) {
// Do NOT restore state
}
}
I'm working with an NSWindowController to implement a preferences window. Apple's documentation states that by default the controller and window aren't deallocated, because it's useful to not have to reload everything, which makes sense. But their documentation goes on to say that you can override that behavior, but not explain how.
Apple's Docs:
When a window is closed and it is part of a document-based
application, the document removes the window’s window
controller from its list of window controllers. This results
in the system deallocating the window controller and the
window, and possibly the NSDocument object itself. When a
window controller is not part of a document-based application,
closing the window does not by default result in the
deallocation of the window or window controller. This is the
desired behavior for a window controller that manages something
like an inspector; you shouldn’t have to load the nib file
again and re-create the objects the next time the user requests
the inspector.
If you want the closing of a window to make both
window and window controller go away when it isn’t
part of a document, your subclass of NSWindowController
can observe the NSWindowWillCloseNotification notification
or, as the window delegate, implement the windowWillClose: method.
I can't find anywhere that explains what to "implement" in the windowWillClose: method.
The window controller can be seen here:
https://github.com/gngrwzrd/gwpreferences/blob/master/GWPreferences/GWPreferences/GWPreferences/GWPrefsWindowController.m
Using the controller can be seen here:
https://github.com/gngrwzrd/gwpreferences/blob/master/GWPreferences/GWPreferences/GWAppDelegate.m - you can see in this code where I'm trying some bridge casting to try and force release objects but it doesn't work.
So the GWPrefsWindowController.dealloc method never gets called. Any ideas?
I understand this question is old, but for those who came here from google, the answer is quite simple.
As stated in the documentation, for non document base applications, you can simply:
Keep a reference for your NSWindowController wherever your are calling it. (In the example below it's referenced by myWindowController;
Make the class calling your NSWindowController implement the protocol NSWindowDelegate;
Release your Window Controller by setting it to nil on windowWillClose: method
To answer the question more precisely. When lazy instantiating your controller, set your class as the delegate:
-(IBAction)showMyWindowAction:(id)sender
{
// If my window controller is not nil
if (!myWindowController)
{
//instantiate it
myWindowController = [[MyWindowController alloc] initWithWindowNibName:#"myWindow"];
// set your class as delegate
[myWindowController setDelegate:self];
}
[myWindowController.window orderFront:self];
}
And then implement the windowWillClose: method from the NSWindowDelegate protocol
-(void)windowWillClose:(NSNotification *)notification
{
//Check if it's the right window that will close
if ([notification.object isEqualTo:myWindowController.window])
{
//Set your controller to nil
myWindowController = nil;
}
}
That's it, your window controller will now dealloc and since we are verifying if it's controller is nil before showing the window, everything will work!
I believe the reason why this is not implemented by default is because the initWithWindowNibName: is a somewhat heavy operation, and thus you have to think if dealloc'ing whatever is on your window will impact more or less than loading your window nib file.
I hope it helped
I have a Cocoa app with ARC enabled. I am using OS 10.8 and Xcode 4.4. The app sporadically crashes upon calls to CGLayerRelease, dumping messages like the following to console:
error for object 0x10a2d2000: entry for pointer being freed from death-row vanished
If I comment out the calls to CGLayerRelease the crashing stops. Do I need to call CGLayerRelease in ARC enabled projects? The documentation isn't very clear about this.
It is hard to post code for this because it is spread throughout a large file for a class that inherits from NSView. The class has a data member CGLayerRef layer;. Then inside the drawRect function:
if (layer == nil) {
layer = CGLayerCreateWithContext(context, self.frame.size, NULL);
// draw some stuff into CGLayerGetContext(layer)
}
CGContextDrawLayerInRect(context, self.bounds, layer);
The class is also a delegate of its window and has the following function
- (void)windowDidEndLiveResize:(NSNotification *)notification {
if (layer) {
//CGLayerRelease(layer);
layer = nil;
}
[self setNeedsDisplay:YES];
}
Also inside a property setter
- (void)setPitch:(NSArray *)aPitch {
pitch = aPitch;
if (layer) {
//CGLayerRelease(layer);
layer = nil;
}
}
Now if I uncomment the calls to CGLayerRelease then I sporadically get the crash mentioned above. I stress "sporadically" because it does not always happen.
Yes, you need to call CGLayerRelease() in ARC. The above assignments look correct, but I suspect you may have others. You're directly accessing your ivars, which is the #1 cause of these kinds of crashes. Do not access your own ivars except in init and dealloc. Create an accessor for setLayer: that correctly releases the old one and creates a new one and always use that.
It's very possible that your CGLayer code is fine, and that it's something else like a CGPath that you're adding to the CGLayer. Move all your ivars to accessors. Otherwise you will be chasing these kinds of bugs forever.
I have several panels that contain NSTextField controls bound to properties within the File's Owner object. If the user edits a field and then presses Tab, to move to the next field, it works as expected. However if the user doesn't press Tab and just presses the OK button, the new value is not set in the File's Owner object.
In order to workaround this I have set Updates Continuously in the binding, but this must be expensive (EDIT: or at least it's inelegant).
Is there a way to force the bind update when the OK button is pressed rather than using Updates Continuously?
You're right that you don't need to use the continuously updates value option.
If you're using bindings (which you are), then what you should be doing is calling the -commitEditing method of the NSController subclass that's managing the binding. You'd normally do this in your method that closes the sheet that you're displaying.
-commitEditing tells the controller to finish editing in the active control and commit the current edits to the bound object.
It's a good idea to call this whenever you are performing a persistence operation such as a save.
The solution to this is to 'end editing' in the action method that gets called by the OK button. As the pane is a subclass of NSWindowController, the NSWindow is easily accessible, however in your code you might have to get the NSWindow via a control you have bound to the controller; for example NSWindow *window = [_someControl window].
Below is the implementation of my okPressed action method.
In summary I believe this is a better solution to setting Updated Continuously in the bound controls.
- (IBAction)okPressed:(id)sender
{
NSWindow *window = [self window];
BOOL editingEnded = [window makeFirstResponder:window];
if (!editingEnded)
{
logwrn(#"Unable to end editing");
return;
}
if (_delegateRespondsToEditComplete)
{
[_delegate detailsEditComplete:&_mydetails];
}
}
Although this is really old, I absolutely disagree with the assumption that this question is based on.
Countinously updating the binding is absolutely not expensive. I guess you might think this updates the value continuously, understanding as "regularly based on some interval".
But this is not true. This just means it updates whenever you change the bound value. This means, when you type something in a textView, it would update as you write; this is what you'd want in this situation.
I created new Document-Based-App.
I implemented dataOfType in subclass of NSDocument
- (NSData*) dataOfType:(NSString *)typeName error:(NSError *__autoreleasing *)outError
{
return [NSKeyedArchiver archivedDataWithRootObject:bcmwc.bindingsController.arrangedObjects];
}
in xib http://i.minus.com/iH2Rj9v5oOhTn.png
When I click "Save" from menu nothing's gonna happen, any errors in console.
I set a breakpoint in dataOfType, and when I clicked "Save", application didn't stop.
Any suggestions?
EDIT
I think it may be connected with fact I use custom NSWindowController, and custom xib.
I made a test when I load custom xib, everything is fine dataOfType method is invoked etc..
Should I connect in some way my custom xib (window) with subclass of NSDocument?
It looks like your Save menu item is connected properly, so let's concentrate on the code (+1 for having posted it in the first place).
You do nothing in your code to ensure -archivedDataWithRootObject: returns valid data or set an error if it doesn't. My best guess (because you haven't provided nearly enough detail to do anything but guess) is that you're returning nil because your call to -archivedDataWithRootObject: is doing the same. Check to see if you get valid data and set the outError if not.
Why would you get nil? Perhaps one or more of the objects in the object graph created by archiving your array controller's -arrangedObjects array isn't <NSCoding> compliant. This is likely the case if your array controller holds objects of a custom class you created rather than a standard Property List container.
Read the Archives and Serializations Programming Guide (in particular, the Encoding and Decoding Objects section) to learn how to make your custom class <NSCoding> compliant so that it knows how to serialize itself (write itself into a byte stream suitable for NSKeyedArchiver, etc. and create an instance of itself from such a byte stream).
Also, you really need to learn to use the debugger. You're groping around in a dark cave with lots of pitfalls and no flashlight without it. Try setting a breakpoint in methods you expect to be called. If they're not called, you can check outlets/actions, etc. If they are, you can step through each line and make sure everything is executing as you expected. If you write a little more verbose code, you can more easily inspect the results when paused in the debugger. Two lines in your case will help you more than one:
- (NSData*) dataOfType:(NSString *)typeName error:(NSError *__autoreleasing *)outError
{
NSData * myData = [NSKeyedArchiver archivedDataWithRootObject:bcmwc.bindingsController.arrangedObjects];
// You should be handling nil (setting a descriptive error) here if (!myData)...
return data; // breakpoint here; you should now see myData is likely nil
}