Best way to implement Enums with Core Data
Asked Answered
F

9

110

What is the best way to bind Core Data entities to enum values so that I am able to assign a type property to the entity? In other words, I have an entity called Item with an itemType property that I want to be bound to an enum, what is the best way of going about this.

Fractionize answered 26/10, 2009 at 11:28 Comment(0)
S
131

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"];
}
Seddon answered 26/10, 2009 at 11:45 Comment(3)
Thank you — too bad Core Data doesn't support this natively. I mean: Xcode generates class files, why not enums?Froemming
The last code is if you want to observe item itemTypeRaw. However, you can simply observe item itemType instead of itemTypeRaw right?Luanaluanda
With Xcode 4.5 you don't need any of this. Take a look at my answer. You just need to define the enum as an int16_t and you're set.Deangelo
D
80

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.

Deangelo answered 4/11, 2012 at 23:27 Comment(22)
Are there any limits with which iOS / OS X versions this will work with?Larrup
It’s been working for as long as I remember, but I can't make any promises. I remember using this on iOS 5, but I don't know why it wouldn't work with iOS 4...Deangelo
Note, this is a C++11 feature called "enum classes". cprogramming.com/c++11/…Accepted
No, this has nothing to do with C++11. It's part of clang 3.3 supporting Enumerations with a fixed underlying type for ObjC. C.f. clang.llvm.org/docs/…Deangelo
How do you avoid losing this code every time you regenerate the model class? I have been using Categories so that the core domain entities can be regenerated.Cowen
Since this doesn't have the retain keyword, will it still get stored in the database?Lajoie
The retain is related to memory management, not whether it gets stored into the database or not.Deangelo
I agree with Rob. I don't want this to have to be regenerated over and over again. I prefer the category.Viscus
Sure looks like C++ with all the underscoresAsperges
@Cowen To avoid the enum being overwritten every time the model class is regenerated, I wrote my typedef in a separate .h header file. So only two things need changing when the file is regenerated, the property type to the enum and re-including the header file with the enum. Not perfect, but the best I could come up with if following this pattern.Devolution
@Cowen Categories is a way to do it, but instead you could also use mogenerator: github.com/rentzsch/mogenerator. Mogenerator will generate 2 classes per entity, where one class will always be overwritten on data model changes and the other subclasses that class for custom stuff and never gets overwritten.Works
I'm getting a bad access with thisAsperges
How can you do this in Swift? If I am correct then this "trick" is not reproducible in Swift. See as well my question here: #27656719Laggard
Swift enum is something very, very different from a C / ObjC enum. I'm not sure if there's any good way to fudge it…Deangelo
I have been using this in my code but this throws me errors every time: CoreData: error: Property <> is a scalar type on class <> that does not match its Entity's property's scalar type. Dynamically generated accessors do not support implicit type coercion. Cannot generate a setter method for it. I just want to use the accepted answer after trying so much.Gerik
@Daniel Eggert , i try to use this but i am getting an error, Declaration of 'int16_t" must be imported from module . Can you help me on that. Many thanks in advanceAbject
In ObjC int16_t is already defined if you import Foundation or CoreData. Are you using Swift? You need to make sure that your scalar type in the entity definition matches the int16_t -> 16 bit.Deangelo
I was using this technique until I discovered a major problem that doing it this way prevents changedValues from containing the key.Asperges
@Gerik I've been having the same error , because i thought i was doing the same implementation, but there's one thing you and I didn't notice at first. "typedef enum Types_e : int16_t {" this line, you probably have something like that instead "typedef enum {"Onshore
Any chance you could edit Types_e and Types_t to something clearer? thanksAsperges
If you're getting BAD_ACCESS errors or similar, be sure that the proper type (i.e. int16_t) is set in your model file. If you're using Xcode to create your NSManagedObject subclass, make sure that the "use scalar properties for primitive data types" setting is checked.Trilbee
@Asperges is right! this seems to mess up CoreData change notificationsBroncho
M
22

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.

Milling answered 16/11, 2009 at 16:58 Comment(5)
Interesting. It definitely seems doable.Fractionize
brilliant idea! so much easier than creating tables in the db, unless your db is filled from a web service then its probably best to use a db table!Amalee
Here's an example: renovatioboy.wordpress.com/2011/10/06/…Corabella
I like it. I'm going to use this approach in my project. I like that I can also contain all my other meta information about the meta data within the NSNumber category. (i.e. linking strings to the enum values)Devolution
Really great idea! Very useful for associating string identifiers, using directly in JSON, Core Data, etc.Organelle
V
5

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.

Voluminous answered 30/8, 2014 at 17:17 Comment(0)
A
2

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
Asperges answered 25/7, 2013 at 17:37 Comment(0)
V
1

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];
Virgulate answered 24/1, 2012 at 16:37 Comment(0)
F
1

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.

Fairish answered 24/9, 2016 at 2:55 Comment(0)
C
0

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
Corabella answered 23/9, 2012 at 10:54 Comment(0)
S
0

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.

Seventy answered 12/7, 2019 at 8:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.