Better way to manage lifetime of instance created in class method - objective-c

In a pet application I'm writing, on the main window, I have a few custom views aligned under each other, each with a label, a combobox and a button.
Clicking the button invokes code that finds the combobox in the same view, and then calls the following function (a class method of RVListEditorController):
+ (void) editComboBox: (NSComboBox *) aComboBox
{
// Analyze says "possible leak", but both methods ending the panel will release the controller.
RVListEditorController *controller = [[RVListEditorController alloc] initWithComboBox: aComboBox];
[NSApp beginSheet: [controller window]
modalForWindow: [aComboBox window]
modalDelegate: controller
didEndSelector: NULL
contextInfo: nil];
}
The code creates an instance of RVListEditorController. That controls a panel that allows me to edit the list in the combobox (remove items, sort items, etc.). It has, among other controls, two buttons that close it, Cancel and OK.
The code for the two buttons is:
- (IBAction) closeSheetWithOK: (id) sender
{
[NSApp endSheet: editingPanel];
[editingPanel orderOut: self];
[comboBoxValues setArray: valuesCopy];
if (comboBoxValues.count > 0)
[editedComboBox setStringValue: [comboBoxValues objectAtIndex: 0]];
[self release];
}
- (IBAction) closeSheetWithCancel: (id) sender
{
[NSApp endSheet: editingPanel];
[editingPanel orderOut: self];
[self release];
}
These are the only two buttons that close the sheet. My question is about the lifetime management of the instance. It is allocated in the class method, but then control is handed to Cocoa again and the class method ends. The only place I could find to release the instance is in the two handlers for the closing buttons. My problem is that beginSheet:modalForWindow:modalDelegate:didEndSelector:contextInfo: doesn't simply open the sheet and then waits until it closes again, returning a value how it was closed. If that were the case, I could close the instance in the class method, and I would feel better.
My question: Is there perhaps a better way to handle the lifetime of the instance, or is there something in Cocoa that allows me to open a sheet window-modally and then wait for it to close again, so I could release the instance right after that? I can't think of any, but I am a relative newbie, after all.
FWIW, the code works, so there are no errors. I am simply not very happy with the construct that I allocate something in a class method that must then be released in two instance methods of itself.

That looks to me like something which should not be a class method and the problems you are having defining its lifecycle are a warning sign that it is being created without clear ownership.

I am simply not very happy with the construct that I allocate something in a class method that must then be released in two instance methods of itself.
There is a certain logic to that - but I would also claim that a window-modal sheet would more naturally be initiated by an instance method. The window is after all a representation of some object, not just a class.
That didn't answer your more general question about life cycles, though.

I managed to get something that worked to my satisfaction. I provided beginSheet: with a method to be called after the sheet ended, giving controller as the context info. IOW:
[NSApp beginSheet: [controller window]
modalForWindow: [aComboBox window]
modalDelegate: controller
didEndSelector: #selector(sheetDidEnd:returnCode:contextInfo)
contextInfo: (void *)controller];
}
The code for the two buttons is now:
- (IBAction) closeSheetWithOK: (id) sender
{
[comboBoxValues setArray: valuesCopy];
if (comboBoxValues.count > 0)
[editedComboBox setStringValue: [comboBoxValues objectAtIndex: 0]];
[NSApp endSheet: editingPanel];
}
- (IBAction) closeSheetWithCancel: (id) sender
{
[NSApp endSheet: editingPanel];
}
and the code for sheetDidEnd:returnCode:contextInfo: is:
- (void) sheetDidEnd: (NSWindow *) sheet returnCode: (NSInteger) returnCode contextInfo: (void *) contextInfo
{
[sheet orderOut: (id)contextInfo];
[(id)contextInfo release];
}
That is, IMO, the best that can be done for situations like this. The procedure would have been the same if this had been called from an instance method of the window controller, AFAICT.

Related

NSPopover loses first responder

