Should I need to unbind cocoa-bindings in dealloc of windowController? - objective-c

I have window controller and view controller that use core data bindings, but I want to be able to have those views really, truly get deallocated. I then want to reset the managedObjectContext and at that point have given up as much memory as possible.
I have found that I am required to unbind things which I've bound in the Nib, or the MOC keeps the Nib objects retained.
The issue is this EXEC_BAD_ACCESS trace:
If I do not unbind all of the bindings, even those created in Interface Builder, reset on the MOC causes an EXEC_BAD_ACCESS because bindings are attempting to reflect changes in the MOC in the view that's gone, through an array controller that should be gone, but isn't.
So I did this in the window controller's dealloc:
- (void) dealloc
{
NSLog(#"Wincon dealloc");
#autoreleasepool {
// Remove subview to ensure subview dealloc
[_viewController.view removeFromSuperviewWithoutNeedingDisplay];
// Tear down bindings to ensure MOC can reset
for (NSObject<NSKeyValueBindingCreation>* object in
#[_watcherAC, _watchersTimesTreeController, _watcherTableView, _itemsOutlineView])
{
for (NSString* binding in [object exposedBindings])
[object unbind:binding];
}
}
}
which is triggered this way:
- (void) switchToBackgroundMode
{
NSLog(#"SwitchToBackgroundMode");
// Hide the menu and dock icon
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
// Force every view to deallocate before reset
#autoreleasepool {
// Need to check loaded to prevent closing a closed window and
// triggering a second call to applicationShouldTerminateAfterLastWindowClosed
if ([self.wincon isWindowLoaded]) [self.wincon close];
self.wincon = nil;
}
NSLog(#"About to resetCoreDataStack");
[self resetCoreDataStack];
}
... and now I don't get any errors with that resetCoreDataStack
The stack trace above comes with a log file like this:
2014-05-29 15:54:35.794 MyApp[10230:303] Switch to BG in appShouldTerminate
2014-05-29 15:54:35.794 MyApp[10230:303] SwitchToBackgroundMode
2014-05-29 15:54:35.808 MyApp[10230:303] Wincon dealloc
2014-05-29 15:54:35.830 MyApp[10230:303] About to resetCoreDataStack
2014-05-29 15:54:35.830 MyApp[10230:303] Reset Core Data
{Exception thrown iff wincon dealloc doesn't unbind everything}
And so the window controller dealloc is definitely called when it's nilled in the autoreleasepool, but MOC reset causes an EXEC_BAD_ACCESS unless that wincon dealloc does unbind on a bunch of crap in the Nib.
So the question is:
Given a Nib owned by a custom window controller (self.wincon) with arrayController objects bound to an external managedObjectContext, what needs to be done to force everything in the Nib to be released and unbound? Is there some step that I'm missing that causes me to have to do this unbinding manually?
[EDIT] Some new debug code:
NSLog(#"Wincon dealloc");
#autoreleasepool {
// Remove subview to ensure subview dealloc
[_viewController.view removeFromSuperviewWithoutNeedingDisplay];
_viewController = nil;
self.window = nil;
}
#autoreleasepool {
// Tear down bindings to ensure MOC can reset
for (NSObject<NSKeyValueBindingCreation>* object in
#[_watcherAC, _watchersTimesTreeController, _watcherTableView, /*_itemsOutlineView*/])
{
NSLog(#"Bindings for %#", [object className]);
for (NSString* binding in [object exposedBindings]) {
NSLog(#"BI for %#: %#", binding, [object infoForBinding:binding]);
[object unbind:binding];
}
}
the log below is the bindingInfo for the bindings still alive when dealloc is called for the windowController
2014-05-29 21:00:39.967 SaleWatch[11249:303] Wincon dealloc
2014-05-29 21:00:39.975 SaleWatch[11249:303] Bindings for NSArrayController
2014-05-29 21:00:39.978 SaleWatch[11249:303] Bindings for NSTreeController
2014-05-29 21:00:39.989 SaleWatch[11249:303] BI for contentSet: {
NSObservedKeyPath = "selection.fetchTimesForOutlineView";
NSObservedObject = "[entity: SWWebStoreWatcher, number of selected objects: 1]";
}
2014-05-29 21:00:39.991 SaleWatch[11249:303] Bindings for NSTableView
2014-05-29 21:00:39.991 SaleWatch[11249:303] BI for selectionIndexes: {
NSObservedKeyPath = selectionIndexes;
NSObservedObject = "[entity: SWWebStoreWatcher, number of selected objects: 1]";
}
2014-05-29 21:00:40.001 SaleWatch[11249:303] BI for content: {
NSObservedKeyPath = arrangedObjects;
NSObservedObject = "[entity: SWWebStoreWatcher, number of selected objects: 1]";
}
2014-05-29 21:00:40.020 SaleWatch[11249:303] About to resetCoreDataStack
I forced wincon.window = nil in the new code, and these three objects still aren't nil, though the outlineView the treeController is for did become nil. There could be a retain cycle here, but I don't see how it'd be my fault... yet.

Related

Forcing an object to deallocate under ARC

I'm working on an iPad photo collage app that draws perhaps hundreds of UIImageViews on the screen at once.
There is a button that lets the user "re-create", which is suppose to run a for loop to [photo removeFromSuperview] on all photos and then initialize a new batch, in that order.
I'm using ARC, and my console tells me that my Photo's dealloc method isn't being called until AFTER the next batch has been drawn, meaning I'm running into memory issues, even though I'm trying to remove the first set before adding the next set.
Is there a way to either 1) wait until all the photos have been properly dealloc'd or 2) force all the photos to dealloc immediately under ARC?
You are probably putting your image views in an autorelease pool without realizing it. You may be able to fix this by wrapping your own autorelease pool around your for-loop.
For example, I made a very simple test project with one image view and one button under my top-level view. When I tap the button, it removes the image view and creates a new one. It removes the image view by looping over the top-level view's subviews. Here's the code:
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self initImageView];
}
- (IBAction)redoWasTapped:(id)sender {
[self destroyImageView];
[self initImageView];
}
- (void)destroyImageView {
for (UIView *subview in self.view.subviews) {
if ([subview isKindOfClass:[UIImageView class]]) {
[subview removeFromSuperview];
}
}
}
- (void)initImageView {
UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"picture.jpg"]];
imageView.frame = CGRectInset(self.view.bounds, 100, 100);
[self.view addSubview:imageView];
}
#end
When I ran this under the Allocations instrument with “Record reference counts” enabled, I saw that each removed image view was not deallocated during destroyImageView. Instead, it was deallocated later when the run loop called -[NSAutoreleasePool release].
Then I changed destroyImageView to manage its own autorelease pool:
- (void)destroyImageView {
#autoreleasepool {
for (UIView *subview in self.view.subviews) {
if ([subview isKindOfClass:[UIImageView class]]) {
[subview removeFromSuperview];
}
}
}
}
When I ran it again under Instruments, I saw that each removed image view was deallocated during destroyImageView, at the end of the #autoreleasepool block.
ARC deallocs any object to which there are no more strong references. So to dealloc something, simply set all the variables pointing to it to nil and make sure the object is not involved in any circular reference.

