ylliX - Online Advertising Network

Using Core Data with Swift


Over the past month or so I’ve been diving into Swift, after many years of working with Objective-C on Macs and iOS. It’s been a change but, gradually, I’m learning the Swift way of doing things. On the way I’ve run into a few bumps in the road when dealing with Core Data, and I thought would be useful to share how I got past them.

Xcode Generated Subclasses Considered Harmful

This is the main impetus for this post. Most other stuff I would expect people to work around eventually, but this one is kind of big.

Xcode’s generated NSManagedObject subclasses are limited but useful in their own way. If you don’t need much, they’ll do. Everyone else would use mogenerator. That’s if you’re using Objective-C, though. With Swift there’s a decent chance that using Xcode’s subclasses will actually crash your app.

Both Core Data and Swift include the concept of an optional value. But when Xcode generates subclasses, it doesn’t consider whether Core Data thinks the value is optional when generating the Swift file. It generates non-optional properties every time. This conflict means that you end up a Swift non-optional property that could reasonably have a nil value. The compiler sees that the property is not optional, so it doesn’t mind that you aren’t checking this (in fact, Swift rules mean it’s not possible to check, because the property is not optional). This is exactly the kind of problem Swift’s compiler is supposed to catch for you, but here it can’t.

For example, suppose you configure a Core Data attribute like this:

Since summary is optional, it’s allowed to have a nil value. But if you have Xcode generate a subclass for this entity, you get

    @NSManaged var summary: String

…which is not optional, and which the Swift compiler will therefore assume to have non-nil values. But the @NSManaged prevents it from complaining that you aren’t initializing the property.

If you write code that treats summary as non-optional, the compiler won’t mind. But if you try to access summary when it’s nil, your app will crash.

This is a trivial fix, just make the property optional. Or even better, an implicitly unwrapped optional:

    @NSManaged var summary: String!

But people who use Xcode’s subclass code could reasonably assume that doing so is at least safe, even if maybe it could do more than it does.

If you’re using mogenerator, you’re covered for Core Data optionals. It makes sure Core Data optionals become Swift optionals. I’d take it a step farther and make all properties optional in Swift even if they’re required by Core Data. Core Data doesn’t enforce the “required” rule until you save changes, so even non-optional attributes can legally be nil at almost any time.

rdar://20153926

What is this thing… called @NSManaged?

Generated subclass attributes have an @NSManaged decoration, but what does that mean? Apple says

Apply this attribute to a stored variable property of a class that inherits from NSManagedObject to indicate that the storage and implementation of the property are provided dynamically by Core Data at runtime based on the associated entity description.

For Objective-C developers that means it’s taking the place of @dynamic. In both languages, Core Data will do its internal magic to ensure that Core Data style accessor methods exist at run time even though they’re not defined in your code.

From a Swift perspective, @NSManaged also tells the compiler that it doesn’t need to care whether the property has a value before initialization is complete, even if the property is not a Swift optional. It’s like declaring that this is a computed property with accessors to be provided later. However as the discussion above illustrates, this can be a really bad idea for non-optional attributes.

Regardless though, you do need @NSManaged unless you’re providing custom accessor methods.

Getters and Setters

But what about when you need a custom getter or setter method for a Core Data attribute? Swift has an elegant way of linking accessor methods directly to the corresponding property declaration. The aptly named get and set blocks are just what you need. Except… if you add these blocks, the compiler will warn you that 'NSManaged' [is] not allowed on computed properties. How now, since this is a Core Data attribute?

Although the documentation could be read as meaning that @NSManaged is required, it actually isn’t. It’s only needed if you’ll be letting Core Data handle the accessors. If you’re providing your own, drop it. Core Data’s accessor magic is not documented but it seems you can’t just override it like you’d override other methods.

A minor annoyance is that since you now have a computed read/write property, you can’t provide custom setter behavior unless you also provide a custom getter. Even if you don’t need to change the default getter behavior, you need to provide the code.

Put it all together and the declaration and custom accessors for a property named text might look like this:

    public var text: String?
        {
        set {
            self.willChangeValueForKey("text")
            self.setPrimitiveValue(newValue, forKey: "text")
            self.didChangeValueForKey("text")
            
            self.dateModified = NSDate()
        }
        get {
            self.willAccessValueForKey("text")
            let text = self.primitiveValueForKey("text") as? String
            self.didAccessValueForKey("text")
            return text
        }
    }

[Thanks to Casey Fleser for pointing out that the initial version of the code above left out calls to willAccessValueForKey and didAccessValueForKey.]

An array of WAT?

When fetching data, the return type of executeFetchRequest:error: is [AnyObject]?. That makes some sense since the only guaranteed detail is that the result is either nil or an array of something. The actual array contents depend on how your fetch is configured. You’ll make things easier on yourself if you downcast the result to something more specific. What you need depends on the result type you’re asking for:

  • NSManagedObjectResultType (the default fetch type, a.k.a. “normal” fetching): if you have a custom class for your entity, you’ll get an array of that, so downcast to [MyClassName]?. If you don’t (or don’t know for some reason) use [NSManagedObject]?.
  • NSManagedObjectIDResultType: Use [NSManagedObjectID]?.
  • NSDictionaryResultType: This returns an array of dictionaries, each of which has String keys and AnyObject values. Use [[String : AnyObject]]?. The value types match up with the Core Data attribute type. You’ll need to know those types is to use the values, and downcast to String?, Int?, etc.
  • NSCountResultType: I’ve never seen this used, since countForFetchRequest:error: exists. But for the sake of completeness, use [Int]? here.

Let’s Be Careful Out There

Like I said earlier, Xcode’s generated code was the main reason for writing this. If you have Xcode generate your subclasses, look carefully at what it gives you. If you don’t want to worry about that (and want better subclasses overall), use mogenerator.





Source link

Leave a Reply

Your email address will not be published. Required fields are marked *