使用核心数据实现枚举的最佳方法

将核心数据实体绑定到枚举值,以便能够为实体分配类型属性的最佳方法是什么?换句话说,我有一个名为 Item的实体,它有一个 itemType属性,我希望将它绑定到一个枚举,解决这个问题的最佳方法是什么。

21877 次浏览

You'll have to create custom accessors if you want to restrict the values to an enum. So, first you'd declare an enum, like so:

typedef enum {
kPaymentFrequencyOneOff = 0,
kPaymentFrequencyYearly = 1,
kPaymentFrequencyMonthly = 2,
kPaymentFrequencyWeekly = 3
} PaymentFrequency;

Then, declare getters and setters for your property. It's a bad idea to override the existing ones, since the standard accessors expect an NSNumber object rather than a scalar type, and you'll run into trouble if anything in the bindings or KVO systems try and access your value.

- (PaymentFrequency)itemTypeRaw {
return (PaymentFrequency)[[self itemType] intValue];
}


- (void)setItemTypeRaw:(PaymentFrequency)type {
[self setItemType:[NSNumber numberWithInt:type]];
}

Finally, you should implement + keyPathsForValuesAffecting<Key> so you get KVO notifications for itemTypeRaw when itemType changes.

+ (NSSet *)keyPathsForValuesAffectingItemTypeRaw {
return [NSSet setWithObject:@"itemType"];
}

An alternative approach I'm considering is not to declare an enum at all, but to instead declare the values as category methods on NSNumber.

Since enums are backed by a standard short you could also not use the NSNumber wrapper and set the property directly as a scalar value. Make sure to set the data type in the core data model as "Integer 32".

MyEntity.h

typedef enum {
kEnumThing, /* 0 is implied */
kEnumWidget, /* 1 is implied */
} MyThingAMaBobs;


@interface myEntity : NSManagedObject


@property (nonatomic) int32_t coreDataEnumStorage;

Elsewhere in code

myEntityInstance.coreDataEnumStorage = kEnumThing;

Or parsing from a JSON string or loading from a file

myEntityInstance.coreDataEnumStorage = [myStringOfAnInteger intValue];

The code pasted below works for me, and I've added it as full working example. I'd like to hear opinions on this approach, as I plan to used it extensively throughout my apps.

  • I've left the @dynamic in place, as it is then satisfied by the getter/setter named in the property.

  • As per the answer by iKenndac, I have not overridden the default getter/setter names.

  • I've included some range checking via a NSAssert on the typedef valid values.

  • I've also added a method to obtain a string value for the given typedef.

  • I prefix constants with "c" rather than "k". I know the reasoning behind "k" (math origins, historical), but it feels like I am reading ESL code with it, so I use "c". Just a personal thing.

There is a similar question here: typedef as a Core data type

I'd appreciate any input on this approach.

Word.h


#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>


typedef enum {
cPresent            = 0,
cFuturProche        = 1,
cPasseCompose       = 2,
cImparfait          = 3,
cFuturSimple        = 4,
cImperatif          = 5
} TenseTypeEnum;


@class Word;
@interface Word : NSManagedObject


@property (nonatomic, retain) NSString * word;
@property (nonatomic, getter = tenseRaw, setter = setTenseRaw:) TenseTypeEnum tense;


// custom getter & setter methods
-(void)setTenseRaw:(TenseTypeEnum)newValue;
-(TenseTypeEnum)tenseRaw;
- (NSString *)textForTenseType:(TenseTypeEnum)tenseType;


@end




Word.m




#import "Word.h"


@implementation Word


@dynamic word;
@dynamic tense;


// custom getter & setter methods
-(void)setTenseRaw:(TenseTypeEnum)newValue
{
NSNumber *numberValue = [NSNumber numberWithInt:newValue];
[self willChangeValueForKey:@"tense"];
[self setPrimitiveValue:numberValue forKey:@"tense"];
[self didChangeValueForKey:@"tense"];
}




-(TenseTypeEnum)tenseRaw
{
[self willAccessValueForKey:@"tense"];
NSNumber *numberValue = [self primitiveValueForKey:@"tense"];
[self didAccessValueForKey:@"tense"];
int intValue = [numberValue intValue];


NSAssert(intValue >= 0 && intValue <= 5, @"unsupported tense type");
return (TenseTypeEnum) intValue;
}




- (NSString *)textForTenseType:(TenseTypeEnum)tenseType
{
NSString *tenseText = [[NSString alloc] init];


switch(tenseType){
case cPresent:
tenseText = @"présent";
break;
case cFuturProche:
tenseText = @"futur proche";
break;
case cPasseCompose:
tenseText = @"passé composé";
break;
case cImparfait:
tenseText = @"imparfait";
break;
case cFuturSimple:
tenseText = @"futur simple";
break;
case cImperatif:
tenseText = @"impératif";
break;
}
return tenseText;
}




