Using Core Data in Swift

Using Core Data in Swift

Core Data brings a bit of Objective-C legacy into Swift world making our work with managed objects not as safely typed as we wish it to be.

Making a better use of scalar types

Xcode provides us with two ways of dealing with the following group of primitive values. We can use them as NSNumber?:

class ManagedObject: NSManagedObject {

    @NSManaged public var double: NSNumber?
    @NSManaged public var float: NSNumber?
    @NSManaged public var boolean: NSNumber?
    @NSManaged public var integer16: NSNumber?
    @NSManaged public var integer32: NSNumber?
    @NSManaged public var integer64: NSNumber?
}

or scalar types:

class ManagedObject: NSManagedObject {

    @NSManaged public var double: Double
    @NSManaged public var float: Float
    @NSManaged public var bool: Bool
    @NSManaged public var integer16: Int16
    @NSManaged public var integer32: Int32
    @NSManaged public var integer64: Int64
}

The latter is more convenient to use in Swift, however it is not ideal:

  • attributes cannot be declared as optionals - which makes perfect sense in some cases
  • attributes will return zero before they are assigned another value - this may be unexpected sometimes

Both issues can be resolved by introducing two layers of properties, private for Core Data use, and public (or internal) providing convenient API.

class Movie: NSManagedObject {

    @NSManaged private var cd_userRating: NSNumber?
    @NSManaged private var cd_duration: NSNumber?
}

extension Movie {

    // nil represents no rating at all
    var userRating: Double? {

        get { return cd_userRating?.doubleValue }
        set { cd_userRating = userRating.map { NSNumber(value:$0) }}
    }

    // movie duration in minutes
    var duration: Int32 {

        get { return cd_duration!.int32Value }
        set { cd_duration = NSNumber(value:length) }
    }
}

Please note that the second layer is more descriptive about the data model than the first one.

Strongly typed relationships

Auto-generated relationships have a few inconveniences as well:

class ManagedObject: NSManagedObject {

    @NSManaged public var relationToOne: ManagedObject?
    @NSManaged public var relationToMany: NSSet?
}
  • to many relationships are represented by NSSet? instead of Set<T> - strongly typed API fits nicer into Swift
  • to many relationships return nil instead of empty set if there are no related objects - having empty set instead of optional one yields cleaner code
  • to one relationships are optional by default - this may have no sense in some cases

We can apply similar pattern to deal with them:

class Folder: NSManagedObject {

    @NSManaged private var cd_owner: User?
    @NSManaged private var cd_files: NSSet?
}

extension Folder {

    var owner: User {

        get { return cd_owner! }
        set { cd_owner = owner }
    }
    
    var files: Set<File> {

        get { return (cd_files ?? []) as! Set<File> }
        set { cd_files = files as NSSet }
    }
}

Readonly properties

After improving typing for attributes and relationships we can take one step further and make some of them readonly. In that case we may have to provide initializer for assigning initial values.

class Message: NSManagedObject {

    @NSManaged private var cd_sender: Sender?
}

extension Message {

    var sender: Sender { return cd_sender! }
}

extension Message {

    convenience init(context: NSManagedObjectContext, sender: Sender) {

        self.init(context: context)
        cd_sender = sender
    }
}

The foregoing code listing uses init(context moc: NSManagedObjectContext) introduced in iOS 10. If we need to support older systems we can use the following implementation.

extension Message {

    static func makeInstance(sender: Sender, in context: NSManagedObjectContext) -> Message {

        let message = Message.makeInstance(in: context)
        message.cd_sender = sender
        return message
    }
}
@objc protocol NSManagedObjectConveniences {}

extension NSManagedObjectConveniences {

    static func makeInstance(in context: NSManagedObjectContext) -> Self {
        
        let entityName = String(describing: Self.self)
        let entityDescription = NSEntityDescription.entity(forEntityName: entityName, in: context)!
        let managedObject = NSManagedObject(entity: entityDescription, insertInto: context) as! Self
        return managedObject
    }
}

extension NSManagedObject: NSManagedObjectConveniences {}