Creating a Controller for NSOutlineView - objective-c

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.

Related

NSViewController New vs. InitWithNibName issues

I am having a weird error with NSViewController where if I allocate a view using the viewcontroller's regular init message, the view created is not my view, but when using the default NIB name, it does work.
Specifically, this code works all the time. It creates the view defined in the nib file, and displays it in the parentView.
+ (void)createTransparentViewCenteredInView:(NSView*)parentView withText:(NSString*)text duration:(NSTimeInterval)duration {
TransparentAccessoryViewController* controller = [[TransparentAccessoryViewController alloc] initWithNibName:#"TransparentAccessoryViewController" bundle:nil];
NSLog(#"%#", [controller.view class]); // Returns "TransparentAccessoryView" -- CORRECT
[parentView addSubview:controller.view];
}
However, the following code works SOME of the time (which is weird in that it doesn't always fail). With some parentViews, it works perfectly fine, and with others, it doesn't. The parent views are just random custom NSViews.
+ (void)createTransparentViewCenteredInView:(NSView*)parentView withText:(NSString*)text duration:(NSTimeInterval)duration {
TransparentAccessoryViewController* controller = [TransparentAccessoryViewController new];
NSLog(#"%#", [controller.view class]); // Returns "NSSplitView" -- INCORRECT
[parentView addSubview:controller.view];
}
The errors that comes up are as follows (I have no idea why it is bringing up an NSTableView, as I don't have an NSTableView here at all. Also, it is weird that it complains about an NSTableView when the type it prints is an NSSplitView):
2013-04-07 21:33:12.384 Could not connect the action refresh: to
target of class TransparentAccessoryViewController
2013-04-07 21:33:12.384 Could not connect the action remove: to target
of class TransparentAccessoryViewController
2013-04-07 21:33:12.385 * Illegal NSTableView data source
(). Must implement
numberOfRowsInTableView: and tableView:objectValueForTableColumn:row:
The NIB file defines a custom subclassed NSView, called TransparentAccessoryView, and hooks this up to the File Owner's view property, standard stuff (all I did was change the custom class name to TransparentAccessoryView). I added an NSLog's to see what was going on, and for some reason, in the second case, the view class type is incorrect and thinks it is an NSSplitView for some reason. The ViewController class is as follows:
#implementation TransparentAccessoryViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Initialization code here.
}
return self;
}
- (void)awakeFromNib {
self.textField.stringValue = #"";
}
+ (void)createTransparentViewCenteredInView:(NSView*)parentView withText:(NSString*)text duration:(NSTimeInterval)duration {
TransparentAccessoryViewController* controller = [[TransparentAccessoryViewController alloc] initWithNibName:#"TransparentAccessoryViewController" bundle:nil];
NSLog(#"%#", [controller.view class]);
[parentView addSubview:controller.view];
}
#end
I thought that the default init message triggers the viewcontroller to load the NIB named after the viewcontroller, which seems to be the case some of the time as the second version of my code works in certain conditions.
Does anyone know why this behavior is occurring at all?
From the docs:
If you pass in a nil for nibNameOrNil then nibName will return nil and
loadView will throw an exception; in this case you must invoke
setView: before view is invoked, or override loadView.
Therefore, if you're initializing a NSViewController with -init, you should call -setView: to set the view controller's view, or override -loadView. In the latter case, you could certainly implement the UIViewController-like behavior that you're probably expecting -- if nibNameOrNil is nil, try to load a nib that has the same name as the class.
I think that when you call init on a NSViewController, you're assuming that the implementation of init for NSViewController searches for a nib with the same name as the view controller and uses it. However, this is undocumented API or at least I can't seem to find any documentation supporting that assumption. The link you posted on your comments doesn't cite any documentation either and even reiterates that this is undocumented and that Apple could change this implementation at any point.
I think to assure that your code works in future versions of the SDK (and since it is already creating undesired behavior), you should not rely on this assumption. To achieve the same outcome simply override init and initWithNibName:bundle: in such a way as explained by this post:
#implementation MyCustomViewController
// This is now the designated initializer
- (id)init
{
NSString *nibName = #"MyCustomViewController";
NSBundle *bundle = nil;
self = [super initWithNibName:nibName bundle:bundle];
if (self) {
...
}
return self;
}
- (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)bundle
{
// Disregard parameters - nib name is an implementation detail
return [self init];
}

Understanding sample code for adding a row to a table view

I was reading the docs to learn how to add a row in a table view, and I found this example :
- (void)save:sender {
UITextField *textField = [(EditableTableViewTextField *)[tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]] textField];
SimpleEditableListAppDelegate *controller = (SimpleEditableListAppDelegate *)[[UIApplication sharedApplication] delegate];
NSString *newItem = textField.text;
if (newItem != nil) {
[controller insertObject:newItem inListAtIndex:[controller countOfList]];
}
[self dismissModalViewControllerAnimated:YES];
}
I don't understand the method : insertObject:inListAtIndex: or what [[UIApplication sharedApplication] delegate]; stands for; are we putting the data in a plist file? Could someone explain this to me? The UIApplication docs do not really help.
[[UIApplication sharedApplication] delegate] is the main application delegate, typically this is a class named AppDelegate. The main application delegate is the one that is created on application start-up and which is the main controller for your application.
I'm going to assume that you're using something similar to this class as your AppDelegate class.
[controller insertObject:newItem inListAtIndex:[controller countOfList]];
This assumes that your AppDelegate class has a method named insertObject:inListAtIndex: on it. For the class I linked the method looks like this:
- (void)insertObject:(id)obj inListAtIndex:(NSUInteger)theIndex {
[list insertObject:obj atIndex:theIndex];
}
So in this case, that method is adding the object to a member variable of your AppDelegate class called List.
There is no connection to plist. Just message exchange with help of delegates to change table view.
From apple documentation:
When a table view enters editing mode and when users click an editing control, the table view sends a series of messages to its data source and delegate, but only if they implement these methods. These methods allow the data source and delegate to refine the appearance and behavior of rows in the table view; the messages also enable them to carry out the deletion or insertion operation.
Read this:
http://developer.apple.com/library/ios/#documentation/userexperience/conceptual/TableView_iPhone/ManageInsertDeleteRow/ManageInsertDeleteRow.html
And one of the best books - Beginning iphone 4 (or 5)

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.

Better way to manage lifetime of instance created in class method

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.

Image not being set in method

I have a class with a viewDidLoad method and an artworkInfo method as follows:
-(void)viewDidLoad {
mainDelegate = (AppDelegate*)[[UIApplication sharedApplication]delegate];
[super viewDidLoad];
}
-(void)artworkInfo:(NSNumber *)pos{
mainDelegate = (AppDelegate*)[[UIApplication sharedApplication]delegate];
[self.image setImage:(UIImage *)[[mainDelegate.mapAnnotations objectAtIndex:0]image]];
}
the mainDelegate thing is to gain access to the appDelegate where an array is stored, but anyway, with the "[self.image setImage...]" command where it is, the image on the app does not appear, but when I copy that exact line of code into the viewDidLoad method, it shows up like it should. I know that the artworkInfo method is being called because I debugged it and it goes through, so I can't figure out why the command would not be doing anything it's current method while it will in the viewDidLoad...?
Also, here is where the method is called and this new view is loaded from another class:
infoPage *info = [[infoPage alloc] initWithNibName:#"infoPage" bundle:nil];
info.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentModalViewController:info animated:YES];
infoPage *myInfoPage = [[infoPage alloc] init];
[myInfoPage artworkInfo:position];
[info release];
OH, I see the problem. You're instantiating 2 different infoPage classes.
Change this:
infoPage *info = [[infoPage alloc] initWithNibName:#"infoPage" bundle:nil];
info.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentModalViewController:info animated:YES];
infoPage *myInfoPage = [[infoPage alloc] init];
[myInfoPage artworkInfo:position];
[info release];
to this:
infoPage *info = [[infoPage alloc] initWithNibName:#"infoPage" bundle:nil];
info.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentModalViewController:info animated:YES];
[info artworkInfo:position];
[info release];
Ok detailed answer. In order to understand why this image is not displaying properly you have to first look at how Runloops work in Objective C.
While viewDidLoad is the method that is called when a view is loaded and it is technically also called before a view is displayed and it's view objects initialized. Since presentModalViewController is an animation there is actually some threading going on in the works.
viewDidLoad gets called before the animation is created for the presentModalView. This initializes your objects. However, due to some of the inner workings of UI Kit some processes are loaded off into a thread. When they complete they run callback methods on the main UI thread.
Since presentModalViewController is a non-blocking method your artworkInfo method gets added to the mainRunLoop before the initializer form thread adds its callback methods to the main run loop. The best approach would be to have both a UIImage property of your viewController and a UIImageView.
set the value of UIImage by calling artworkInfo BEFORE the presentModalViewController method.
in your ViewDidLoad go ahead and set the value of your UIImageView
[self.imageView setImage:self.image];
Problem solved.
This seems pretty straight forward.
So you initialize your nib and try to call your method artwork before the nib is fully loaded. <-- This is not working for you.
Then you do additional initialization by overrider viewDidLoad per the doco where the nib is loaded <-- This is working for you
So the answer is, when you call setImage before your nib is loaded, then there is nothing to set the image to. When you call setImage in viewDidLoad your nib is loaded and then things should work just fine.
I hope this explains it a bit.