I have a NSPopover in my application with various buttons. One button allows the user to tweet using the NSSharingService:
NSArray* array = #[ #"Tweet something"];
NSSharingService* sharingServiceFB = [NSSharingService sharingServiceNamed:NSSharingServiceNamePostOnTwitter];
[sharingServiceFB performWithItems:array];
This works well, but when the tweet has been sent or cancelled, the focus is returned to the main application (main window) and not the NSPopover. How can I return focus to the NSPopover?
My initial approach was to observe NSWindowDidBecomeKeyNotificationwhich calls a method when notification has been received that does the following
if (self.sheetPopover!=nil){
[self.sheetPopover becomeFirstResponder];
}
However, this did not work as expected and I still have to click twice on the NSPopover to regain focus. Any suggestions as to how to fix this? Thanks.
I found a solution that worked for me. I made my NSPopover a delegate of NSSharingServiceDelegate, and implemented two of the delegate methods where I reset the first responder when the twitter view had closed. Here, self is the NSPopover view.
- (void)sharingService:(NSSharingService *)sharingService didShareItems:(NSArray *)items
{
[self becomeFirstResponder];
[[self window] becomeKeyWindow];
[[self window] becomeMainWindow];
}
- (void)sharingService:(NSSharingService *)sharingService didFailToShareItems:(nonnull NSArray *)items error:(nonnull NSError *)error
{
[self becomeFirstResponder];
[[self window] becomeKeyWindow];
[[self window] becomeMainWindow];
}
Comments as to whether this is the best approach are very welcome.

Cocoa NSOpenGLView - [win setDelegate:view ] shows error. How to delegate manually?

