Objective-C bindings - Binding an enum to an NSPopupButton - objective-c

I'm working on a project which would be ideally suit Cocoa bindings for the UI but I'm having an issue binding the value of an object property and can't find a suitable solution. The object is as follows:
typedef enum tagCSQuality {
kQualityBest = 0,
kQualityWorst = 1
} CSQuality;
#interface CSProfile : NSObject {
NSString *identifier;
NSString *name;
CSQuality quality;
}
In the XIB, I have an object controller whose content object is bound to a "currentSelection" property of the window controller which is an instance of the above object. I've then bound the name and identifier which all work as expected but I cannot see how I can bind the enums.
Ideally I would like an NSPopupButton to display "Best" and "Worst" and pick the correct enum value. I have updated the enum to have an explicit numeric value and I believe that I need a value transformer to convert the values but I'm stuck on exactly how this could be implemented.
Can anyone help me out or point me in the right direction?
Thanks,
J

You can use an NSValueTransformerfor this.
Since the enumeration values are integers only, they are encapsulated in an NSNumber object.
An valid transformer could look like the following.
+(Class)transformedValueClass {
return [NSString class];
}
-(id)transformedValue:(id)value {
CSQuality quality = [value intValue];
if (quality == kQualityBest)
return #"Best";
else if (quality == kQualityWorst)
return #"Worst";
return nil;
}
This can be bound to the Selected Value binding of the NSPopupButton.
If you want to create a bidirectional binding (i.e. be able to select something in the NSPopupButton you have to add the following code for the reverse transformation:
+(BOOL)allowsReverseTransformation {
return YES;
}
-(id)reverseTransformedValue:(id)value {
if ([#"Worst" isEqualToString:value])
return [NSNumber numberWithInt: kQualityWorst];
else if ([#"Best" isEqualToString:value])
return [NSNumber numberWithInt: kQualityBest];
return nil;
}

An enum is not an object. Cocoa bindings are a way to connect model objects to view objects.

If you are using Interface Builder, you can embed enum represented integer for each NSMenuItem items through property panel. Then select NSPopUpButton and specify binding 'selected tag' to the property with key path.
In this example, assume, IB's file owner is CSProfile. Prepare NSPopUpButton with two NSMenuItem items and tag them with 0(kQualityBest) and 1(kQualityWorst). Then navigate 'selected tag' of NSPopUpButton and check bind to 'File's owner'(CSProfile) with Model Key Path 'quality'.
#interface CSProfile : NSObject {
NSString *identifier;
NSString *name;
CSQuality quality;
}
#property (assign) CSQuality quality;

Related

Set property values of an Objective-C class using reflection

I am trying to learn reflection in Objective-C. I've found some great information on how to dump the property list of a class, especially here, but I want to know if it is possible to set a property's value using reflection.
I have a dictionary of keys (property names) and values (all NSStrings). I want to use Reflection to get the property and then set its value to the value in my dictionary. Is this possible? Or am I dreaming?
This has nothing to do with the dictionary. I am merely using the dictionary to send in the values.
Like this question, but for Objective C.
- (void)populateProperty:(NSString *)value
{
Class clazz = [self class];
u_int count;
objc_property_t* properties = class_copyPropertyList(clazz, &count);
for (int i = 0; i < count ; i++)
{
const char* propertyName = property_getName(properties[i]);
NSString *prop = [NSString stringWithCString:propertyName encoding:NSUTF8StringEncoding]];
// Here I have found my prop
// How do I populate it with value passed in?
}
free(properties);
}
Objective C properties automatically conform to the NSKeyValueCoding protocol. You can use setValue:forKey: to set any property value by a string property name.
NSDictionary * objectProperties = #{#"propertyName" : #"A value for property name",
#"anotherPropertyName" : #"MOAR VALUE"};
//Assuming class has properties propertyName and anotherPropertyName
NSObject * object = [[NSObject alloc] init];
for (NSString * propertyName in objectProperties.allKeys)
{
NSString * propertyValue = [objectProperties valueForKey:propertyName];
[object setValue:propertyValue
forKey:propertyName];
}
The NSKeyValueCoding protocol, which NSObject implements (see NSKeyValueCoding.h), contains the method -setValuesForKeysWithDictionary:. This method takes exactly the kind of dictionary you describe and sets the appropriate properties (or ivars) of the reciever.
This is absolutely reflection; the code in setValuesForKeysWithDictionary: accesses the properties by the names you give it, and will even find the appropriate ivar if no setter method exists.

Enable a button based on a value in the NSTableview

I have a NSTableview. I need to enable the button based on a value of a column in the tableview. For instance, In the table view i have a column, Status. I have 2 kinds of status, Withdrawn and Booked. If i click on a row which has the status as Withdrawn, i need to disable the withdraw button.
Can i be able to do it through binding? How could i do it? Pls help me out. Thanks.
Provided you create a custom NSValueTransformer, you can enable or disable the button using bindings.
You can bind the Enabled property of the button as follows:
Bind to: arrayController
Controller Key: selection
Model Key Path: status
Value Transformer: MDStatusValueTransformer
NOTE: in place of arrayController, you should select whatever the name of your array controller is in the nib file. In place of MDStatusValueTransformer, you should specify whatever class name you end up naming the class I've provided below.
As I mentioned, you'll need to create a custom NSValueTransformer. The enabled property expects a BOOL wrapped in an NSNumber, but your status property is an NSString. So, you'll create a custom NSValueTransformer that will examine the incoming status NSString, and return NO if status is equal to #"Withdrawn".
The custom NSValueTransformer should look something like this:
MDStatusValueTransformer.h:
#interface MDStatusValueTransformer : NSValueTransformer
#end
MDStatusValueTransformer.m:
#implementation MDStatusValueTransformer
+ (Class)transformedValueClass {
return [NSNumber class];
}
+ (BOOL)allowsReverseTransformation {
return NO;
}
- (id)transformedValue:(id)value {
if (value == nil) return nil;
if (![value isKindOfClass:[NSString class]]) return nil;
if ([value isEqualToString:#"Withdrawn"]) {
return [NSNumber numberWithBool:NO];
}
return [NSNumber numberWithBool:YES];
}
#end

NSPopupButton Bindings with Value Transformer

I don't know if what I see with a popup button populated by bindings with a value transformer is the way it's supposed to be or not -- the unusual thing I'm seeing (at least with respect to what I've seen with value transformers and table views) is that the "value" parameter in the transformedValue: method is the whole array bound to the array controller, not the individual strings in the array. When I've done this with table views, the transformer is called once for each displayed row in the table, and the "value" parameter is whatever object is bound to that row and column, not the whole array that serves as the content array for the array controller.
I have a very simple app to test this. In the app delegate there is this:
+(void)initialize {
RDTransformer *transformer = [[RDTransformer alloc] init];
[NSValueTransformer setValueTransformer:transformer forName:#"testTransformer"];
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
self.theData = #[#{#"name":#"William", #"age":#"24"},#{#"name":#"Thomas", #"age":#"23"},#{#"name":#"Alexander", #"age":#"64"},#{#"name":#"James", #"age":#"47"}];
}
In the RDTransformer class is this:
+ (Class)transformedValueClass {
return [NSString class];
}
+(BOOL)allowsReverseTransformation {
return NO;
}
-(id)transformedValue:(id)value {
NSLog(#"%#",value);
return value;
}
In IB, I added an NSPopupButton to the window and an array controller to the objects list. The content array of the controller is bound to App Delegate.theData, and the Content Values of the popup button is bound to Array Controller.arrangedObjects.name with the value transformer, testTransformer.
When I run the program, the log from the transformedValue: method is this:
2012-09-19 20:31:39.975 PopupBindingWithTransformer[793:303] (
)
2012-09-19 20:31:40.019 PopupBindingWithTransformer[793:303] (
William,
Thomas,
Alexander,
James
)
This doesn't seem to be other people's experience from what I can see on SO. Is there something I'm doing wrong with either the bindings or the value transformer?
Unfortunately, this is how NSPopUpButton works. The problem is not limited to that control. If you try binding an NSArrayController.contentArray to another NSArrayControllers.arrangedObject.someProperty you will get the same problem. Here is a simple workaround that I use in all my value transformers, which makes them work with both tables and popups:
You can modify your value transformer in the following way:
-(id)transformedArrayValue:(NSArray*)array
{
NSMutableArray *result = [NSMutableArray array];
for (id value in array)
[result addObject:[self transformedValue:value]];
return result;
}
-(id)transformedValue:(id)value
{
if ([value isKindOfClass:[NSArray class]])
return [self transformedArrayValue:value];
// Do your normal-case transform...
return [value lowercaseString];
}
It's not perfect but it's easy to replicate. I actually put the transformedArrayValue: in a class category so that I don't need to copy it everywhere.

How to detect a property return type in Objective-C

I have an object in objective-c at runtime, from which I only know the KVC key and I need to detect the return value type (e.g. I need to know if its an NSArray or NSMutableArray) of this property, how can I do that?
You're talking about runtime property introspection, which happens to be something that Objective-C is very good at.
In the case you describe, I'm assuming you have a class like this:
#interface MyClass
{
NSArray * stuff;
}
#property (retain) NSArray * stuff;
#end
Which gets encoded in XML something like this:
<class>
<name>MyClass</name>
<key>stuff</key>
</class>
From this information, you want to recreate the class and also give it an appropriate value for stuff.
Here's how it might look:
#import <objc/runtime.h>
// ...
Class objectClass; // read from XML (equal to MyClass)
NSString * accessorKey; // read from XML (equals #"stuff")
objc_property_t theProperty =
class_getProperty(objectClass, accessorKey.UTF8String);
const char * propertyAttrs = property_getAttributes(theProperty);
// at this point, propertyAttrs is equal to: T#"NSArray",&,Vstuff
// thanks to Jason Coco for providing the correct string
// ... code to assign the property based on this information
Apple's documentation (linked above) has all of the dirty details about what you can expect to see in propertyAttrs.
Cheap answer: use the NSObject+Properties source here.
It implements the same methodology described above.
The preferred way is to use the methods defined in the NSObject Protocol.
Specifically, to determine if something is either an instance of a class or of a subclass of that class, you use -isKindOfClass:. To determine if something is an instance of a particular class, and only that class (ie: not a subclass), use -isMemberOfClass:
So, for your case, you'd want to do something like this:
// Using -isKindOfClass since NSMutableArray subclasses should probably
// be handled by the NSMutableArray code, not the NSArray code
if ([anObject isKindOfClass:NSMutableArray.class]) {
// Stuff for NSMutableArray here
} else if ([anObject isKindOfClass:NSArray.class]) {
// Stuff for NSArray here
// If you know for certain that anObject can only be
// an NSArray or NSMutableArray, you could of course
// just make this an else statement.
}
This is really a comment addressing an issue raised by Greg Maletic in response to answer provided by e.James 21APR09.
Agreed that Objective-C could use a better implementation for getting these attributes.
Below is a method I quickly threw together to retrieve attributes of a single object property:
- (NSArray*) attributesOfProp:(NSString*)propName ofObj:(id)obj{
objc_property_t prop = class_getProperty(obj.class, propName.UTF8String);
if (!prop) {
// doesn't exist for object
return nil;
}
const char * propAttr = property_getAttributes(prop);
NSString *propString = [NSString stringWithUTF8String:propAttr];
NSArray *attrArray = [propString componentsSeparatedByString:#","];
return attrArray;
}
Partial list of attribute keys:
R Read-only
C Copy of last value assigned
& Reference to last value assigned
N Nonatomic property
W Weak reference
Full list at Apple
You can use isKindOfClass message
if([something isKindOfClass:NSArray.class])
[somethingElse action];
If you know that the property is defined :
id vfk = [object valueForKey:propertyName];
Class vfkClass = vfk.class;
And compare with isKindOfClass, isSubClass, etc.

What is the type for boolean attributes in Core Data entities?

I am using Core Data programmatically (i.e. not using .xcdatamodel data model files) in much the same manner as depicted in Apple's Core Data Utility Tutorial. So my problem is that when I try to add an attribute to an entity with the type NSBooleanAttributeType, it gets a bit buggy. When I add it to my NSManagedObject subclass header file (in the tutorial, that would be Run.h) as
#property (retain) BOOL *booleanProperty;
compiling fails, saying error: property 'booleanProperty' with 'retain' attribute must be of object type.
It seems like some places in Cocoa use NSNumber objects to represent booleans, so I tried setting it to
#property (retain) NSNumber *booleanProperty;
instead. However, this evokes the following runtime errors:
*** -[NSAttributeDescription _setManagedObjectModel:]: unrecognized selector sent to instance 0x101b470
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSAttributeDescription _setManagedObjectModel:]: unrecognized selector sent to instance 0x101b470'
Using GDB, I am able to trace this back to the line in my source code where I add my entity to the managed object model:
[DVManagedObjectModel setEntities:[NSArray arrayWithObjects:myEntityWithABooleanAttribute, myOtherEntity]];
So my question is this: what type should I set booleanProperty to in my custom class header?
Try:
#property (nonatomic) BOOL booleanProperty;
The problem was that you used the retain in the property definition. For that you must have a property for an Objective-C class (it should be able to understand the 'retain' method). BOOL is not a class but an alias for signed char.
I wouldn't recommend the method suggested by Diederik Hoogenboom (i got an error even though my core data attribute was set as Boolean).
It's worth pointing out that although this line will work for a custom object, it will not work for a subclass of NSManagedObject:
#property (nonatomic) BOOL booleanProperty;
Your property should be set as this:
#property (nonatomic, retain) NSNumber *booleanProperty;
When i copy the method declarations for a Boolean type (using the technique suggested by Jim Correia), the getter and setter are typed as:
NSNumber:-(NSNumber *)booleanProperty;
-(void)setBooleanProperty:(NSNumber *)value;
...this is what a Boolean property in core data is set as, and you need to validate your property with something like this:
-(BOOL)validateBooleanProperty:(NSNumber **)toValidate error:(NSError **)outError
{
int toVal = [*toValidate intValue];
if ( (toVal < 0) || (toVal > 1) )
{
NSString *errorString = NSLocalizedStringFromTable(#"Boolean Property", #"TheObject", #"validation: not YES or NO");
NSDictionary *userInfoDict = [NSDictionary dictionaryWithObject:errorString forKey:NSLocalizedDescriptionKey];
NSError *error = [[[NSError alloc] initWithDomain:NSCocoaErrorDomain code:-1 userInfo:userInfoDict] autorelease];
*outError = error;
return NO;
}
return YES;
}//END
…remember to include the validateBooleanProperty declaration in the header file. The setter and getter methods store and retrieve your property using -(id)primitiveValueForKey:(NSString *)key.
Finally you need to explicitly call the validate method from whatever view controller / app delegate you're setting the object from:
NSNumber *boolProp = [[[NSNumber alloc] initWithInt :0] autorelease];
NSError *valError = nil;
if ([TheObject validateBooleanProperty:&boolProp error:&valError] == YES)
{
[TheObject setBooleanProperty :boolProp];
}
In the header,
#property (nonatomic, retain) NSNumber *booleanProperty;
In the implementation,
#dynamic booleanProperty;
To set it to true...
self.booleanProperty = [NSNumber numberWithBool:YES];
To set it to false...
self.booleanProperty = [NSNumber numberWithBool:NO];
To compare it to a literal true boolean:...
self.booleanProperty.boolValue == YES;
To compare it to a literal false boolean:...
self.booleanProperty.boolValue == NO;
For more information: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/nsnumber_Class/Reference/Reference.html#//apple_ref/occ/clm/NSNumber/numberWithBool:
One of the best ways to generate correct accessors in your NSManagedObject subclass is to bring up the contextual menu on a attribute or property in the data modeling tool and choose one of the following commands:
Copy Method Declarations to Clipboard
Copy Method Implementations to Clipboard
Copy Obj-C 2.0 Method Declarations to Clipboard
Copy Obj-C 2.0 Method Implementations to Clipboard
Let Xcode 4.0 decide for you.
In Xcode: select an Entity from your *.xcdatamodel file view.
Select Editor>Create NSMagedObject Subclass...
Xcode declares your Boolean objects as type NSNumber.
Edit: I'm curious what the motivation is for mitigating the xcdatamodel? Anyone?
An attribute of type Boolean in a NSManagedObject is of type NSCFBoolean. This is a private subclass of NSNumber.
I don't know if this is just a typo on your part, but this:
[NSArray arrayWithObjects:myEntityWithABooleanAttribute, myOtherEntity]
is definitely wrong. The last parameter of that method should always be nil.
Use NSNumber. There's no bool in the CoreData framework.