NSOutlineView (Source List) EXC_BAD_ACCESS error with ARC - cocoa

I've an ARC enabled project and within IB I've created a window that holds the source list component which I believe is just a configured NSOutlineView. I'm using the magical delegate method:
- (id)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
for which I cannot find any documentation for at all. Once this method is implemented the root node in my outline view will appear, upon which my entire model gets deallocated. Then when I try and expand the root node the app immediately crashes as model no longer exists.
If I don't use this method, my model remains, the source list works but none of the cells appear (understandably). I'm really not doing any thing fancy here at all.
I've never run into this sort of issue with ARC before, but it's late so there is a chance I've done something dumb and just can't see it. Here's the full code:
#implementation RLListController
- (void)awakeFromNib
{
RLPerson *stan = [[RLPerson alloc] initWithName:#"Stan"];
RLPerson *eric = [[RLPerson alloc] initWithName:#"Eric"];
RLPerson *ken = [[RLPerson alloc] initWithName:#"Ken"];
RLPerson *andrew = [[RLPerson alloc] initWithName:#"Andrew"];
RLPerson *daniel = [[RLPerson alloc] initWithName:#"Daniel"];
RLPerson *aksel = [[RLPerson alloc] initWithName:#"Aksel"];
[stan addChild:eric];
[stan addChild:ken];
[stan addChild:andrew];
[ken addChild:daniel];
[daniel addChild:aksel];
self.people = [#[stan] mutableCopy];
}
#pragma mark - Source List dataSource
- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
{
RLPerson *person = item;
return (item != nil) ? [person.children count] : [self.people count];
}
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
{
RLPerson *person = item;
return (item != nil) ? [person.children count] > 0 : YES;
}
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
{
RLPerson *person = item;
return (item != nil) ? [person.children objectAtIndex:index] : [self.people objectAtIndex:index];
}
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
{
RLPerson *person = item;
return person.name;
}
- (id)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
RLPerson *person = item;
NSTableCellView *cell = [outlineView makeViewWithIdentifier:#"DataCell" owner:self];
cell.objectValue = person;
[cell.textField setStringValue:person.name];
return cell;
}
#end
#implementation RLPerson
- (id)initWithName:(NSString *)name
{
self = [super init];
if(self)
{
_name = [name copy];
_children = [[NSMutableArray alloc] initWithCapacity:0];
}
return self;
}
- (void)addChild:(RLPerson *)child
{
[_children addObject:child];
}
- (void)dealloc
{
NSLog(#"dealloc");
}
#end

I've just figured out a similar crash in my code. I'll describe what the cause was for me... I'm pretty sure the same applies here, but I haven't tested your code.
Be aware that awakeFromNib can be called multiple times if you have multiple NIBs. I believe this is the case if you have NSTableCellView objects embedded within the NSOutlineView within your XIB file, which are extracted when you call makeViewWithIdentifier:owner: within outlineView:viewForTableColumn:item:.
Because you are creating your model objects (stan etc) within awakeFromNib, they are being recreated during these multiple calls. With each call, ARC is cleaning up the previous model objects, but NSOutlineView is still referencing them, hence the later crash when NSOutlineView tries to ask them for more information.
The fix is to move the model object creation out of awakeFromNib, perhaps into an init method instead.
Update:
Some other small points... it also took me a while to find the documentation for the magic outlineView:viewForTableColumn:item: method. For some reason, it is part of the NSOutlineViewDelegate protocol, not NSOutlineViewDataSource. I believe that if you implement this method, you don't need an implementation of outlineView:objectValueForTableColumn:byItem:.

Related

NSOutlineView Source List without a NIB

I am trying to figure out how to do a source list 100% in code. I've been hacking away at SideBar demo and various SO questions/answers and have gotten close, but not quite there. I've copied my code below, this seems to work in that it does build a view, with 3 empty cells I can select. What I can't quite figure out is how to get my text to show up in those cells. I will ultimately replace that with my own view, but I'd settle for the stock rendering to work for now..
In my controller:
#implementation SourceListController {
NSScrollView *_scrollView;
NSOutlineView *_sourceList;
SourceListDataSource *_sourceListDataSource;
}
- (void)loadView {
_scrollView = [[NSScrollView alloc] init];
_sourceList = [[NSOutlineView alloc] init];
_sourceListDataSource = [[SourceListDataSource alloc] init];
[_sourceList setSelectionHighlightStyle:NSTableViewSelectionHighlightStyleSourceList];
NSTableColumn *c = [[NSTableColumn alloc] initWithIdentifier: #"Column"];
[c setEditable: NO];
[c setMinWidth: 150.0];
[_sourceList addTableColumn: c];
[_sourceList setOutlineTableColumn:c];
[_sourceList setDelegate:_sourceListDataSource];
[_sourceList setDataSource:_sourceListDataSource];
[_scrollView setDocumentView:_sourceList];
[_scrollView setHasVerticalScroller:YES];
[_sourceList reloadData];
NSLog(_sourceList.dataSource == _sourceListDataSource ? #"YES" : #"NO");
self.view = _scrollView;
}
And in my data source/delegate:
#implementation SourceListDataSource {
}
- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
return item == nil ? 3 : 0;
}
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
return [NSObject new];
}
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
return item == nil;
}
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item {
NSLog(#"Here %f", tableColumn.width);
NSTableCellView *result = [[NSTableCellView alloc] init];
NSTextField *textField = [[NSTextField alloc] init];
[textField setStringValue:#"Test"];
[result addSubview:textField];
return result;
}
UPDATE: Further digging tells me viewForTableColumn doesn't even get called.. I am stumped as to why..
UPDATE 2: Figured out that it was the missing column and outlineTableColumn leading to not calling the delegate method. I fixed that and updated the code, but still no text shows up.
Your delegate method needs changes, it won't work that way, and also doesn't make use of the new caching methods for views. Mostly, use the NSTableCellView.textFieldproperty instead of adding your own NSTextFieldwithout frame and frame constraints. So, the code that should work looks like this:
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item {
NSTableCellView *result = [outline makeViewWithIdentifier:#"MyView" owner:self];
if (result == nil) {
// Create the new NSTextField with a frame of the {0,0} with the width of the table.
// Note that the height of the frame is not really relevant, because the row height will modify the height.
CGRect myRect = CGRectMake(0,0,outlineView.framesize.width,0);
result = [[NSTableCellView alloc] initWithFrame:myRect];
// The identifier of the view instance is set to MyView.
// This allows the cell to be reused.
result.identifier = #"MyView";
}
result.textField.stringValue = #"Test";
return result;
}
All the above typed in SO.

NSTableView error: preparedCellAtColumn:row: was Called

I'm having an issue where when you select a top level object in an NSOutlineView, an error message is generated saying:
"View Based NSTableView error: preparedCellAtColumn:row: was called. Please log a bug with the backtrace from this log, or stop using the method."
The NSOutlineView I am using is set to View Based. I have no idea why the preparedCellAtColumn method is even being called. I added the method and placed a breakpoint to try and trace what is calling it, but XCode looks to be blocking the execution of it when it fires this exception.
Edit - Delegate and DateSource Methods
- (BOOL) itemAtIndexIsHeader: (NSInteger) index
{
return [self isHeader: [_projectPane itemAtRow: index]];
}
- (BOOL) isHeader: (id) item
{
return [item isKindOfClass: [Folder class]];
}
- (BOOL) outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item
{
return NO;
}
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
{
//item is nil when the outline view wants to inquire for root level items
if (item == nil)
return [[[PMDataManager sharedManager] allFolders] objectAtIndex: index];
else{
Folder *folder = (Folder *) item;
return [[[folder projects] allObjects] objectAtIndex: index];
}
}
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
{
return [self isHeader: item];
}
- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
{
if (item == nil) { //item is nil when the outline view wants to inquire for root level items
return [[[PMDataManager sharedManager] allFolders] count];
}
else if ([self isHeader: item]) {
Folder *folder = (Folder *) item;
return [[[folder projects] allObjects] count];
}
return 0;
}
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item {
if ([self isHeader: item]){
PMProjectHeaderCell *cell = [outlineView makeViewWithIdentifier:#"HeaderCell" owner:self];
Folder *folder = (Folder *) item;
[[cell headerText] setStringValue: [folder name]];
return cell;
}
else{
PMProjectCell *cell = [outlineView makeViewWithIdentifier:#"ProjectCell" owner:self];
Project *project = (Project *) item;
[[cell projectNameTextField] setStringValue: [project name]];
return cell;
}
return nil;
}
- (void) outlineViewSelectionDidChange:(NSNotification *)notification
{
selectedProjectIndex = [_projectPane selectedRow];
[self reloadRightPane];
[self refresh: nil];
}
Verify that your table view content mode matches the datasource/delegate methods you are trying to use.
If you're trying to use cell-based datasource, verify that the table view content mode is "Cell Based". The same for view based.

draggingEntered not called

I have an NSBox subclass called dragBox. I want to be able to drag it around a canvas. The code is as follows:
-(void) awakeFromNib
{
[[self superview] registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]];
}
-(void) mouseDown:(NSEvent *)theEvent
{
[self dragImage:[[NSImage alloc] initWithContentsOfFile:#"/Users/bruce/Desktop/Untitled-1.png"] at:NSMakePoint(32, 32) offset:NSMakeSize(0,0) event:theEvent pasteboard:[NSPasteboard pasteboardWithName:NSDragPboard] source:self slideBack:YES];
}
-(NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender // validate
{
NSLog(#"Updated");
return [sender draggingSourceOperationMask];
}
-(NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender {
NSLog(#"Drag Entered");
return [sender draggingSourceOperationMask];
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
NSLog(#"Move Box");
[self setFrameOrigin:[sender draggingLocation]];
return YES;
}
-(BOOL) prepareForDragOperation:(id<NSDraggingInfo>)sender
{NSLog(#"Prepared");
return YES;
}
Why isn't dragEntered being called? I have tried to use all the pboard types and such. Nothing seems to work. I have also changed the registerForDraggedTypes to just work off of the [self] view. The box is a subview of a canvas.
Bruce
I found that awakeFromNib was the wrong place to put my registerForDragTypes call since I am programmatically adding my view (i.e. not adding it via a Nib). I had to put the call into initWithFrame:
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self registerForDraggedTypes: [NSArray arrayWithObjects:NSTIFFPboardType,NSFilenamesPboardType,nil]];
}
return self;
}
Bruce,
Your Code needs to be changed in the below way. I believe that view should be registered for drag types to make the method draggingEntered to get called.
#interface NSModifiedBox : NSBox
#end
#implementation NSModifiedBox
- (void)drawRect:(NSRect)dirtyRect
{
// Drawing code here.
[self registerForDraggedTypes:
[NSArray arrayWithObjects:NSTIFFPboardType,NSFilenamesPboardType,nil]];
[super drawRect:dirtyRect];
}
- (NSDragOperation)draggingEntered:(id )sender
{
if ((NSDragOperationGeneric & [sender draggingSourceOperationMask])
== NSDragOperationGeneric)
{
return NSDragOperationGeneric;
} // end if
// not a drag we can use
return NSDragOperationNone;
}
- (BOOL)prepareForDragOperation:(id )sender
{
return YES;
}
#end
Now Drag and Drop a NSBox on the Xib and the Modify the class of NSBox to NSModifiedBox.
Set a break point to the method "draggingEntered".
Now Drag a ".png" or ".gif" file and drop on the NSModifiedBox and you see the "draggingEntered" will get invoked
Or you can check by using NSLog as well inside a "draggingEntered".
Hope my answer will help you :)

Custom NSView Drag Destination

I'm trying to create a simple NSView that will allow a folder from Finder to be dragged onto it. A folder path is the only thing I want the view to accept as a draggable item. I've been trying to follow the Apple documentation, but so far nothing's working. So far, I've just tried to get the view to work with any file type, but I can't even seem to do that. Here's what I have so far:
-(id) initWithFrame:(NSRect)frameRect
{
if (self = [super initWithFrame:frameRect])
{
NSLog(#"getting called");
[self registerForDraggedTypes:[NSArray arrayWithObjects:NSPasteboardTypeString,
NSPasteboardTypePDF,
NSPasteboardTypeTIFF,
NSPasteboardTypePNG,
NSPasteboardTypeRTF,
NSPasteboardTypeRTFD,
NSPasteboardTypeHTML,
NSPasteboardTypeTabularText,
NSPasteboardTypeFont,
NSPasteboardTypeRuler,
NSPasteboardTypeColor,
NSPasteboardTypeSound,
NSPasteboardTypeMultipleTextSelection,
NSPasteboardTypeFindPanelSearchOptions, nil]];
}
return self;
}
-(BOOL) prepareForDragOperation: (id<NSDraggingInfo>) sender
{
NSLog(#"preparing for drag");
return YES;
}
The initWithFrame: method is getting called, but when I try to drag into the view the prepareForDragOperation: method doesn't ever seem to get called. My questions:
What am I doing wrong? Why isn't prepareForDragOperation: ever getting called?
What do I need to do to get the drag operation to only support dragging folders?
Update
I updated my registerForDraggedTypes: method with every type I could find. It now looks like this:
[self registerForDraggedTypes:[NSArray arrayWithObjects:NSPasteboardTypeString,
NSPasteboardTypePDF,
NSPasteboardTypeTIFF,
NSPasteboardTypePNG,
NSPasteboardTypeRTF,
NSPasteboardTypeRTFD,
NSPasteboardTypeHTML,
NSPasteboardTypeTabularText,
NSPasteboardTypeFont,
NSPasteboardTypeRuler,
NSPasteboardTypeColor,
NSPasteboardTypeSound,
NSPasteboardTypeMultipleTextSelection,
NSPasteboardTypeFindPanelSearchOptions,
NSStringPboardType,
NSFilenamesPboardType,
NSPostScriptPboardType,
NSTIFFPboardType,
NSRTFPboardType,
NSTabularTextPboardType,
NSFontPboardType,
NSRulerPboardType,
NSFileContentsPboardType,
NSColorPboardType,
NSRTFDPboardType,
NSHTMLPboardType,
NSURLPboardType,
NSPDFPboardType,
NSVCardPboardType,
NSFilesPromisePboardType,
NSMultipleTextSelectionPboardType, nil]];
I've noticed that the prepareForDragOperation: method isn't getting called when I drag a folder into the view. Did I miss a step?
Here's a simple little drag & drop view meeting those criteria:
MDDragDropView.h:
#interface MDDragDropView : NSView {
BOOL isHighlighted;
}
#property (assign, setter=setHighlighted:) BOOL isHighlighted;
#end
MDDragDropView.m:
#implementation MDDragDropView
#dynamic isHighlighted;
- (void)awakeFromNib {
NSLog(#"[%# %#]", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
[self registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, nil]];
}
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender {
NSLog(#"[%# %#]", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
NSPasteboard *pboard = [sender draggingPasteboard];
if ([[pboard types] containsObject:NSFilenamesPboardType]) {
NSArray *paths = [pboard propertyListForType:NSFilenamesPboardType];
for (NSString *path in paths) {
NSError *error = nil;
NSString *utiType = [[NSWorkspace sharedWorkspace]
typeOfFile:path error:&error];
if (![[NSWorkspace sharedWorkspace]
type:utiType conformsToType:(id)kUTTypeFolder]) {
[self setHighlighted:NO];
return NSDragOperationNone;
}
}
}
[self setHighlighted:YES];
return NSDragOperationEvery;
}
And the rest of the methods:
- (void)draggingExited:(id <NSDraggingInfo>)sender {
[self setHighlighted:NO];
}
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender {
return YES;
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
[self setHighlighted:NO];
return YES;
}
- (BOOL)isHighlighted {
return isHighlighted;
}
- (void)setHighlighted:(BOOL)value {
isHighlighted = value;
[self setNeedsDisplay:YES];
}
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
if (isHighlighted) {
[NSBezierPath setDefaultLineWidth:6.0];
[[NSColor keyboardFocusIndicatorColor] set];
[NSBezierPath strokeRect:self.frame];
}
}
#end
The reason prepareForDragOperation: isn't being called is that the dragging destination sequence follows a precise set of steps, and if the earlier steps aren't implemented, or are implemented but return a "stop the drag operation" type of answer, the later methods are never reached. (In your case, it doesn't appear that you've implemented the draggingEntered: method, which would need to return something other than NSDragOperationNone to continue on in the sequence).
Before prepareForDragOperation: is sent, the view is first sent a series of dragging destination messages:
A single - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender.
Depending on the NSDragOperation mask returned from that method, the following will be called if it's implemented in your class:
Multiple - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender.
Depending on the NSDragOperation mask returned from that method, then prepareForDragOperation: will be called.
I'm using NSURLPboardType to register for stuff being dropped from the Finder (when I drag a file or a folder to my application, it receives them as urls)
Try this. And if it works, it'll solve your second problem : just check if the URL is a folder to accept or reject the drop :
// if item is an NSURL * :
CFURLHasDirectoryPath((CFURLRef)item)
// returns true if item is the URL of a folder.

Why are my table view delegate methods not being called?

I'm building a document based Mac application. I have two classes myDocument and Person. The difficulty I'm having is when I push the button to add a new Person in the table view and display it it doesn't show in the table view. I've placed log statements inside the delegate methods. Since my log statements are not being displayed in the console I know they are not being called. Here are the implementations of the delegate methods
- (int)numberOfRowsInTableView:(NSTableView *)aTableView
{
return [employees count];
}
- (id)tableView:(NSTableView *)aTableView
objectValueForTableColumn:(NSTableColumn *)aTableColumn
row:(int)rowIndex
{
// What is the identifier for the column?
NSString *identifier = [aTableColumn identifier];
NSLog(#"the identifier's name is : %s",identifier);
// What person?
Person *person = [employees objectAtIndex:rowIndex];
// What is the value of the attribute named identifier?
return [person valueForKey:identifier];
}
- (void)tableView:(NSTableView *)aTableView
setObjectValue:(id)anObject
forTableColumn:(NSTableColumn *)aTableColumn
row:(int)rowIndex
{
NSString *identifier = [aTableColumn identifier];
Person *person = [employees objectAtIndex:rowIndex];
NSLog(#"inside the setObjectMethod: %#",person);
// Set the value for the attribute named identifier
[person setValue:anObject forKey:identifier];
[tableView reloadData];
}
Here is a pic of my .xib
Here are my actions methods
#pragma mark Action Methods
-(IBAction)createEmployee:(id)sender
{
Person *newEmployee = [[Person alloc] init];
[employees addObject:newEmployee];
[newEmployee release];
[tableView reloadData];
NSLog(#"the new employees name is : %#",[newEmployee personName]);
}
-(IBAction)deleteSelectedEmployees:(id)sender
{
NSIndexSet *rows = [tableView selectedRowIndexes];
if([rows count] == 0){
NSBeep();
return;
}
[employees removeObjectAtIndexs:rows];
[tableView reloadData];
}
You forgot to bind the document's tableView outlet to the actual table view. Thus your reloadData messages are sent to nil.

Resources