I'm programming in Eclipse (not Xcode) on Yosemita 10.10...
I try to catch MouseMoved event, but it not called (mouseDown, mouseDragged - works fine). So I'm using this example code from here
http://lists.apple.com/archives/mac-opengl/2003/Feb/msg00069.html
but compiller show error on
[app setDelegate: view];
(- cannot initialize a parameter of type 'id' with an lvalue of type 'NSView *')
If I comment this line - it's work, but mouseMoved don't calling.
Please help! I'm newbie in objective-c
OS X does not automatically track the mouse movement events for you unless you request them.
In order to receive mouseMoved: events, you should add an NSTrackingArea to your subclass of NSOpenGLView. For example:
- (void)awakeFromNib {
NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:self.frame options:NSTrackingActiveAlways|NSTrackingMouseMoved owner:self userInfo:nil];
[self addTrackingArea:trackingArea];
}
After that, your mouseMoved: method will be called.
- (void)mouseMoved:(NSEvent *)theEvent {
NSLog(#"moved");
}
You can optionally implement updateTrackingAreas if you need to update your tracking area manually when the view resizes. For details, please refer to Using Tracking-Area Objects.

Creating a Controller for NSOutlineView

I'm having an issue with regards to creating a separate Controller class for an NSOutlineView I have.
I've created a new class named LTSidebarViewController and in my MainMenu.xib file I've added an Object to the 'workbench' and linked it to my LTSidebarViewController class. I've also set the delegate and datasource to be linked to the NSOutlineView in MainMenu.xib.
What I am looking to do is create an instance of this class from within - (void)applicationDidFinishLaunching:(NSNotification *)aNotification in my AppDelegate file and when I do so I want to pass in the App Delegate's managedObjectContext. So, I've created a custom init method in LTSidebarViewController which looks like so:
-(id)initWithManagedObject:(NSManagedObjectContext*)managedObject{
self = [super init];
if (self) {
self.managedObjectContext = managedObject;
NSFetchRequest *subjectsFetchReq = [[NSFetchRequest alloc]init];
[subjectsFetchReq setEntity:[NSEntityDescription entityForName:#"Subject"
inManagedObjectContext:self.managedObjectContext]];
subjectsArray = [self.managedObjectContext executeFetchRequest:subjectsFetchReq error:nil];
_topLevelItems = [NSArray arrayWithObjects:#"SUBJECTS", nil];
// The data is stored in a dictionary
_childrenDictionary = [NSMutableDictionary new];
[_childrenDictionary setObject:subjectsArray forKey:#"SUBJECTS"];
// The basic recipe for a sidebar
[_sidebarOutlineView sizeLastColumnToFit];
[_sidebarOutlineView reloadData];
[_sidebarOutlineView setFloatsGroupRows:NO];
// Set the row size of the tableview
[_sidebarOutlineView setRowSizeStyle:NSTableViewRowSizeStyleLarge];
// Expand all the root items; disable the expansion animation that normally happens
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0];
[_sidebarOutlineView expandItem:nil expandChildren:YES];
[NSAnimationContext endGrouping];
// Automatically select first row
[_sidebarOutlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
}
return self;
}
I also have all the required methods in this class, - (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item etc.
Inside the - (void)applicationDidFinishLaunching:(NSNotification *)aNotification method in the App Delegate, I have the following:
LTSidebarViewController *sidebarViewController = [[LTSidebarViewController alloc] initWithManagedObject:self.managedObjectContext];
My problem is that this isn't working, I don't get any errors and the app runs but no data is displayed in the NSOutlineView.
Now from what I can tell the problem is that when the MainMenu.xib file is initially loaded, it's automatically creating an instance of my LTSidebarViewController class and calling it's init method but because my init method isn't doing anything the app doesn't finish launching correctly.
Am i taking the correct approach here? In simple terms all I'm looking for is to have a separate file that is used as the datasource for my NSOutlineView.
When working with NSOutlineView I generally put in extreme amounts of logging to figure out what's going on. I would probably do something like the following (maybe you have already done some of this):
Make sure you really have data in subjectsArray by logging it, e.g.
NSLog(#"subjectsArray");
NSLog(#"%#", subjectsArray);
Make sure you have implemented the NSOutlineView Datasource protocol methods from NSOutlineView Datasource Methods in your AppDelegate.m file and that they're returning the appropriate data.
If you need help implementing these, try a tutorial such as Source Lists and NSOutlineView.
I usually wind up with NSLog statements in each of the NSOutlineView data source methods to make sure they are being called and that I understand what each is expecting and returning.
Make sure your delegate and datasource are not nil for some reason in your initWithManagedObject:(NSManagedObjectContext *)managedObject method by logging them, e.g.
NSLog(#"datasource: %#", [self datasource]);
NSLog(#"delegate: %#", [self delegate]);
If you find that for some reason they are nil, you could manually set them just to make sure that's not the problem, e.g. in initWithManagedObject:
[self setDelegate: [NSApp delegate]];
[self setDatasource: [NSApp delegate]];
As far as whether this is the "correct" approach: I'm not clear from your code whether you're intending that the sideBarController is both the delegate and the datasource or whether the AppDelegate is serving those roles. Obviously, you'll need to implement the delegate and datasource protocols in the appropriate files. You certain can have AppDelegate serve those roles, although it seems to make more sense to have your sideBarController do that.
A small note: I sometimes access AppDelegate's managedObjectContext directly from supporting files with something like
-(NSManagedObjectContext *)managedObjectContext
{
return [[NSApp delegate] managedObjectContext];
}
rather than passing the managedObjectContext in manually to every file.

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?

Best design pattern for a window opening another window in cocoa application

I am learning how to create osx applications with Cocoa/Objective-C. I am writing a simple app which will link together two different tutorials I have been going through. On start up a choice window loads with 2 buttons, one button loads one window and the other loads the other window. When either button is clicked the choice window closes.
The choice window controller object was added to the MainMenu.xib file so it is created at launch. The window is then opened using the awakeFromNib message.
I want the result of one button to open up the 'track controller' tutorial application from the ADC website. The action looks like this:
- (IBAction)trackButton:(id)sender {
TMTrackController *trackController = [[TMTrackController alloc] init];
[self.window close];
}
I added an init method to the TMTrackController class which looks like this:
- (id) init {
if (self = [super init]) {
[self showWindow];
TMTrack *myTrack = [[TMTrack alloc] init];
myTrack.volume = 50;
self.track = myTrack;
[self updateUserInterface];
return self;
}
else {
return nil;
}
}
- (void) showWindow {
if(!self.window) {
[NSBundle loadNibNamed:#"trackWindow" owner:self];
}
[self.window makeKeyAndOrderFront:self];
}
I am not sure this is the best way to be doing this as I know that the choiceController class will be released when it is closed thus getting rid of the TMTrackController class too. However even when I untick the 'release when closed' box of the ChoiceWindow.xib it breaks too.
What is the correct way to do this?
With xib s in the same project use:
#interface
#property (strong) NSWindowController *test;
#implementation
#synthesize test;
test = [[NSWindowController alloc] initWithWindowNibName:#"XIB NAME HERE"];
[test showWindow:self];
[home close];
It is not completely the same but this is my solution for such problems: Stackoverflow
Just ignore my statement in this answer regarding showing the window as a modal window. Everything else is still valid. This way you could have your personal window controller and it controls everything there is within the xib. This is a huge advantage for maintaining the project afterwards (and you keep to the application logic).