Challenge Delete Row


#1

Added this to BNRDocument.h

Went to BNRDocument.xib and added a button and connected it to my IBAction via the ‘File Owner’

Then implemented it in BNRDocument.M

[code]-(IBAction)deleteItem:(id)sender
{
//First Check if there was something to delete

if ([itemTableView selectedRow] ==-1) {
    NSAlert *alert = [[NSAlert alloc] init];
    [alert setMessageText:@"There Isn't Anything Selected To Delete."];
    [alert runModal];
}

//Remove the Row
[todoItems removeObjectAtIndex:[itemTableView selectedRow]];

//Reload the data
[itemTableView reloadData];

//Tell the file it needs to be saved
[self updateChangeCount:NSChangeDone];

}
[/code]

I have no idea what Aaron is asking for with the errors. I find the Apple documentation … well let’s put it this way, it is purposefully written in a way that it is not helpful. My opinion anyway. Hopefully my opinion changes over the course of learning Objective-C.

I did see in the NSAlert section this

(NSAlert *)alertWithError:(NSError *)error

Which if I read right, does this:
“extracts the localized error description, recovery suggestion, and recovery options from error and uses them as the alert’s message text, informative text, and button titles, respectively.”


#2

I pretty much came to the same solution.

Added this to the header:

Then a quick method in the implementation:

[code]-(IBAction)deleteSelectedItem:(id)sender
{
// We need to get the current row and remove it from the todoItems array

[todoItems removeObjectAtIndex:[itemTableView selectedRow]];

// Refresh the tableview
[itemTableView reloadData];

// update the change count
[self updateChangeCount:NSChangeDone];

}[/code]

And hooked it up to a new “Delete Item” button.

I didn’t error check for a selected row as you can’t select one with nothing on it.

I’m going to mess around a bit more and see what other fun stuff I can do with it.


#3

macshome, you can hit the delete button with no row selected, in which case the selectedRow message returns -1, causing an exception.

BrianH, I created my own NSError object for testing purposes. The dialog looked odd, with “Reason” and “Suggestion” (if I recall correctly) as literal strings (pretty sure the word “Description” was not displayed).

- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError
{
    todoItems = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListMutableContainersAndLeaves format:NULL error];
    
    NSMutableDictionary *errDict = [[NSMutableDictionary alloc] init];
    [errDict setValue:@"Reason" forKey:NSLocalizedFailureReasonErrorKey];
    [errDict setValue:@"Description" forKey:NSLocalizedDescriptionKey];
    [errDict setValue:@"Suggestion" forKey:NSLocalizedRecoverySuggestionErrorKey];
    // Bogus error
    NSError *fakeError = [[NSError alloc] initWithDomain:@"us.epep.test"
                                                    code:2 userInfo:errDict];
    *outError = fakeError;
    return NO;
}

#4

Ahh I see that now. I didn’t notice it before, but luckily it was easy to fix.


#5

Hi There,

I am finding this challenge a little difficult to understand. I have created the delete button and I can delete items for the list and I make sure that I only attempt to delete the item if one is selected:

-(IBAction)deleteSelectedItem:(id)sender
{
    if ([todoItems count] > 0) {
        //get selected rows
        NSInteger deleterow = [itemTableView selectedRow];
        //remove the row if one is selected
        if (deleterow != -1) {
            [todoItems removeObjectAtIndex:deleterow];
            //refresh the data
            [itemTableView reloadData];
            //-updateChange count: tells the application whether or not the document
            //has unsaved changes. NSChangeDoen flags the document as unsaved.
            [self updateChangeCount:NSChangeDone];
        }
        
    }
}       

However, I have no idea what to do with the NSError challenge. Am I supposed to provide a dialog when deletion fails or is something odd supposed to happen when saving or loading a file?

Thanks for any suggestions,

Adam.


#6

I did two things in this challenge first I set itemTableView as a delegate and implemented this code:

