Working with Tables: Writing an Address Book Application
Pages: 1, 2, 3, 4
Problems
Unfortunately version 2 has a subtle, but serious flaw that results from the simple fact that it is not safe to modify a mutable array while it is being enumerated. Consider the situation of a table with five records in it, and you want to delete the first and last records (you can do discontinuous row selection by command-clicking). So we do [tableView selectedRowEnumerator] and in return we get an enumerator of NSNumber objects for 0 and 4.
The first time we send nextObject to the enumerator, we get returned the NSNumber "0", and we end up removing the first member of the mutable array records. Before this, our array had five records with indexes 0 through 4. Now our array has four records with indexes 0 through 3 (the remaining records all get shifted down one slot to fill the space previously taken by the record we just deleted).
The problem is that the next time through the loop, nextObject will return the other NSNumber in the enumerator, 4. We then try to remove the object at index 4 in records, but the previous removal of the object at index 0 ensured that there will be no object at index 4. The result of this is that we get an index out-of-range error, and the last record remains untouched. The moral of the story is to not change the contents of an array while enumerating it, and we have to adopt a more indirect approach to removing multiple records.
So we have to come up with a way to remove from records the selected rows after the enumeration has taken place. One thing we could do during the enumeration is to build an array of records we wish to remove, but not remove them until the enumeration has finished. We can then use the NSMutableArray method, removeObjectsInArray:, to remove from the receiver any objects contained in the array we created during the enumeration. Here we have version 3:
-(IBAction)deleteRecord:(id)sender
{
NSEnumerator *enumerator = [tableView selectedRowEnumerator];
NSNumber *index;
NSMutableArray *tempArray = [NSMutableArray array];
id tempObject;
while ( (index = [enumerator nextObject]) ) {
tempObject = [records objectAtIndex:[index intValue]]; // No modification, no problem
[tempArray addObject:tempObject]; // keep track of the record to delete in tempArray
}
[records removeObjectsInArray:tempArray]; // we're golden
[tableView reloadData];
}
So we changed this by creating two variables: tempObject and tempArray. During the enumeration we did just what we said we would: retrieve the record from records corresponding to the selected row in each loop of the enumeration and store it in tempArray. After the enumeration finished, we have in tempArray all of the records from our data structure records that were selected for deletion in the table. With removeObjectsInArray, we remove from records all of the objects contained in tempArray (we're basically deleting the intersection of these two collections -- the common objects). Well! Fixed that problem!
Alert panels
Because deleting is not "undoable" in our implementation, it would probably be good idea to provide some sort of system feedback requesting user confirmation to a delete command so records aren't inadvertently deleted -- like with an alert dialog. The AppKit function, NSRunAlertPanel, allows us to do just that. This function takes the five arguments listed below.
Arg 1: a string representing the title of the alert panel.Arg 2: message text to display in the alert panelArg 3: text to display in default buttonArg 4: text label for alternate buttonArg 5: text label for second alternate button
If you want to format your message text a la printf(), you can do so by adding the variables to display in the message as optional arguments at the end of the argument list.
The return value of this function is an integer that indicates which button was pressed. AppKit defines several constants related to alert panels: NSAlertDefaultReturn, NSAlertAlternateReturn, NSAlertOtherReturn, and NSAlertErrorReturn, whose values are 0, 1, 2, and 3 respectively. The values of these constants are the same as the integer values returned by NSRunAlertPanel, and can thus be used to determine which button was pushed with an equality test.
Let's change our code to incorporate an alert panel. While we're at it, let's have the system beep at us when the panel opens using the AppKit function NSBeep(). Here is what our final, version 4 of the deleteRecord code looks like:
-(IBAction)deleteRecord:(id)sender
{
int status;
NSEnumerator *enumerator;
NSNumber *index;
NSMutableArray *tempArray = [NSMutableDictionary array];
id tempObject;
if ( [tableView numberOfSelectedRows] == 0 )
return;
NSBeep();
status = NSRunAlertPanel(@"Warning!", @"Are you sure you want to delete the selected record(s)?", @"OK", @"Cancel", nil);
if ( status == NSAlertDefaultReturn ) {
enumerator = [tableView selectedRowEnumerator];
while ( (index = [enumerator nextObject]) ) {
tempObject = [records objectAtIndex:[index intValue]];
[tempArray addObject:tempObject];
}
[records removeObjectsInArray:tempArray];
[tableView reloadData];
}
}
The first thing we did after declaring are variables was to use NSTableView's numberOfSelectedRows message to check how many rows are selected. If there are zero rows selected, then there's really no point in trying to delete nothing, and we return from the method.
Next we issued a beep with NSBeep() and opened up an alert panel -- the return value we store in the variable status. If you don't want one of the buttons to be displayed in the alert panel, use nil as the argument value for that button's text. We did this above to eliminate the third button from the alert panel. You can see what this alert panel looks like in the image below.
|
Then we compared status to the constant NSAlertDefaultReturn and executed our previously written record deletion code.
We've now set up a nice, simple system of working with the data, but we haven't yet talked about how the table actually gets the data from our array to the interface. The next section will focus on this.
Setting up the data source
Now is the time that we set up the data source. Recall from above that in Interface Builder we made a connection between Controller and tableView indicating that Controller would act as tableView's data source. Whenever a table view receives a reloadData message, that's its signal to go to send messages to the data source (Controller) to retrieve the data to display. What are the messages the table view object sends to the dataSource object to retrieve data? Before I can answer that question I need to say a word about Objective-C protocols.
We can find those messages in the NSTableDataSource protocol, which is a part of AppKit. In Objective-C we already know of class interfaces, which publicly publish all of the messages that can be sent to that class. But messaging is a two-way street.
Often it is useful to know what messages a class sends so we can prepare potential receivers of those messages to respond to them appropriately and intelligently. Protocols do just that. A protocol makes public knowledge any methods that an instance of a class sends. A class is said to conform to a protocol if it implements the methods laid out in the protocol.
In our situation, tableView is sending out messages to its data source expecting data back. The protocol NSTableDataSource tells us the what the messages are, including their arguments and what return value the tableView is expecting. For a dataSource object to function properly, it must implement the minimum required methods for it to be a dataSource and conform to the protocol.
If we look at the NSTableDataSource protocol reference, we'll find that there are two methods that we must implement for a table to display a data source's data. These two methods are numberOfRowsInTableView:, and tableView:objectValueForTableColumn:row:.
The first of these two methods is simple. Basically, this method is the table view's way of asking the data source "How many records do you have?" and the data source responds kindly with an integer. Add this method to your Controller implementation file with the following code:
- (int)numberOfRowsInTableView:(NSTableView *)aTableView
{
return [records count];
}
The argument aTableView refers to the table view that is asking for the information. This argument allows for one dataSource object to manage data for multiple table views. In our application, we only have one, so we don't have to make use of that argument. If you had more than one table in your application, however, you might use this argument in a conditional statement: If aTableView is Table 1, then return the number of records in Array 1, or if aTableView is Table 2, then return the number of records in Array 2.
Following NSTableDataSource's recipe, we see that the next method to implement is tableView:objectValueForTableColumn:row:. The code for our implementation looks like this:
-(id)tableView:(NSTableView *)aTableView
objectValueForTableColumn:(NSTableColumn *)aTableColumn
row:(int)rowIndex
{
id theRecord, theValue;
theRecord = [records objectAtIndex:rowIndex];
theValue = [theRecord objectForKey:[aTableColumn identifier]];
return theValue;
}
Again, we're not going to use the aTableView variable from the arguments. All that interests us here is rowIndex and aTableColumn. Whenever we send tableView a reloadData message, the table view in turn scans through the columns and rows, sending this message to get the contents for each cell in the table. By attaching to the message row and column information, we're able to zero in on the correct data value within our two-dimensional data structure.
What we did first in our implementation was declare two generic object variables. By declaring these variables as type id, we eliminate the risk of trying to assign a variable to an object that it is not typed to. In the second line we assigned to theRecord the dictionary object we have stored at the rowIndex index of records.
Then, we use this dictionary to obtain the appropriate object to be displayed in aTableColumn. How do we know which is the right object? Well, fortunately we made the column identifiers and dictionary keys for each data field all the same, so all we have to do is send the dictionary object, theRecord, an objectForKey: message with the identifier string of the column as the key argument. This string is obtained by sending aTableColumn an identifier message.
For example, tableView might send the message to get data from the data source with row 0 and aTableColumn in the argument list. Our code will access the first element of records (corresponding to the first row of the table) and point the variable theRecord to that. We then get the column identifier string we set in Interface Builder from aTableColumn, which might be "First Name" and use that as a key to access the first name data contained in theRecord. The returned object from objectForKey: is then stored in the variable theValue which is then returned to tableView.
We finally have the primary pieces of code in place to make this fly, so compile it, hope there aren't any typos or anything, and run it. You should be able to add, insert, and delete records, and they should all appear in the table view.
As a final exercise, the NSTableDataSource protocol mentions a method that we can implement that allows us to change the data of a record directly in the table. If you double-click on a cell, you will be able to edit it, but the changes won't be saved unless this method is implemented in the dataSource object. Go ahead and give it a go.
And, the end
So that's how we implement a table in Cocoa. It seems like a lengthy process, but its really straightforward once you realize how the different pieces and objects interact. We had in our application the Controller object, which served the noble purpose of adding or subtracting records from our data structure in response to user actions, as well as letting the table know when it should update its contents.
We also had a simple data structure, which was nothing more than a collection of standard Cocoa mutable dictionaries penned up in a standard Cocoa mutable array. Finally, our table view had a dataSource outlet that told the table where to send messages to get data to display.
In our case, we made Controller our dataSource. The only requirement an object needed to work as a dataSource was that it minimally conform to the NSTableDataSource protocol. If you want to learn more about protocols in general, check it out in Object-Oriented Programming and The Objective-C Language (by Apple, located online for those of you who still haven't read this book). If you're having trouble, you can download the project folder for AddressBook.
This tiny application has quite a bit of functionality for the amount of work we did, but it's hardly complete. For one, we have no way to save the data between different sessions. Notice that we didn't create this application as a document-based application, which we certainly could have done, but I want to show you a different way we can save data (think about how you might save and load the data for this address book if we had gone the doc-based app route. Is there anyway we can make this transparent to the user so that the same set of data is opened at launch time by default, and saved each time we modify it?). So the plan for the next column is to implement data saving, as well as let our application remember other things about itself before running (such as window size). Until then, happy coding!
Michael Beam is a software engineer in the energy industry specializing in seismic application development on Linux with C++ and Qt. He lives in Houston, Texas with his wife and son.
Read more Programming With Cocoa columns.
Return to the Mac DevCenter.
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 56 of 56.
-
Works but with a little bug
2005-06-19 17:24:01 jz87 [Reply | View]
I'm running this on XCode 2.1 that came with Tiger, and there seems to be a slight bug with delete version 4. If you have any duplicate records, if you select any one of the copies and delete, it'll delete all the copies. -
Ok fixed it
2005-06-19 17:49:39 jz87 [Reply | View]
I modified the code a bit, this will work if you have Panther or Tiger.
- (IBAction)deleteRecord:(id)sender
{
int status;
NSEnumerator* enumerator;
NSNumber* index;
NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
if ([tableView numberOfSelectedRows] == 0)
return;
NSBeep();
status = NSRunAlertPanel(@"Warning!", @"Are you sure you want to delete the selected record(s)?", @"OK", @"Cancel", nil);
if (status == NSAlertDefaultReturn)
{
enumerator = [tableView selectedRowEnumerator];
while (index = [enumerator nextObject])
{
[indexSet addIndex:[index intValue]];
}
[records removeObjectsAtIndexes:indexSet];
[tableView reloadData];
}
}
The documentation says that NSMutableIndexSet is only available since Panther. The remove object from array probably removes all matching objects, so it would remove all duplicates of an entry even if you just selected one. This version only removes the entries that were actually selected.
-
Won't Work
2005-01-27 22:43:55 DSG [Reply | View]
Your articles are Great learning tools. Thanks you for your work.
I just can't get this to work.
I'm using X code 1.1
I get two warnings when I run the compiler:
Controller.m:74: cannot find method `-indentifier'; return type `id' assumed
Controller.m:74: `NSTableColumn' may not respond to `-indentifier'
Controller.m:35: `NSMutableDictionary' may not respond to `+array'
The last warning is in the tempArray = object. I changed it to NSMutableArray as one of the other post mentioned.
- (id) tableView: (NSTableView *) aTableView
objectValueForTableColumn (NSTableColumn *)aTableColumn
row: (int) rowIndex
{
id theRecord, theValue;
theRecord = [records objectAtIndex:rowIndex];
Warning line: theValue = [theRecord objectForKey: [aTableColumn indentifier]];
return theValue;
}
When I run the program to add a name, I get this on the consol:
2005-01-27 23:20:20.723 Address Book[655] *** -[NSTableColumn indentifier]: selector not recognized
2005-01-27 23:20:20.725 Address Book[655] *** -[NSTableColumn indentifier]: selector not recognized
*** malloc[655]: Deallocation of a pointer not malloced: 0xbfffe4c0; This could be a double free(), or free() called with the middle of an allocated block; Try setting environment variable MallocHelp to see tools to help debug
The code is exactly as your artical discribes. It won't add the names in text fields. Whats up?? Thanks!!
-
no more supported?
2004-10-30 06:36:07 Valennad [Reply | View]
I am sure, this column is not supported for Xcode.
However the AddressBook doesn't run with Xcode in this form. Fortunately the next column consists a link to the Folder with the AddressBook 2. I have downloaded it and compared the sources. I found one more outlet-connection names dataSource between NSTableView and Control-Instance. I have added the connection and it runs!!!!!! I haven't found anything about the connection in this column or earlier. And what about you? It's a pity - I have lost so much time to find it out :((((((( Why are the columns not supportet for Xcode? Imagine: we are Beginner and if something not runs we are helpless.
Once more: open Interface Builder, open Info-Panel (shift-command-I), double-cklick on a TableView. The Info-panel schould be titled: "NSTableView Info". Ctrl-drag from TableView to Controller-Instance. Release the mouse. Click on "Outlets" in the Info-Panel. Choose "dataSource"-outlet and press "Connect"-button - connection with Controller is built. Now - build and run!
-
possible typo
2004-08-03 14:45:08 doublewood [Reply | View]
I am using xcode 1.2 and it will not work unless the
under deleteRcord:
NSMutableArray *tempArray = [NSMutableDictionary array];
is changed to
NSMutableArray *tempArray = [NSMutableArray array];
-
Java Version - Solution
2003-04-28 08:59:35 anonymous2 [Reply | View]
Nothing adds to my table!! I´ve tried everything and still there is nothing... Copied your solution and still nothing :(
Any tips? It compiles like it should so it have to be something else
-
Simpler but still Confused
2003-04-12 13:33:17 anonymous2 [Reply | View]
I've just read that column about Tables and still can't get it to work properly. Besides, I want to implement something simpler.
I have a NSTableView with a single column.
All I want to implement is this :
I enter a word or phrase in a NSTextField, and when I hit Return, that word adds to the NSTableView.
Thinking Cocoa is simple enough, I'm trying to associate my array, which contains every line I enter in the TextField, but I still get errors...
-
Not working for NSTextViews?
2003-03-05 04:22:22 anonymous2 [Reply | View]
Thanks for the great article.
I want to use NSTextViews instead of NSTextFields, i tried this but the following code won't work:
[record setObject:[firstNameView string]forKey:@"First Name"];
Somehow when you add the text from a NSTextView to the table it replaces the text of previously added records!
I worked around this by copying the text of the NSTextView to a NSTextField first like this:
[firstNameField setStringValue:[firstNameView string]];
[record setObject:[firstNameField stringValue]forKey:@"First Name"];
But there must be another way, i tried textStorage but that didn't work either, what 's going on that i don't get?
Cheers.
-
Erros Compiling
2003-03-04 19:11:14 anonymous2 [Reply | View]
Im getting 2 errors on compiling which i dont understand, Im sure they are very simple but Im debuting :D
Ths first is: illegal expression, found `@implementation'
@implementation Controller
The second is: "in file included from Controller.m:2: method definition not in class context
- (IBAction)addRecord:(id)sender
I inclued the lines it shows me were the errors are appearing. Any suggestions?
-
Solution for "Final Exercise"
2003-02-18 10:20:21 ewrenbeck [Reply | View]
I thought for future reference I would post my solution to the "Final Exercise". It didn't look like anyone had posted this code except in the java version of the solution.
I'm just as new to Cocoa as everyone else that is probably reading this article, so take this with a grain of salt:
- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex
{
// we know which table it is, no need to worry about it
// determine the row we want to change
//NSLog(@"\nrowIndex:%i", rowIndex);
id record = [records objectAtIndex:rowIndex];
// set the value
[record setObject:anObject forKey:[aTableColumn identifier]];
// reload table
[aTableView reloadData];
}
-
Where to put the code on page 3 and 4?
2002-12-08 10:17:45 anonymous2 [Reply | View]
I'm a totally newbie about programming...and, even if I love the easyness of IB and PB I am now stopped at page 3 because I don't know where to put the code about the "actions". Would someone help me please?
Thanks in advance
Paolo Basile
Italy
-
compiling woes
2002-10-21 21:33:33 anonymous2 [Reply | View]
I love these tutorials, and followed this one to a 't', but whenever i try to compile, i get a boatload of errors, such as:
/Users/kevind/AddressBook/Controller.m:11: illegal expression, found `unknown'
/Users/kevind/AddressBook/Controller.m:13: illegal expression, found `unknown'
there must be 20 or so of those, (they are contained in the create and delete record methods. Most of the errors come when calling things in any of the attached frameworks (which are linked and everything). Does anyone have _any_ clues as to how to fix this so i can move on? -
'unknown' errors
2003-01-22 01:19:10 edenwaith [Reply | View]
I had the same problem, too. I copied the material off the web site and pasted it into Project Builder. There might be some 'unseen' items that Project Builder doesn't like. What you need to do is erase all of the white space before each line and then press TAB to add your own white space.
Another problem I found was with creating the tempArray in the deleteRecord method. I used this to get things working:
NSMutableArray *tempArray = [[NSMutableArray alloc] init]; -
'unknown' errors
2003-05-15 10:51:02 anonymous2 [Reply | View]
NSMutableArray *tempArray = [[NSMutableArray alloc] init];
This will fix the error but you need to release the tempArray at the end of the method otherwise there will be a memory leak.
or you can do this which returns an autoreleased array?
NSMutableArray *tempArray = [NSMutableArray array];
-
Thanks again ...
2002-07-22 07:30:10 hgegenfurtner [Reply | View]
... for this excellent tutorial that actually got me started (instead of just startled) with my first own project. Thanks Mike, thanks O'Reilly!
The only improvement I'd suggest were for a better proof-reading. (I'd volunteer.)
-
Java Version - Solution
2002-05-25 15:03:39 jsumnertx [Reply | View]
I've been doing these exercises in Java rather than Objective C.
The code is below. Some notes:
1) Similar to the the Color Meter example, your Controller inherits from java.lang.Object rather than NSObject
2) Enable the debugger using the executables tab and set the debugger to "Java Debugger"
3) The alert dialogs are in a different object heirarchy.
4) The row enumerator returns "java.lang.Integer" objects rather than NSNumber. This, unfortunately, doesn't seem to be documented anywhere in Apple's docs.
5) Some of the methods weren't named exactly the same but they were easy enough to find when perusing the documentation.
6) You can't use "awakeFromNib" to initialize your variables. Set them in your class initialization instead.
Enjoy!
/* Controller */
import com.apple.cocoa.foundation.*;
import com.apple.cocoa.application.*;
public class Controller{
NSTextField emailField; /* IBOutlet */
NSTextField firstNameField; /* IBOutlet */
NSTextField homePhoneField; /* IBOutlet */
NSTextField lastNameField; /* IBOutlet */
NSTableView tableView; /* IBOutlet */
NSMutableArray records = new NSMutableArray();;
public NSDictionary createRecord()
{
NSMutableDictionary record = new NSMutableDictionary();
record.setObjectForKey(firstNameField.stringValue(), "First Name");
record.setObjectForKey(lastNameField.stringValue(), "Last Name");
record.setObjectForKey(emailField.stringValue(), "Email");
record.setObjectForKey(homePhoneField.stringValue(), "Home Phone");
return record;
}
public void addRecord(Object sender) { /* IBAction */
records.addObject(createRecord());
tableView.reloadData();
}
public void deleteRecord(Object sender) { /* IBAction */
if (tableView.numberOfSelectedRows() == 0)
return;
NSApplication.beep();
int status = NSAlertPanel.runAlert("Warning!", "Are you sure you want to delete the selected record(s)?", "OK", "Cancel", null);
if ( status == NSAlertPanel.DefaultReturn ) {
NSMutableArray tempArray = new NSMutableArray();
NSEnumerator enumerator = tableView.selectedRowEnumerator();
Integer item;
item = (Integer)enumerator.nextElement();
while ( item != null ) {
tempArray.addObject(records.objectAtIndex(item.intValue()));
item = (Integer)enumerator.nextElement();
}
records.removeObjectsInArray(tempArray);
tableView.reloadData();
}
}
public void insertRecord(Object sender) { /* IBAction */
int index = tableView.selectedRow();
if (index >= 0)
records.insertObjectAtIndex(createRecord(), index);
else
records.addObject(createRecord());
tableView.reloadData();
}
public int numberOfRowsInTableView(NSTableView aTableView){
return records.count();
}
public Object tableViewObjectValueForLocation( NSTableView aTableView, NSTableColumn aTableColumn, int rowIndex){
NSDictionary theRecord;
Object theValue=null;
theRecord = (NSDictionary)records.objectAtIndex(rowIndex);
if (theRecord!=null)
theValue = theRecord.objectForKey(aTableColumn.identifier());
return theValue;
}
}
-
Nothing Visible
2002-03-28 12:33:54 qmaze [Reply | View]
This is my first Cocoa lesson and after creating the address book and fixing my errors, I run the app but when I try to add anything, nothing shows up. Id I continue to click Add the scroll bar actually starts moving on the table view but no info.
What did I do wrong. Any help? -
Nothing Visible
2002-06-15 22:20:23 blueskyz [Reply | View]
I had the same problem! Finally figured out that you have to make sure to do the following:
InterfaceBuilder-
double click each NSTableColumn
Tools/Show Info
make sure you enter the identifier at the bottom.
Your entry won't stick unless you 'tab' out of the field
That should fix it.
Make sure your identifier value matches the
forKey:@"..." in createRecord -
RE: Nothing Visible
2002-06-09 14:59:40 hesnikke [Reply | View]
if it makes you feel any better, i'm haing the same problem! i'm not sure why, and i havn't figured out how to fix it...
if you came up with a solution, please reply! :) -
RE: Nothing Visible
2002-06-15 22:21:16 blueskyz [Reply | View]
I had the same problem! Finally figured out that you have to make sure to do the following:
InterfaceBuilder-
double click each NSTableColumn
Tools/Show Info
make sure you enter the identifier at the bottom.
Your entry won't stick unless you 'tab' out of the field
That should fix it.
Make sure your identifier value matches the
forKey:@"..." in createRecord
-
Anyone had selection problems with NSTableView?
2002-02-03 07:38:54 trav [Reply | View]
When the user finishes editing a field in an NSTableView, I want to select that row, and end editing (instead of editing the next row). However, selectRow:byExtendingSelection: doesn't work for this purpose...the AppKit documentation says it should stop all field editing, but it doesn't. Has anyone found a workaround for this (I assume) bug? Thanks!
-
Has anyone used NSTableView.SetAutosaveTableColumns() ?
2001-08-16 20:16:13 jakem [Reply | View]
In Interface Builder, when editing the attributes of a NSTableView there is a field called "AutoSave File Name". From reading the API I figured out that this is for specifying a file in which to automatically save a custom table layout (column width, order etc) for each user of the app. However, I can't get it to work!! I've specified some file names in Interface Builder, and called setAutosaveTableColumns(true) for each of my NSTableViews, but when I run the app, make modifications to the table layout, quit and run it again - the modifications aren't maintained. What am I doing wrong?? BTW I program in Java - I'd love pointers to any Java specific Cocoa resources! -
more info...
2001-08-16 20:57:54 jakem [Reply | View]
I just realised where the problem is (I think!). Interface Builder does not save the information about a NSTableView's autoSave name!! I enter the name in the attribute field, save the .nib file, quit, re-open the .nib file and the autoSave field is blank again!! - strange :( I guess the only solution is to set the autoSave name programatically, using the SetAutosaveName() method. However, where do I place this call? any ideas? -
more info...
2001-08-16 22:33:37 Michael Beam |
[Reply | View]
That's weird that IB doesn't save the info. I've gotten it to work for me. Just a thought, did you press enter or click on another field after entering the autosave name before you saved the nib file? I had this problem when i was first learning how to tables and the column identifiers would never stick b/c i would type the name and then click another column header to do it again.
If you want to set a table view's autosave name programatically, i think the best place to do this would be in awakeFromNib or whatever initialization method you use.
You can also do the same thing for windows. If you look at a window's attributes in the info panel, you will notice a field for the window's title, and a seperate one for the window's name. The name they speak of here is an autosave name, and by giving it a name you're application will rememeber window size and position between launches.
Mike -
more info...
2001-08-22 04:49:50 jakem [Reply | View]
Well, I've tried everything and IB simply does not save my table's autosave name. I can enter a autosave name for the window and this works great.. but no autosave for a table. Could this be because may tables are within a tab pane?? Has anyone else had this problem?
-
phone number field not displaying
2001-08-16 16:22:39 psheldon [Reply | View]
Haven't a clue to give. Do you guys get phone number field showing? Gotta download the (changed) working version, maybe then this paragraph will wind up in more decent draft.
;-)
Tried building the third protocol as per spec in the help viewer, but I don't know what gesture gets it going so I can change the data directly in the table. I suspected that an enter while the column and row was selected was the gesture, but that didn't work.
What's it? -
phone number field not displaying
2001-08-16 16:39:47 psheldon [Reply | View]
Sample project does display phone Field. Now I need to get some savvy for comparing the source code. It might be a whole lot easier to review the right way than look through the wrong way I wrote it.
We'll see.
-
corr I translate Mike's web pages to pdf
2001-08-16 20:53:53 psheldon [Reply | View]
I should have written : "Key value correct in source, incorrect in web page."
Hey, we all make mistakes if we are "pushing the edge" (like Mike, Help Viewer writers, debuggers all do).
;-) -
(A) phone number field not displaying
2001-08-16 20:13:22 psheldon [Reply | View]
Took a break for exercise and then took fresh look at nib and speculated how it might have its columns accessed.
The key name was Email but Mike had written forKey:@"email Address" in creatRecord of pdf instructions. He wrote it up correctly in the example source code.
The gesture question remained. I tried using the clipboard icon in help viewer and got a strange copy into bbedit and projectbuilder, eg ">" for ">", "&" for "&", "<" for "<". And with the straight copy fixed, I got direct data entry! The gesture was double click, change cell, enter.
The straight paste prettyprinted made clear the translations in the paste were really a fly in the ointment.
;-)
Feeling groovy.
-
(A) phone number field not displaying
2001-08-16 22:17:41 Michael Beam |
[Reply | View]
Sorry about the code typo. It should be changed to Email soon. Thanks for catching it. I knew i'd be causing myself trouble by changing all the @"email Address" keys to @"Email"!
Mike
-
Why not this?
2001-08-15 13:59:43 zootbobbalu [Reply | View]
I know their are millions of ways to program the same
thing, so I know this will sound like critisism, but I
can't help myself :-)
What would the pitfalls be if you kept a running count
of the number of deletions and just subtracted this
number from the NSNumber that the NSEnumerator returns with the nextObject message?
It would look something like this:
-(IBAction)deleteRecord:(id)sender
{
NSEnumerator *enumerator = [tableView selectedRowEnumerator];
NSNumber *index;
int deletecount;
int modindex;
for (deletecount=0,(index= [enumerator nextObject]), deletecount++ ) {
modindex=[index intValue] - deletecount;
[records removeObjectAtIndex:modindex];
}
[tableView reloadData];
}
-
Why not this?
2001-08-16 22:25:06 Michael Beam |
[Reply | View]
If this works for you then go for it! There's always more than one way of doing someting, as you pointed out. It looks like this would solve the problem of removeObjectsInArray: removing _all_ objects from records that matches anything in the argument array.
Mike -
Why not this?
2002-03-06 13:10:46 jeff_t [Reply | View]
If you wanted to be really clever (and neat), you could use the reverseObjectEnumerator, and you wouldn't have to worry about modifying the index at all. -
Why not this?
2003-02-20 06:30:18 swiftjlc [Reply | View]
I finally managed to find a couple of hours to play with cocoa. Great tutorial! Thanks for getting me started quickly, and thanks to Apple (and Next) for the impressive design.
Here is an implementation using reverseObjectEnumerator, thus tempArray is no longer needed.
- (IBAction) deleteRecord:(id)sender
{
int status;
NSNumber *index;
NSArray *array = [[tableView selectedRowEnumerator] allObjects];
NSEnumerator *enumerator = [array reverseObjectEnumerator];
if ([array count] <= 0)
return;
while (index = [enumerator nextObject]) {
[records removeObjectAtIndex:[index intValue]];
[tableView deselectRow:[index intValue]];
}
[tableView reloadData];
} -
Why not this?
2003-07-18 17:14:31 rgh [Reply | View]
This seems to work really well. Thanks! I do have one question. Cocoa documentation says that NSMutableArray should not be modified while enumerating. Could this break in some circumstance? I'll keep playing with it to see if I can get it to break.
-
Error in sample code
2001-08-14 06:10:58 johnts [Reply | View]
In the text of the article on the last page, in the sample code for the delete routine, there is this line:
NSMutableArray *tempArray = [NSMutableDictionary array];
That's incorrect. It should be
NSMutableArray *tempArray = [NSMutableArray array];
-
Error in sample code
2001-08-14 08:45:11 Michael Beam |
[Reply | View]
Thanks for catching that for me! I just sent in the correction.
Mike
-
Table with a NSPopUpMenu-column
2001-08-14 01:09:12 jtouwen [Reply | View]
I've made a few tables already, but i want to go a step further. I want to add a column to my tables with predefined values, as a popupmenu.
Could you give me a hint or expand this example with predifind categories as 'Friends', 'Work' etc.
Jasper
-
Off topic questions
2001-08-13 22:47:00 canyonrat [Reply | View]
I suppose everyone noticed that you can't send an array message to an NSMutableDictionary class object and have it work?
Should there be an @private directive at the start of the instance variable declarations in Controller.h to indicate it's a concrete class? Does ObjC use some other convention to disclose the author's intention about whether the class is concrete or abstract?
I notice that ObjC programmers tend to declare public accessors for everything and use messages to self rather in preference to assignment. This does make the program flow better but it also is a very different attitude than the typical C++ programmers fetish for encapsulation. Those guys begrudge every little chink in he armor around their instance variables. I can hear them looking at typical ObjC code an sniffing, "with that many accessors it's not an object, it's just a sruct."
So who is (mostly) right? Is the ObjC style the enemy of scaling to large projects? Is the C++ style just a knee-jerk?
-
Error Messages
2001-08-13 21:46:08 johnts [Reply | View]
Thanks for this article - I just happen to be trying to figure out tables! I had a couple of errors. Firstly, when I went to build it for the first time, it was complaining that the class didn't implement the createRecord method, even though it was there. I ended up quitting from Project Builder and reloading and it built fine.
Now the runtime errors. I've seen the first one below when I've deleted a record, either one or many. Doesn't happen every time. The second one happens if you select multiple rows and include the last row:
Aug 14 00:39:30 AddressBook[549] _windowDeviceRound: error creating graphics ctxt object for ctxt:77175, window:-1
Aug 14 00:39:30 AddressBook[549] *** -[NSCFArray objectAtIndex:]: index (3) beyond bounds (3)
Then strange things happen. When I then add a couple rows, then delete one one, the whole table is removed! -
Error Messages
2001-08-13 21:49:36 johnts [Reply | View]
An update. I did a Clean project and rebuilt and I am still getting "Controller.m:40: warning: `Controller' does not respond to `createRecord'" The method is right there, and spelled correctly. i've retyped it several times too. It's even in the method popup at the top of the window!
-
Error Messages
2003-02-18 10:15:58 ewrenbeck [Reply | View]
Thanks for the great article. I was getting starting with using tables and this was a big help.
I noticed that nobody really responded to this error message. You should forward declare the createRecord method in the header file.
Specifically, update the "controller.h" file and insert the following code just before the @end statement:
-(NSDictionary *)createRecord;
For maintainability, you really don't want to get into the business of worrying about which method goes where. Forward declarations solves this nightmare. -
Error Messages
2001-08-13 22:00:07 johnts [Reply | View]
Duh. My bad. I moved the method before the add and insert methods and it built fine. I still see the errors with deleting the whole table and the error messages. -
Error Messages
2001-08-13 22:13:01 johnts [Reply | View]
I figured out why it was removing multiple entries - my test consisted of inserting the same thing over and over. So, it would remove the duplicate rows, even though I only selected one. -
Error Messages
2001-08-14 08:48:50 Michael Beam |
[Reply | View]
Yeah, i just noticed the same thing the other day when i was trying out some stuff on it. My testing too consisted of entering several rows that were the same. That the way removeObjectsInArray: works ( i didn't think of that at the time of writing this however), to remove ALL objects from the receiver that might match a particular object in the argument array.
Mike -
Error Messages
2001-09-01 12:40:23 TheBum [Reply | View]
I know this suggestion is a couple of weeks late, but I just finished working through the exercise. I came up with a one-line correction to the duplicate deletion problem that allows apparent duplicate entries to remain in the array (emphasis on "apparent"). Just add the following line to createRecord:
[record setObject:[NSDate dateWithTimeIntervalSince1970:time(nil)] forKey:@"Tag"];
This approach uses the current system time to uniquely tag each record behind the scenes. You'll also have to add a "#import <time.h>" to the top of the .m file.
-
before closing shop in IB
2001-08-13 15:20:33 psheldon [Reply | View]
I saw a paragraph that sort of phased me . I think the statement "Interface builder has provisions for connecting a table view to a data sourrce object ..." covers tersely something profound . The next paragraph uses a spaced compound word, "data source" which is not the same thing as datasource and says data source is in the Controller . I think Apple oop pascal had a menagerie of distinctions of being in, belonging to, etc.
I know I am going to take a tumble about my confusion later, maybe three articles from now, if I don't try to struggle to say my confusion now . This is embarrassing probably naive question .
Where is an outlet :
where it comes from or
where it is connected to ?
I'm going to sweat a bit and stick my neck out .
How can we carefully say where an outlet is ? I think it might be wise to say, place an outlet from NSTableView to provide a hookup in controller . Or maybe we might say, though an outlet is owned by NSTableView because the info and wire comes from there, the outlet is "in" where it is needed for information . But, there is something else, where it is needed as something to send messages with, messages to the NSTableView! So, though NSTableView has the datasource, Controller is the data source.
I think I got it. Reading on!
;-) -
before closing shop in IB
2001-08-13 15:35:49 Michael Beam |
[Reply | View]
In general outlets are instance variables in a class that point to other objects and let use access those objects they point to. Like in Controller we created all the outlets that let us send messages to the text fields, for instance.
Now in NSTableView, the implementors of that class--the folks over at apple--created several outlets in NSTableView that the code of NSTableView uses to send messages to other objects. One of these outlets is called dataSource, and it is used to send messages to a data source object (two words). What we did is to make a connection between Controller, and the dataSource outlet in NSTableView. Does that help? If you have any more questions don't sweat sticking your head out; you can always send me an email too if you like. Good luck! -
before closing shop in IB
2001-08-13 15:43:59 psheldon [Reply | View]
Thanks Mike. Meditating on what you said .
;-) -
before closing shop in IB
2001-08-13 18:27:19 Michael Beam |
[Reply | View]
You're welcome...hope it makes sense!







I've seen your Address book Application.
It's running properly, but i would like to ask you one question:where the data is getting stored in the application because when i close the application and then again run it all the data i stored is lost(i.e i am not able to see that in my table again).
Is there any means to keep the data stored.
Is there something like database in cocoa for storing the data.