I've been attempting this for two days, and constantly running into dead ends.
I've been through Aaron Hillegass's Cocoa Programming for MAC OS X, and done all the relevant exercises dealing with NSTableview and mutable arrays, and I have been attempting to modify them to suit my needs.
However none of them seem to be using an array with objects as a data source, it seems to use the tableview as the datasource.
I'm trying to implement Jonas Jongejan's "reworking" of my code here, with a Cocoa front end to display the results.
Any pointers or suggestions I know this should be simple, but I'm lost in the wilderness here.
I can populate the table by setting the array
It's pretty simple really, once you get to understand it (of course!). You can't use an NSArray directly as a table source. You need to either create a custom object that implements NSTableViewDataSource or implement that protocol in some existing class - usually a controller. If you use Xcode to create a standard document based application, the document controller class - (it will be called MyDocument) is a good class to use.
You need to implement at least these two methods:
– numberOfRowsInTableView:
– tableView:objectValueForTableColumn:row:
If you have a mutable array whose values you'd like to use in a table view with one column, something like the following should do as a start:
– numberOfRowsInTableView: (NSTableView*) aTableView
{
return [myMutableArray count];
}
– tableView: (NSTableView*) aTableView objectValueForTableColumn: (NSTableColumn *)aTableColum row: (NSInteger)rowIndex
{
return [myMutableArray objectAtIndex: rowIndex];
}
It has just occurred to me that you could add the above two methods as a category to NSArray replacing myMutableArray with self and then you can use an array as a data source.
Anyway, with a mutable array, it is important that any time you change it, you need to let the table view know it has been changed, so you need to send the table view -reloadData.
If your table view has more than one column and you want to populate it with properties of objects in your array, there's a trick you can do to make it easier for yourself. Let's say the objects in your array are instances of a class called Person with two methods defined:
-(NSString*) givenName;
-(NSString*) familyName;
and you want your table view to have a column for each of those, you can set the identifier property of each column to the name of the property in Person that that column displays and use something like the following:
– tableView: (NSTableView*) aTableView objectValueForTableColumn: (NSTableColumn *)aTableColum row: (NSInteger)rowIndex
{
Person* item = [myMutableArray objectAtIndex: rowIndex];
return [item valueForKey: [tableColumn identifier]];
}
If you replace valueForKey: with valueForKeyPath: and your Person class also has the following methods:
-(Person*) mother;
-(Person*) father;
-(NSString*) fullName; // concatenation of given name and family name
you can add table columns with identifiers like: father.fullName or mother.familyName and the values will be automatically populated.
You could go the datasource route and do all of the heavy lifting yourself, or you could let bindings do all the heavy lifting for you. Add an NSArrayController to the nib file that has the table view in it. Make sure that the File's Owner of the nib is set to the same class that has the mutable array in it. Bind the contentArray of the array controller to File's Owner.myMutableArray. For each column bind Value to the array controller arrangedObjects and add the appropriate key path. This will allow you to get things like user sorting for free if you ever need it.
On the iPhone (I know you're talking about Mac, but maybe this could help) you have to use delegation for loading a tableView. It asks for a cell and you use your array to fill-in the data where needed.
I'm not sure if this works for the Mac, but it'd be worth looking into.
Maybe set dataSource to self and use those delegate methods to access your array based on the row and column #
Apple has a whole guide for Table View Programming so I suggest you start with the Using a Table Data Source section of the that guide.
Related
I have two NSTableViews on screen; I just want to drag a row from one table to the other table. I see lots of tips here and there but I do not see a complete example and I'm a bit confused. I saw examples that were totally different to Apples sample apps TableView playground and drag and drop outlineView.
I decided to use Apples method, but now im stuck. TableView playground implement these methods in their model object.
- (NSArray *)writableTypesForPasteboard:(NSPasteboard *)pasteboard
- (id)pasteboardPropertyListForType:(NSString *)type
- (NSPasteboardWritingOptions)writingOptionsForType:(NSString *)type pasteboard:(NSPasteboard *)pasteboard
I dont understand how to set these up. For the 1st method i returned an array with string #"com.mycompany.myapp.mypasteboardtype"as suggested in this question.
What should i put for the 2nd method? My model is a custom object that has a number of strings, Arrays, and dictionary variables. I also do not understand the 3rd method. I wish there was some example i could see that does a simple drag from one table to another with a custom model object.
EDIT: My implementation based on response below
-(id)pasteboardPropertyListForType:(NSString *)type {
return [NSKeyedArchiver archivedDataWithRootObject:self];
}
-(NSArray *)writableTypesForPasteboard:(NSPasteboard *)pasteboard {
return [NSArray arrayWithObject:myDragType];
}
// Other methods that need to be implemented
-(id)initWithPasteboardPropertyList:(id)propertyList ofType:(NSString *)type {
return [NSKeyedUnarchiver unarchiveObjectWithData:propertyList];
}
+(NSArray *)readableTypesForPasteboard:(NSPasteboard *)pasteboard {
return [NSArray arrayWithObject:myDragType];
}
// And finally your object needs comply with NSCoder protocol. These following 2 methods needs to go in the object model associated with a row.
-(void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:oneOfMyIvarsToEncode forKey:#"someKey"];
}
-(id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
oneOfMyEndodedIvars = [aDecoder decodeObjectForKey:#"someKey"];
}
return self;
}
What should i put for [pasteboardPropertyListForType:]? My model is a custom object that has a number of strings, Arrays, and dictionary variables.
It depends on what type you're being asked for.
If it's your own custom type that you made up, you can put whatever you want. Really—if it's your type that you've invented, you can return whatever you want here, as long as it's a valid property list. You just have to make sure that your paste/drop code (pasteboard reading) is expecting the same stuff.
If you want to support internal drags (reordering and/or relocation within the hierarchy), you should have at least one custom type that identifies the object so that you can look up the same object when accepting the drop (in order to move it rather than duplicate it). For a Core Data object, you might use the absolute string of the object's identifying URI.
If you want to support dragging to other applications, you should have at least one non-custom type that those other applications will recognize. What type(s) you support will depend on what your model represents: if they're images (or recipes for creating them, such as stacks of layers), you could support PNG, TIFF, JPEG, etc.; if they're contacts, you could support vCard; if they're text, you could support plain text and/or RTF and/or HTML and/or WebArchive and/or Microsoft Word; etc.
You return an array of the types that this object can be turned into from writableTypesForPasteboard:; afterward, pasteboardPropertyListForType: must look at what type it was asked for and return a property list of that type.
For most external formats, pasteboardPropertyListForType: must return a data object. For your own custom formats, it's usually either a dictionary or an array of dictionaries, although, as I said, the only real requirements are that it must be a plist of some sort and your reading code must be able to understand it.
I also do not understand [writingOptionsForType:pasteboard:].
You must return a bit mask indicating when and how you will write the type to the pasteboard. The available options are documented.
Currently, there's only one: You can promise the data. This means that the pasteboard will not immediately ask you for the data; it will wait until the user pastes or drops it somewhere (or the data is otherwise requested by any application—e.g., a poorly-written drop validation method could request the data and examine it during validation, rather than drop acceptance). Only then will it call pasteboardPropertyListForType: for that type. (And this is a per-type option; you can choose to promise some types but not others.)
Promising is great for data that is expensive to compute and/or store; for example, a compressed archive (compute) or a large image (store).
I've got a NSMutableArray (containing NSMutableDictionary instances) bound to an NSArrayController (the NSArrayController is in turn bound to NSTableView columns).
What is the most Cocoa-, and KVO- friendly way of, programmatically :
adding a new empty object (NSMutableDictionary) to the array?
removing currently selected object? (after removing, the previous item - if exists - should be selected)
I've always been doing this in a way I don't particularly like - and I'm sure it's not the best way around (too many lines of code for something so simple : in Cocoa that indicates a wrong take on the subject :-)).
My code (quite an overkill, actually) :
Adding to the Array
NSMutableArray* oldParams = [paramsArray mutableCopy];
[oldParams addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:#"Parameter",#"Parameter",#"",#"Value", nil]];
[self setParamsArray:oldParams];
[paramsController setSelectionIndex:[paramsArray count]-1];
Removing currently selected object from the Array
if ([paramsArray count]>0)
{
int s = [paramsController selectionIndex];
NSMutableArray* oldParams = [paramsArray mutableCopy];
[oldParams removeObjectAtIndex:s];
[self setParamsArray:oldParams];
if (s<=[paramsArray count]-1)
[paramsController setSelectionIndex:s];
else
[paramsController setSelectionIndex:[paramsArray count]-1];
}
So, what are your opinions on that?
Given that the array controller is bound to a property named paramsArray on some object, the best approach is to define the key-value coding indexed accessors on that object's class. Then, use those accessors to mutate the to-many relationship represented by the property in a KVO-compliant manner.
For example, implement -insertObject:inParamsArrayAtIndex: and then use that to add an object. If you like the convenience of NSMutableArray's -addObject: method, you can write an -addObjectToParamArray method that forwards to -insertObject:inParamsArrayAtIndex:.
By the way, "paramsArray" is a poor name for a property. The property name shouldn't encode the type used to implement it. If you look at the templates for the indexed accessor names, you'll see that Apple is expecting to-many relationship properties to just be a plural noun like "params" (no "Array"). For example, -paramsAtIndexes: is better than -paramsArrayAtIndexes:.
You have to think of your array as the controller's backing store, and that it's managing it for you.
Adding an object:
[[self accountsArrayController] addObject:accountDictionary];
Removing the currently selected object:
[[self accountsArrayController] remove:nil];
You'll have to write another line or two to make that previous item selected, but that's an exercise I leave to you.
I have an NSTableView that binds via an NSArrayController to an NSMutableArray of NSString's.
There's also a TextView in the same window. What I want to do is display a number of occurrences of each NSString in the NSTableView.
I've thought of a way to do this, but that doesn't look elegant way of doing this:
Inherit from NSString and define new method that performs a search in predefined object (NSTextView) and returns the number of occurrences.
I'm guessing there must be a more natural way of achieving the same result?
EDIT:
Sorry, should have clarified. NSSMutableArray is an array of NSObjects that have an NSString property. I suppose I could define an extra method (findAllOccurencesOfString:inString:) which would return a number. But the question is how do I bind to this function and in that binding how to I pass a var (pointer to textField)??
You'll need to have a wordCount (read only) property on whatever objects are in your table data source, this will have to call your new method internally using the object's own string value, as you can't pass parameters in bindings (unless they've changed, I haven't used bindings for a while as I've been concentrating on iOS). Then bind to this property for the column in the table. Presumably you don't need to pass the pointer to the textfield as there is only one?
I have an NSOutlineView that is bound to an NSTreeController.
In Interface builder, I have bound each column of NSOutlineView to the tree controller with the Controller key "arrangedObjects" and model key path as the entity attribute or a method in my entity class.
Now all other columns sort perfectly except for one special column. The special column has a model key path bound to a method that is declared in my Entity class. This method depending on some condition in my code will return either NSString or NSDictionary.
When it returns an NSDictionary, the delegate method:
- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
parses the NSDictionary and returns a double value for that cell. Also note that the cell in this case is derived from NSCell and displays a progress bar.
In short, my special column will display a mix of progress bars and strings depending on the situation.
I would like to implement sorting so that all progress bars stay together and the strings are sorted alphabetically.
Perhaps you will have some luck if you try setting the table column's sort descriptor to a descriptor you create with a comparator that looks at the objects' classes. (You could also try filling in the "Sort Key" in IB, using the "class" key, but I think this might be less likely to work...)
Well i found a solution to this myself and just 2 minutes after posting my query :) The solution was to add a custom sort key in IB and define a method with that key name in my Entity class that returns an NSInteger. Since i want all NSDictionary objects to stay together and all NSStrings to be sorted, i return appropriate integer based on the object type.
Note for anyone who might be stuggling with sort not working: Make sure binding for sortDescriptor is enabled in IB.
try
NSSortDescriptor *sorter = [[[NSSortDescriptor alloc]
initWithKey:NULL
ascending:YES
selector:#selector(localizedCaseInsensitiveCompare:)] autorelease];
[[myOutlineView tableColumnWithIdentifier:#"Name"]setSortDescriptorPrototype:sorter];
I have a custom class in Obj-C called RouteManager which contains an array of NSStrings. Each string is a bus stop name which is used as a key for a dictionary to get the rest of the information for the bus stop (basically, just [busStopDictionary allkeys]). In one of the situations where my app uses this array, I want to return the array sorted by the distance from the user. I've started setting up the code to be able to call sortedArrayUsingSelector on my array with the following method:
- (NSComparisonResult)compareByDistance:(NSString*) otherStop
{
// Return appropriate NSOrdered enum here based on comparison of
// self and otherStop
}
My problem is that in the case where compareByDistance is a method of RouteManager, self refers to the instance of RouteManager. However, I need self to refer to the NSString that the compare is being called on. So, I assumed I needed to setup a category, as such:
#interface NSString (Support)
-(NSComparisonResult) compareByDistance:(NSString*)otherStop;
#end
This got my self reference correct, however this comparison uses values from the RouteManager class. When implemented as seen above, the NSString (Support) implementation obviously complains that those values are undeclared.
That should provide enough background info for my question. How do I go about doing this? I would like my category of NSString, which consists solely of the method compareByDistance, to be able to use values from the current instance of my class, RouteManager, which inherits from NSObject. Ideally, I feel as though the category should somehow be within RouteManager. I feel there has to be some way to accomplish this that is cleaner than passing the necessary values into compareByDistance. Thanks in advance for any and all help.
Your best bet would be to define a custom class for a bus stop, instead of storing them as strings and dictionaries.
Make the BusStop class have properties for Name, Location and whatever else. Implement the compareByDistance: method on the BusStop class.
You can still use a dictionary if you need to look them up by name. Just store them with the name as the dictionary's key, and the BusStop object as the dictionary's value.