@end

You can do this way, way simpler:

typedef enum Types_e : int16_t {
TypeA = 0,
TypeB = 1,
} Types_t;


@property (nonatomic) Types_t itemType;

And in your model, set itemType to be a 16 bit number. All done. No additional code needed. Just put in your usual

@dynamic itemType;

If you're using Xcode to create your NSManagedObject subclass, make sure that the "use scalar properties for primitive data types" setting is checked.

I set the attribute type as 16 bit integer then use this:

#import <CoreData/CoreData.h>


enum {
LDDirtyTypeRecord = 0,
LDDirtyTypeAttachment
};
typedef int16_t LDDirtyType;


enum {
LDDirtyActionInsert = 0,
LDDirtyActionDelete
};
typedef int16_t LDDirtyAction;




@interface LDDirty : NSManagedObject


@property (nonatomic, strong) NSString* identifier;
@property (nonatomic) LDDirtyType type;
@property (nonatomic) LDDirtyAction action;


@end

...

#import "LDDirty.h"


@implementation LDDirty


@dynamic identifier;
@dynamic type;
@dynamic action;


@end

If you're using mogenerator, have a look at this: https://github.com/rentzsch/mogenerator/wiki/Using-enums-as-types. You can have an Integer 16 attribute called itemType, with a attributeValueScalarType value of Item in the user info. Then, in the user info for your entity, set additionalHeaderFileName to the name of the header that the Item enum is defined in. When generating your header files, mogenerator will automatically make the property have the Item type.

I have done this a lot and find the following form to be useful:

// accountType
public var account:AccountType {
get {
willAccessValueForKey(Field.Account.rawValue)
defer { didAccessValueForKey(Field.Account.rawValue) }
return primitiveAccountType.flatMap { AccountType(rawValue: $0) } ?? .New }
set {
willChangeValueForKey(Field.Account.rawValue)
defer { didChangeValueForKey(Field.Account.rawValue) }
primitiveAccountType = newValue.rawValue }}
@NSManaged private var primitiveAccountType: String?

In this case, the enum is pretty simple:

public enum AccountType: String {
case New = "new"
case Registered = "full"
}

and call it pedantic, but I use enums for field names, like this:

public enum Field:String {


case Account = "account"
}

Since this can get laborious for complex data models, I wrote a code generator that consumes the MOM / entities to spit out all the mappings. My inputs end up being a dictionary from Table/Row to Enum type. While I was at it, I also generated JSON serialization code. I've done this for very complex models and it has turned out to be a big time saver.

Solution for Auto Generated Classes

from Xcode's Code Generator (ios 10 and above)

If you create an Entity named "YourClass", Xcode automatically will choose "Class Definition" as default a Codegen type at "Data Model Inspector". this will generate classes below:

Swift version:

// YourClass+CoreDataClass.swift
@objc(YourClass)
public class YourClass: NSManagedObject {
}

Objective-C version:

// YourClass+CoreDataClass.h
@interface YourClass : NSManagedObject
@end


#import "YourClass+CoreDataProperties.h"


// YourClass+CoreDataClass.m
#import "YourClass+CoreDataClass.h"
@implementation YourClass
@end

We'll choose "Category/Extension" from Codegen option instead of "Class Definition" in Xcode.

Now, If we want to add an enum, go and create another extension for your auto-generated class, and add your enum definitions here like below:

// YourClass+Extension.h


#import "YourClass+CoreDataClass.h" // That was the trick for me!


@interface YourClass (Extension)


@end




// YourClass+Extension.m


#import "YourClass+Extension.h"


@implementation YourClass (Extension)


typedef NS_ENUM(int16_t, YourEnumType) {
YourEnumTypeStarted,
YourEnumTypeDone,
YourEnumTypePaused,
YourEnumTypeInternetConnectionError,
YourEnumTypeFailed
};


@end

Now, you can create custom accessors if you want to restrict the values to an enum. Please check the accepted answer by question owner. Or you can convert your enums while you set them with explicitly conversion method using the cast operator like below:

model.yourEnumProperty = (int16_t)YourEnumTypeStarted;

Also check

Xcode automatic subclass generation

Xcode now supports automatic generation of NSManagedObject subclasses in the modeling tool. In the entity inspector:

Manual/None is the default, and previous behavior; in this case, you should implement your own subclass or use NSManagedObject. Category/Extension generates a class extension in a file named like ClassName+CoreDataGeneratedProperties. You need to declare/implement the main class (if in Obj-C, via a header the extension can import named ClassName.h). Class Definition generates subclass files named like ClassName+CoreDataClass as well as the files generated for Category/Extension. The generated files are placed in DerivedData and rebuilt on the first build after the model is saved. They are also indexed by Xcode, so command-clicking on references and fast-opening by filename works.