Background
I, like scores of programmers before me, am working on an application that deals with money. I'm relatively new to Cocoa programming, but after reading through the manuals I decided that I would try to use Core Data because it provides a number of features that I want and should save me from re-inventing the wheel. Anyway, my question doesn't have anything to do with whether or not I should use Core Data: it has to do with the behavior of Core Data and XCode themselves.
UPDATE: I filed a bug report with Apple and was informed that it is a duplicate of problem ID 9405079. They are aware of the issue, but I have no idea when or if they are going to fix it.
The Problem
For some reason that I cannot understand, XCode floors the Min Value and Max Value constraints when I edit a Decimal property in my managed object model. (I'm using Decimal properties for the reasons described here.)
Assume that I have a Core Data entity with a Decimal attribute named value
(this is merely for illustration; I've used other attribute names, as well). I want it to have a value greater than 0, but because XCode will only allow me to specify a minimum value (inclusive), I set Min Value equal to 0.01
. Much to my surprise, this results in a validation predicate of SELF >= 0
! I get the same result when I change the minimum value: all fractional values are truncated (the minimum value is floored). The maximum value has the same behavior.
By way of illustration, the value
property in the following screenshot will result in validation predicates of SELF >= 0
and SELF <= 1
.
Strangely enough, though, if I change the type of this property to either Double or Float, the validation predicates will change to SELF >= 0.5
and SELF <= 1.2
, as expected. Stranger still, if I create my own data model following the Core Data Utility Tutorial, the validation predicates are set correctly even for decimal properties.
Original Workaround
Since I can't find any way to fix this problem in XCode's managed object model editor, I have added the following code—indicated by the begin workaround
and end workaround
comments—to my application delegate's managedObjectModel
method (this is the same application delegate that XCode provides by default when you create a new project that uses Core Data). Note that I am adding a constraint to keep the Transaction
entity's amount
property greater than 0.
- (NSManagedObjectModel *)managedObjectModel {
if (managedObjectModel) return managedObjectModel;
managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];
// begin workaround
NSEntityDescription *transactionEntity = [[managedObjectModel entitiesByName] objectForKey:@"Transaction"];
NSAttributeDescription *amountAttribute = [[transactionEntity attributesByName] objectForKey:@"amount"];
[amountAttribute setValidationPredicates:[NSArray arrayWithObject:[NSPredicate predicateWithFormat:@"SELF > 0"]]
withValidationWarnings:[NSArray arrayWithObject:@"amount is not greater than 0"]];
// end workaround
return managedObjectModel;
}
Questions
- Is this really a bug in how XCode generates validation predicates for decimal properties in managed object models for Core Data?
- If so, is there a better way to work around it than the ones I have described here?
Repro Code
You should be able to reproduce this issue with the following sample code for a DebugController
class, which prints out the constraints on every property in a managed object model to a label. This code makes the following assumptions.
- You have an application delegate named
DecimalTest_AppDelegate
- Your application delegate has a
managedObjectContext
method - Your managed object model is named "Wallet"
Take the following steps to use this code.
- Instantiate the
DebugController
in Interface Builder. - Connect the controller's
appDelegate
outlet to your application delegate. - Add a wrapping label (
NSTextField
) to your user interface and connect the controller'sdebugLabel
outlet to it. - Add a button to your user interface and connect its selector to the controller's
updateLabel
action. - Launch your application and press the button connected to the
updateLabel
action. This prints your managed object model's constraints todebugLabel
and should illustrate the behavior that I've described here.
DebugController.h
#import <Cocoa/Cocoa.h>
// TODO: Replace 'DecimalTest_AppDelegate' with the name of your application delegate
#import "DecimalTest_AppDelegate.h"
@interface DebugController : NSObject {
NSManagedObjectContext *context;
// TODO: Replace 'DecimalTest_AppDelegate' with the name of your application delegate
IBOutlet DecimalTest_AppDelegate *appDelegate;
IBOutlet NSTextField *debugLabel;
}
@property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext;
- (IBAction)updateLabel:sender;
@end
DebugController.m
#import "DebugController.h"
@implementation DebugController
- (NSManagedObjectContext *)managedObjectContext
{
if (context == nil)
{
context = [[NSManagedObjectContext alloc] init];
[context setPersistentStoreCoordinator:[[appDelegate managedObjectContext] persistentStoreCoordinator]];
}
return context;
}
- (IBAction)updateLabel:sender
{
NSString *debugString = @"";
// TODO: Replace 'Wallet' with the name of your managed object model
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Wallet" inManagedObjectContext:[self managedObjectContext]];
NSArray *properties = [entity properties];
for (NSAttributeDescription *attribute in properties)
{
debugString = [debugString stringByAppendingFormat:@"\n%@: \n", [attribute name]];
NSArray *validationPredicates = [attribute validationPredicates];
for (NSPredicate *predicate in validationPredicates)
{
debugString = [debugString stringByAppendingFormat:@"%@\n", [predicate predicateFormat]];
}
}
// NSPredicate *validationPredicate = [validationPredicates objectAtIndex:1];
[debugLabel setStringValue:debugString];
}
@end
Thanks everyone.