I have an NSTableView that contains several columns, each of which I've setup to be sortable by mapping the column's sort key to a property in my model and providing a compare: selector. This works perfectly for those columns that have a direct mapping to an NSString or NSInteger property in my model.
However, I have a couple of columns that are, effectively, aggregates of model data — i.e. my objectValueForTableColumn method will dig into the model and often uses two or more properties to generate the appropriate NSString to be displayed within that row/column.
I've been trying to get these columns to sort, but I'm not sure what goes into the sort key or selector. I've tried using one of the property names, tried using compare:, etc... but the code either does nothing or generates an exception.
I've read through Apple's documentation on NSTableView sorting and searched the web for examples, but it's not entirely clear how this situation should be handled. How do I map these columns to a key, and how does the comparison take place. As mentioned, the data displayed in the table is a string, which would seem to be sortable via compare:, or is there some mechanism for providing a custom compare function?
Thanks!
Cocoa provides built in sort descriptors, but these only handle basic data types: strings, dates, numbers. In your case, you will need to define a custom sort descriptor and pass that to the table - your data model is not a simple data type. All a sort descriptor does is take two values and reports back whether value 1 is greater than value 2, less than, or if they are equal. It would look something like this:
- (NSSortDescriptor*)myCustomSortDescriptor
{
if (_myCustomSortDescriptor == nil) {
NSComparisonResult (^myModelSortBlock)(id, id) = ^NSComparisonResult(id obj1, id obj2) {
//
// Here you have two objects from your model, i.e. the
// the two rows that NSTableView wants to check the order of.
// This is where your custom comparison goes. Then you return one of:
//
if (obj1 < obj2)
return NSOrderedAscending;
else if (obj1 > obj2)
return NSOrderedDescending;
else
return NSOrderedSame; // they must be the same
};
_myCustomSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"self"
ascending:YES
comparator:myModelSortBlock];
}
return _myCustomSortDescriptor;
}
I've packaged this in a method for convenience, but you can define it anywhere you want. You then pass your custom sort descriptor to the table.
Related
I understand I can return an NSDictionary by doing
- (NSDictionary *)keyWithValue {
return #{#"key" : #"value"};
}
but how can I return that without the enclosing #{} dictionary?
There is no tuples in Objective C unlike in Swift, Python etc. So the common way to return 2 different objects is to return an array or a dictionary.
You also can try something like:
- (NSString *)keyWithValue:(NSString**)value {
*value = #"value";
return #"key";
}
It should be used following way:
NSString *v;
NSString *k = [self keyWithValue:&v];
// now v contains #"value"
Objective-C, like C before it, doesn't allow the return of multiple values from a method. (Essentially, although a method or function can accept any number of arguments as input, it can only have a single return value.) There are historical and implementation reasons for this design but it can be frustrating when you simply have a pair/tuple to return.
If you have a method that has two distinct "results" that you need to return to the caller, you have a few choices. The very simplest in your case is to do something like what you are doing here and "wrapping" the values in a dictionary. You could similarly wrap them in a two-value array (which is a little less good since it relies on an implicit contract between caller and callee that there will be exactly two items in the array).
However, a clean and fairly standard approach here would be to create a small class with only two properties on it, and create, fill in, and return that instance with your pair of values. This arguably uses less runtime overhead than a collection object, and has the nice benefit of being semantically explicit and easy to understand for anyone else looking at it.
(There is yet another way, which involves passing pointers as arguments that are "outparams", but that's only idiomatic in rare circumstances in ObjC and I wouldn't recommend it here.)
There is no way to return a key value pair without a dictionary because that is the definition of the dictionary data structure. From apple docs:
The NSDictionary class declares the programmatic interface to objects that manage immutable associations of keys and values
You access the value with
[myDictionary objectForKey:#"myKey"];
If you want to use the returned key-value pair in another dictionary
NSMutableDictionary *otherDict = [NSMutableDictionary alloc] init];
[otherDict setObject:[myDictionary objectForKey:#"myKey"] forKey:#"myKey"];
I've been reading through a few different programs that use the sortUsingSelector method to sort an array with string objects and I can't figure out how they do it.
Each program begins by defining the sort method as follows:
[myBook sort]; /**myBook is the name of the array in the addressBook class**/
-(void) sort
{
[book sortUsingSelector: #selector (compareNames:)];
}
/**compareNames is defined in the addressCard class**/
The sort method uses a selector method that seems to do all the work:
-(NSComparisonResult) compareNames: (AddressCard *) element
{
return [name compare: element.name];
}
Its important to note that there are two different classes: addressCard and addressBook.
I know the compare method returns NSOrderedAscending, NSOrderedSame, or NSOrderedDescending to the sortUsingSelector method. But, how do these methods go about sorting everything? I feel like I'm missing something huge. For instance, how does the compare method know which elements in the array to compare? I imagine that element[0] of the array is compared with element[1] and then a sort occurs then the next element is compared...Does the compare method have a default definition that I'm overlooking?
The compare: method (or any method you use as the parameter in sortUsingSelector:) just has one job: given exactly two objects (a pair), tell me how to order them. That is all it does.
It knows how to do this because it is defined by the class it is sent to. In this case, name is an NSString so we use NSString's definition of the compare: method - a definition that knows how to order strings (using rules about alphabetical order).
It is the sortUsingSelector: method that actually hands the compare: method pairs to, uh, compare. And how it chooses those pairs is its business. You are not told how it picks those pairs. Deciding what pairs to choose and in what order is a deep business; it is the subject of your Computer Science 101 class. But you are shielded deliberately from those details in this situation.
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).
So I have a situation where im using a class as a kind of struct. now i want to override the isEqual: method so if this type of object is inside an array, i can use [objects indexOfObject:obj]
but, now say obj contains objects called duck, and chicken, i would like to go [objects indexOfObject:duck] and it will actually give me the index of obj
so i tried something like this:
- (BOOL)isEqual:(id)anObject {
if([anObject isKindOfClass:[Duck class]]){
return [anObject isEqual:_duck];
}
else if([anObject isKindOfClass:[Chicken class]]){
return [anObject isEqual:_chicken];
}
return [anObject isEqual:self];
}
which doesnt work and isnt even getting called... think that has to do with overriding hash, which i tried by just returning self.hash (effectively obj.hash) but that didnt work.
Do you think something like this is possible? or should i just use for loops to search through all my obj's to find which duck is contained in which obj and return the index of it (which i can do, just wanted to try be cool and neat at the same time)
It sounds using -isEqual: is a bad idea here.
You can't have a DuckAndChicken that compares equal to its Duck and its Chicken, (and vice versa) because in order to stay consistent, all ducks and chickens would then have to compare equal.
Example:
duck1 + chicken1 compares equal to duck1
chicken1 compares equal to duck1 + chicken1
duck2 + chicken1 compares equal to chicken1
duck2 compares equal to duck1
Universe explodes
The good news is that you don't have to use -indexObject: to retrieve from the array. NSArray has a handy -indexOfObjectPassingTest: method that should do what you want.
[ducksAndChickens indexOfObjectPassingTest:^BOOL(DuckAndChicken *obj, NSUInteger idx, BOOL *stop) {
if ([obj.duck isEqual:myDuck]) {
// woop woop...
}
}];
Yes, it sounds like a hash issue.
I would avoid trying to do this by overriding isEqual: because you're liable to break things that you don't think you'd break.
Instead add a custom method that you can call to determine your version of equivalence. Then have a helper method or a category on NSArray which adds my_indexOfObject: and does the iteration.
Its hard to tell what you really want to do from your example but could you perhaps have two dictionaries one where ducks are the key and one where chickens are the key and the object value is either the actual parent object or a NSNumber with the index in the array. This would make lookup much quicker though would take up more memory, and could make synchronisation between the three data structs.
I'm writing a game using cocos2d-iphone and our stages are defined in .plist files. However, the files are growing large - so I've developed an editor that adds some structure to the process and breaks most of the plist down into fixed fields. However, some elements still require plist editor type functionality, so I have implemented an NSOutlineView on the panels that show 'other parameters'. I am attempting to duplicate the functionality from XCode's 'Property List Editor'.
I've implemented the following system; http://www.stupendous.net/archives/2009/01/11/nsoutlineview-example/
This is very close to what I need, but there is a problem that I've spent most of today attempting to solve. Values for the key column are calculated 'backwards' from the selected item by finding the parent dictionary and using;
return [[parentObject allKeysForObject:item] objectAtIndex:0];
However, when there are multiple items with the same value within a given dictionary in the tree, this statement always returns the first item that has that value (it appears to compare the strings using isEqualToString: or hash values). This leads to the key column showing 'item1, item1, item1' instead of item1, item2, item3 (where items 1-3 all have value ''). I next tried;
-(NSString*)keyFromDictionary:(NSDictionary*)dict forItem:(id)item
{
for( uint i = 0; i < [[dict allKeys] count]; i++ ) {
id object = [dict objectForKey:[[dict allKeys] objectAtIndex:i]];
if ( &object == &item ) {
return [[dict allKeys] objectAtIndex:i];
}
}
return nil;
}
But this always returns nil. I was hoping that somebody with a bit more experience with NSOutlineView might be able to provide a better solution. While this problem only appears once in the linked example, I've had to use this a number of times when deleting items from dictionaries for instance. Any help would be greatly appreciated.
return [[parentObject allKeysForObject:item] objectAtIndex:0];
However, when there are multiple items with the same value within a given dictionary in the tree, this statement always returns the first item that has that value …
Well, yeah. That's what you told it to do: “Get me all the keys for this value; get me the first item in the array; return that”.
… this statement always returns the first item that has that value (it appears to compare the strings using isEqualToString: or hash values).
It's not that statement that's doing it; it's how dictionaries work: Each key can only be in the dictionary once and can only have exactly one object as its value, and this is enforced using the hash of the key and by sending the keys isEqual: messages (not the NSString-specific isEqualToString:—keys are not required to be strings*).
The values, on the other hand, are not uniquified. Any number of keys can have the same value. That's why going from values to keys—and especially to a key—is so problematic.
*Not in NSDictionary, anyway. When you attempt to generate the plist output, it will barf if the dictionary contains any non-string keys.
I next tried;
-(NSString*)keyFromDictionary:(NSDictionary*)dict forItem:(id)item
{
for( uint i = 0; i < [[dict allKeys] count]; i++ ) {
id object = [dict objectForKey:[[dict allKeys] objectAtIndex:i]];
if ( &object == &item ) {
return [[dict allKeys] objectAtIndex:i];
}
}
return nil;
}
But this always returns nil.
That's the least of that code's problems.
First, when iterating on an NSArray, you generally should not use indexes unless you absolutely need to. It's much cleaner to use fast enumeration.
Second, when you do need indexes into an NSArray, the correct type is NSUInteger. Don't mix and match types when you can help it.
Third, I don't know what you meant to do with the address-of operator there, but what you actually did was take the address of those two variables. Thus, you compared whether the local variable object is the same variable as the argument variable item. Since they're not the same variable, that test always returns false, which is why you never return an object—the only other exit point returns nil, so that's what always happens.
The problem with this code and the earlier one-liner is that you're attempting to go from a value to a single key, which is contrary to how dictionaries work: Only the keys are unique; any number of keys can have the same value.
You need to use something else as the items. Using the keys as the items would be one way; making a model object to represent each row would be another.
If you go the model-object route, don't forget to prevent multiple rows in the same virtual dictionary from having the same key. An NSMutableSet plus implementing hash and isEqual: would help with that.
You probably should also make the same change to your handling of arrays.
To clarify, I eventually resolved this problem by creating proxy objects for each of the collections in the plist file (so, for every NSMutableArray or NSMutableDictionary). This meant that I essentially mirrored the Plist structure and included references back to the original objects at each level. This allowed me to store the array index for each object or the dictionary key, so when saving items back from the outline view to the Plist structures, I used the 'key' or 'index' properties on the proxy object.