- (void)tableViewSelectionDidChange:(NSNotification *)notification { // In order to use this method you have to make sure that // NSTableview is also a delegate if ([itemTableView selectedRow] > -1) { [deleteButton setEnabled:YES]; } else { [deleteButton setEnabled:NO]; } }

That way the delete button is only enabled when a row is selected. And so I don’t have to error check so much when the selected row is to be deleted:

- (IBAction)deleteSelectedItem:(id)sender; { NSInteger row = [itemTableView selectedRow]; [itemTableView deselectRow:row]; [todoItems removeObjectAtIndex:row]; [deleteButton setEnabled:NO]; [itemTableView reloadData]; [self updateChangeCount:NSChangeDone]; }

And I can’t figure out implementing the NSError on this one…are we supposed to return state that there was an error and what kind? Or return it back? In the readFromData method it returns a BOOL and it returns the answer to todoItems != nil. YES if it has a file and NO if it doesn’t. But the other method call that writes the data, dataOfType it just returns either an empty array or our filled array. Kind of at a loss.


#7

I tried to solve the NSError challenge as described in the earlier chapter 23 on page 152. Additionally I found some help in the “Error Handling Programming Guide” in the chapter Using and Creating Error Objects. When I now modify a saved task file with a text editor by deleting a keyword and try to open this file with TahDoodle, I receive a more detailed error description through my popup window telling me, that the file is corrupt. But I can’t suppress the following standard popup error message from the NSDictionary Object. Has somebody an idea or better solution ?

- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError
{    
    NSError *error = nil; //Create the error object but don't alloc or initialize it
    todoItems = [NSPropertyListSerialization propertyListWithData:data
                                                          options:0 
                                                           format:NULL 
                                                            error:&error];
 
    if (todoItems == nil) {          // there is an error
        NSAlert *theAlert = [NSAlert alertWithError:error];  //create the pop up alert and give it the error description
        [theAlert runModal]; //show the pop up with the error on the screen
        return NO;  //returning NO its telling the NSDirectory object that "open" failed - this leads to another pop up message
                         // of which I don't now how to suppress it.
    } else {
        return YES;
    }
}

#8

I’m with everyone else on this thread, the delete was relatively minor to implement, the NSError on the other hand. I’m not sure,
somehow I don’t think the if (outerror) logic is quite correct, somehow we should be implementing something where error: is getting a error:&ourOutError and this object is something we write etc, I’m just puzzled as to the actual implementation.

Yuk, hated pointers years ago, and somehow, pointers seem involved here.


#9

[quote=“macshaggy”]I did two things in this challenge first I set itemTableView as a delegate and implemented this code:

- (void)tableViewSelectionDidChange:(NSNotification *)notification { // In order to use this method you have to make sure that // NSTableview is also a delegate if ([itemTableView selectedRow] > -1) { [deleteButton setEnabled:YES]; } else { [deleteButton setEnabled:NO]; } }

That way the delete button is only enabled when a row is selected. And so I don’t have to error check so much when the selected row is to be deleted:

- (IBAction)deleteSelectedItem:(id)sender; { NSInteger row = [itemTableView selectedRow]; [itemTableView deselectRow:row]; [todoItems removeObjectAtIndex:row]; [deleteButton setEnabled:NO]; [itemTableView reloadData]; [self updateChangeCount:NSChangeDone]; }
[/quote]

I like that. Can you please share how you went about making itemTableView a delegate?

I’m trying to do the same thing but using Target-action instead of delegate, and can’t figure out the details. It seems as though I’d need to make the “Delete Item” button an instance variable of the BNRDocument like so:
IBOutlet NSButton *deleteItemButton;

Then create an IBAction method:

  • (IBAction)enableDeleteButton:(id)sender;

It’s when I need to implement it in the method that I start getting lost, as I can’t figure out how to identify the button to tell it to enable/disable as appropriate.

Am I missing something?

Thanks in advance!
mb


#10

I’ve begun implementing the NSError portion of this challenge, unlike an earlier post, I don’t get the second popup. I think this may be on the right track but I’m not sure.

[code]- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError *)outError
{
/

Insert code here to read your document from the given data of the specified type. If outError != NULL, ensure that you create and set an appropriate error when returning NO.
You can also choose to override -readFromFileWrapper:ofType:error: or -readFromURL:ofType:error: instead.
If you override either of these, you should also override -isEntireFileLoaded to return NO if the contents are lazily loaded.

NSException *exception = [NSException exceptionWithName:@"UnimplementedMethod" reason:[NSString stringWithFormat:@"%@ is unimplemented", NSStringFromSelector(_cmd)] userInfo:nil];
@throw exception;
return YES;
*/
todoItems = [NSPropertyListSerialization propertyListWithData:data
                                                      options:0
                                                       format:NULL
                                                        error];
//if (YES) {
if (*outError) {
    NSDictionary *errorDictionary  = [NSDictionary dictionaryWithObject:@"Your XML file is bad!"
                                                                 forKey:NSLocalizedFailureReasonErrorKey];
    *outError = [NSError errorWithDomain:NSOSStatusErrorDomain
                                    code:unimpErr
                                userInfo:errorDictionary];
    
    return NO;
    
    // NSLog(@"Contents: %@", errorDictionary);
}

return (todoItems != nil);

}
[/code]

I don’t believe my Return is correct. I’m triggering the error by having a corrupted XML file. Anyway, I’m going to continue with this to see if I can do a better job.


#11

There is three things to do:

  1. In the Interface Builder “SHIFT-CTRL” click on the window and select the Table View.
  2. Then CTRL click and drag the arrow from the selected Table View to the “File’s Owner” which in this case should become the delegate.
  3. Select “delgate” from the menu.

Don’t forget that you also need a pointer to your Delete button as we want to change its state and therefor need a reference to it (remember: we did not need a pointer to the Insert button because we never sent messages in its direction. But when we want to fiddle around with a button, we need to know where the object is located in memory, so we need a pointer in this case).

Finally, you need to connect the delete button just like we connected the Insert button (but this time the action points to deleteItem, of course.

[code]#import <Cocoa/Cocoa.h>

@interface BNRDocument : NSDocument
{
NSMutableArray *todoItems;

IBOutlet NSTableView *itemTableView;
IBOutlet NSButton *deleteButton; // A pointer to the delete button

}

  • (IBAction)createNewItem:(id)sender;
  • (IBAction)deleteItem:(id)sender; // <-- and here
    @end[/code]

Happy coding,

iFlash


#12

On the one hand, I, like many of you, are still wrestling with how to implement NSError for this challenge and haven’t yet figured out how that should work or what approach to take.

OTOH, here is my code for the delete button which works fine EXCEPT for the NSAlert modal. For some reason, a second alert box pops up no matter which button is clicked and the actions are carried out correctly after clicking the buttons in the second popup that appears. A nudge from anyone toward where else to look in the docs to explain why this is occurring or comments on what it is I’m missing would be greatly appreciated! :slight_smile:

- (IBAction)deleteSelectedItem:(id)sender
{
    //this action appears to assume that an array already exists
    //so no need to make one, so go get the selected cell
    //and delete it then refresh the list and signal unsaved changes
    
    //is there anything selected? If not, then the button does nothing
    
    if ([itemTableView selectedRow] == -1) {
        
        return;
        
    } else {
        //delete the item
        //first ask if this is OK
        
        NSAlert *alert = [[NSAlert alloc] init];
        [alert addButtonWithTitle:@"OK"];
        [alert addButtonWithTitle:@"Cancel"];
        [alert setMessageText:@"Delete this item?"];
        [alert setInformativeText:@"Deleted items cannot be restored."];
        [alert setAlertStyle:NSWarningAlertStyle];
        
        [alert runModal];
        
        if ([alert runModal] == NSAlertFirstButtonReturn) {
            
            //OK was clicked, delete the item
            [todoItems removeObjectAtIndex: [itemTableView selectedRow]];
            [itemTableView reloadData];
            [self updateChangeCount:NSChangeDone];
        
        } else {
            //user canceled
            return;
        }
    }
}

#13

[quote][code] [alert runModal];

    if ([alert runModal] == NSAlertFirstButtonReturn) {

[/code][/quote]

You send the alert object runModal twice, only capturing the result on the 2nd invocation. Either drop the first call, or store the return value in a variable and check the value of the variable in the if statement.


#14

Thank you, macintux - you’re a gem. I see now that I misunderstood the examples in the “Using the NSAlert Class” system guide.
AND thank you for the “Aha” moment. It’s nice for a noob such as myself to get those occasionally :).

If I wanted to capture the return value from [alert runModal], this code snippet appears to work well:

        NSAlert *alert = [[NSAlert alloc] init];
        [alert addButtonWithTitle:@"Cancel"];   //rightmost button and default option
        [alert addButtonWithTitle:@"OK"];       //button to the left of rightmost; buttons are counted from the rightmost edge
        [alert setMessageText:@"Delete this item?"];
        [alert setInformativeText:@"Deleted items cannot be restored."];
        [alert setAlertStyle:NSCriticalAlertStyle];
        
        long buttonSelect = [alert runModal];
        
        if (buttonSelect == NSAlertFirstButtonReturn) {
            
            return; //cancel
            
        } else { //delete the item and update stuff; there are only 2 buttons in this case; would need to use <else if> if more than 2 buttons
            
            [todoItems removeObjectAtIndex: [itemTableView selectedRow]];
            [itemTableView reloadData];
            [self updateChangeCount:NSChangeDone];
        }

#15

I have an annoying issue with my solution to this challenge.

My aim is to move the row selection down 1 unit after deleting the selected row. This improves usability by allowing the user to delete multiple rows in succession without having to keep clicking on a new row to select one. An additional case is needed for if the last row is deleted, so that then selection moves 1 row up instead.

My code so far is:

[code]-(IBAction)deleteItem:(id)sender
{
if ([itemTableView selectedRow] != -1) {

    NSInteger row = [itemTableView selectedRow];
    NSIndexSet *newIdx = [NSIndexSet alloc];

    if (row == [todoItems count] - 1) {
       [newIdx initWithIndex:row - 1]; // Warning exression result unused
    } else {
        [newIdx initWithIndex:row]; // Warning expression result unused
    }
    [itemTableView deselectRow:row]; // needed to prevent exception if the selected row contents have been edited but not yet committed
    [todoItems removeObjectAtIndex:row];
    [itemTableView selectRowIndexes:newIdx byExtendingSelection:NO];
       
// reloaddata tells the table view to refresh and ask its dayasource
//(which happens to be this BNRDocument object in this case)
// For new data to display
[itemTableView reloadData];

// updateChangeCount: tells the application whether or not the document
// has unsaved changes. NSChangeDone flags the document as unsaved.
[self updateChangeCount:NSChangeDone];

}

}[/code]

This code works fine (as far as I can tell) but Xcode gives me 2 warnings that the expressions inside the if/else block are unused.

Why is this and any ideas how to fix it other than rearranging the code so that the line [itemTableView selectRowIndexes:newIdx byExtendingSelection:NO] and the 2 before it are duplicated inside the block?

Also should I make sure to set row = nil; newIdx = nil; when I’m done with them at the end of the method, or does ARC take care of all that?

Thanks

Edit:
After a bit of research I believe the solution is to cast to void.

if (row == [todoItems count] - 1) { (void) [newIdx initWithIndex:row - 1]; // Warning exression result unused } else { (void) [newIdx initWithIndex:row]; // Warning expression result unused } }


#16

Ugh! I’m having trouble with the delete button - haven’t event looked at the NSError thing yet. I’m getting an error with my code, but it looks like I’m using the same code to delete as everyone else:

- (IBAction)deleteItem:(id)sender
{
    // Remove object from todoItems
    [todoItems removeObject:[itemTableView selectedRow]];
    
    // refresh the view
    [itemTableView reloadData];
    
    // indicate there are unsaved changes
    [self updateChangeCount:NSChangeDone];
}

I haven’t even bothered to check if the list is empty or anything. The error I’m getting on the removeObject line is
"Implicit conversion of ‘NSInteger’ (aka ‘long’) to ‘id’ is disallowed with ARC"
and
"Incompatible integer to pointer conversion sending ‘NSInteger’ (aka ‘long’) to parameter of type ‘id’"

So, I’m assuming somehow it’s seeing [itemTableView selectedRow] as a pointer as opposed to an integer. How do I change that???


#17

[quote]
…"Implicit conversion of ‘NSInteger’ (aka ‘long’) to ‘id’ is disallowed with ARC"
and “Incompatible integer to pointer conversion sending ‘NSInteger’ (aka ‘long’) to parameter of type ‘id’”
… seeing [itemTableView selectedRow] as a pointer as opposed to an integer. How do I change that???[/quote]
[itemTableView selectedRow] gives you an integer, the index of the selected row. Therefore, you should first think about how to get the object corresponding to that index or how to delete an object with a given index.


#18

I changed my tableView to allow multiple selections, and implemented this

[code]- (void) deleteSelected:(id)sender
{
if (self.tasks)
{
NSIndexSet * indexes = [self.taskTable selectedRowIndexes];

    if ([indexes count] > 0)
    {
        // Work from the back as removal of items will change any following indexes
        NSUInteger chosenIndex = [indexes lastIndex];
    
        do
        {
            [self.tasks removeObjectAtIndex:chosenIndex];
            chosenIndex = [indexes indexLessThanIndex:chosenIndex];
        } while (chosenIndex != NSNotFound);
        
        [self.taskTable reloadData];
        [self updateChangeCount:NSChangeDone];
   }
}

[/code]

Could delete multiple selections easily enough.