EXC_BAD_ACCESS when closing modal window ([NSWindow _restoreLevelAfterRunningModal]: message sent to deallocated instance)

In my main ViewController I have the following code:
- (IBAction)listFunctions:(id)sender //button clicked
{
FunctionListController *functionListController = [[FunctionListController alloc] initWithWindowNibName:#"FunctionList"];
NSWindow *functionListWindow = [functionListController window];
[NSApp runModalForWindow: functionListWindow];
NSLog(#"done");
}
FunctionListController is the File's Owner of FunctionList.nib and a subclass of NSWindowController and implements the protocol NSWindowDelegate.
Here is the implementation of FunctionListController:
#implementation FunctionListController
- (id)initWithWindow:(NSWindow *)window
{
self = [super initWithWindow:window];
if(self)
{
// Initialization code here.
}
return self;
}
- (void)windowDidLoad
{
[super windowDidLoad];
// Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
self.window.delegate = self;
}
- (void)windowWillClose:(NSNotification *)notification
{
[NSApp stopModal];
}
#end
When the modal window is closed, the NSLog(#"done"); runs and displays, however after listFunctions is done, I get a EXC_BAD_ACCESS error.
With NSZombiesEnabled I get the error [NSWindow _restoreLevelAfterRunningModal]: message sent to deallocated instance.
Edit:
I am using ARC.
Try [functionListWindow setReleasedWhenClosed:NO] and hold a strong reference to your window until closing.
In your listFunctions method, you first create a FunctionListController object:
- (IBAction)listFunctions:(id)sender //button clicked
{
FunctionListController *functionListController = [[FunctionListController alloc] initWithWindowNibName:#"FunctionList"];
which is referenced through a local variable; it will be released at the end of the scope (the method itself);
you then get a reference to the functionListController window and run it as a modal:
NSWindow *functionListWindow = [functionListController window];
[NSApp runModalForWindow: functionListWindow];
This object will be retained by the NSApp.
However, the method exits (runModalForWindow will not block your thread) and functionListController is deallocated:
NSLog(#"done");
}
so you get a dangling reference and a modal window owned by an object which is not longer there. Hence, then crash.
Simply, make functionListController a strong property of your class and it will work.
Your new listFunctions would look like:
- (IBAction)listFunctions:(id)sender //button clicked
{
self.functionListController = [[FunctionListController alloc] initWithWindowNibName:#"FunctionList"];
...
Your functionListWindow is a local variable in your listFunctions: method. When that method finishes executing, you will lose any strong reference you have to that object, so nothing will own it and it will be deallocated. When your modal window actually closes, it tries to send the appropriate message to its delegate, however this no longer exists.
Have you tried making functionListWindow an instance variable on your main view controller?

NSFetchedResultsController keep reference to deallocated delegate controller in storyboard, causing crash

I have a simple UIViewTable, with a drill-down detail realized with a UINavigationController push segue in a storyboard.
It happen from time to time, that the table view controller seems to gets deallocated, while I am in the detail view, therefore I get the famous:
[MyViewController controllerWillChangeContent:]: message sent to deallocated instance
I explain better, I have an NSOperation queue which load my data asynchronously, and fill the table as soon as it just finished. The data are correctly retrieved and the table filled.
For the detail view, I am clicking on a cell and passing the NSManagedObjectID to the destination controller in prepareForSegue method. Very randomly when I made a change to the detail view the fetched controller loose its delegate, or as it seems, the delegate itself which is the controller gets deallocated. Causing a crash.
The fetched results controller is declared as a property:
#property(nonatomic,strong) NSFetchedResultsController *fetchedResultsController;
Then this is how everything is working starting from viewDidLoad.
- (void)viewDidLoad {
[super viewDidLoad];
[self loadDataAsynchronously];
}
-(void)loadDataAsynchronously {
NSOperationQueue *queue = [NSOperationQueue new];
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self
selector:#selector(loadData)
object:nil];
[queue addOperation:operation];
}
-(void)loadData {
NSFetchRequest *findAllEntities = [[NSFetchRequest alloc] init];
[findAllEntities setEntity:ENTITY_DESC];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"created" ascending:YES];
[findAllEntities setSortDescriptors:[NSArray arrayWithObject:sort]];
[findAllEntities setFetchBatchSize:20];
[NSFetchedResultsController deleteCacheWithName:#"MyCache"];
if(self.fetchedResultsController==nil) {
self.fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:findAllPlants
managedObjectContext:MOC
sectionNameKeyPath:nil
cacheName:#"MyCache"];
self.fetchedResultsController.delegate=self;
}
NSError *error=nil;
if (![FRC performFetch:&error]) {
exit(EXIT_FAILURE);
}
[self.dataTableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:YES];
}
this code works, and most of the time also works within the detail view which is called as a segue like this:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
IP2SegueIdentifier segueIdentifier = [IP2Factory segueSolver:[segue identifier]];
MyDestinationViewController *dvc = [segue destinationViewController];
NSIndexPath *indexPath = [TV indexPathForSelectedRow];
dvc.entityID=[[self.fetchedResultsController objectAtIndexPath: indexPath] objectID];
}
and the destination controller correctly get the entity id and reconstruct the object by asking the context.
Then, when I am in the detail view controller, I can make change to the entity, and when I go back to the navigation hierarchy I do a context save.
It is at this point that the application crashes, just right at context save. Not so often, but from time to time.
Because the fetched results controller recognize the change and submitted to its delegate which is already deallocated.
I have few doubts at this point, I am using iOS 5 and ARC so the compiler is supposed to have (almost) full control over release and dealloc methods. And I am also using a storyboard with a simple navigation hierarchy, which should guarantee that the whole previous view controllers chain gets retained.
I also run the profiler for memory leak/zombies analysis, but wasn't able to spot anything wrong, on the contrary I was happy the all the objects management were fine.
I have not many guess at this point, so please feel free to point out something I could have forgotten to check, or something you see wrong in my code.
thanks
First, a note about ARC. While ARC provides auto-zeroing weak pointers, it does not make assign pointers auto-zeroing. NSFetchResultsController uses an assign property for its delegate (see NSFetchedResultsController.h):
#property(nonatomic, assign) id< NSFetchedResultsControllerDelegate > delegate;
It is still your responsibility to clear yourself as the delegate before you deallocate. You typically do this in dealloc:
- (void)dealloc {
_fetchedResultsController.delegate = nil;
}
You may also want to get rid of your fetchedResultsController in viewWillDisappear: (including removing yourself as the delegate). Typically you do not want fetch requests to stay around when you are offscreen. (If you do, you probably should manage the fetch in a model object rather than in a view controller, since a view controller can go away anytime its view is offscreen.)
Your loadData is strange in that it creates a findAllEntities, but actually uses findAllPlants. Was this a typo or a bug? If there is a separate findAllPlants fetch request in an ivar, this could also be a cause of your problems.

UINavigationControllers: How to pass value to higher (parent?) controller in stack?

I have SendingController which push to nav stack SendingDeatilsController (which one has a TableView). User should should pick in TableView one row (it checked by Checkmark) and I would like to pass the value of this row (let it will NSString object) to the SendingController.
How can I realize this behaviour in my application? And is SendingController parent for SendingDetailController (attribute parentController of SDC refers to SC) ??
If you want to implement this behaviour, pass the SendingDetailController a reference to the previous view controller. This way the detail view controller can send a message to the previous one on the stack.
In your SendingDetailController define a weak reference :
// in .h
SendingController *sendingController;
#property(assign) SendingController *sendingController;
// in .m
#synthesize sendingController;
-(void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// retrieve the string and send the message
[sendingController didSelectString:theString];
}
Now before pushing the SendingDetailController on the stack don't forget to set its sendingController property.
// .m
// where you push the vc
if(!sendingDetailController) {
sendingDetailController = [[SendingDetailController alloc]
initWithNibName:#"TheNIBName"
bundle:nil];
sendingDetailController.sendingController = self;
}
[self.navigationController pushViewController:sendingDetailController
animated:YES];
and write the method that will recieve the string.
-(void)didSelectString:(NSString *)aString {
// do anything with string
[self.navigationController popViewControllerAnimated:YES];
}
This should do the job.
For easy asynchronous communication between different UIViewControllers, you may want to look at NSNotification and NSNotificationCenter.
There are plenty tutorials on the web and some good answers here at SO that can show you how to do that exactly.

Memory managment question

I have a button with IBAction, which shows another window:
-(IBAction)someButtonClick:(id)sender
{
anotherView = [[NSWindowController alloc] initWithWindowNibName:#"AnotherWindow"];
[anotherView showWindow:self];
}
I worry about memory management in here. I allocate an object in this IBAction and don't released it. But how can i do it? If i released this object after showing, window will closing immediately.
The view is stored in an instance variable and you have access to it anywhere in your class. Release it in the code that dismisses the view.
Since anotherView is an instance variable you can release it in your dealloc method. But then you still have a memory leak, since every time your button is clicked a new instance of the window controller is created, but only the last one can be freed. You really should use accessors for this. Here is my suggestion:
- (NSWindowController *) anotherView;
{
if (nil == anotherView) {
anotherView = [[NSWindowController alloc] initWithWindowNibName:#"AnotherWindow"];
}
return anotherView;
}
- (void) setAnotherView: (NSWindowController *) newAnotherView;
{
if (newAnotherView != anotherView) {
[anotherView release];
anotherView = [newAnotherView retain];
}
}
- (void) dealloc;
{
[self setAnotherView: nil];
[super dealloc];
}
- (IBAction) someButtonClick: (id) sender;
{
[[self anotherView] showWindow: self];
}
If you use a Objective-C 2.0 property you don't have to write the setter.
And also you should rename your instance variable, the name should reflect what it is. And a View is not a Window Controller.