Skip to content

Creating Core Data Entities

Micrified edited this page Feb 28, 2018 · 2 revisions

This guide outlines how data model entities are created in the Summer and Winter Schools iOS App.

Prelude

Core Data is the persistent storage solution for this iOS application. Xcode projects supply a .xcdatamodeld file with a nice editor for managing Core Data entities and relationships. Within our Xcode project, the database model is named SSDataModel.xcdatamodeld. This guide will show you the workflow for interacting with the database in this application. It will be broken down into two relatively simple steps.

  1. Creating an Entity.
  2. Loading and Saving Entities

Creating an Entity.

Say you've been tasked with providing persistent storage to an announcement object. An announcement has the following attributes:

  1. id: An identifier associated with this object in the remote resource database.
  2. schoolId: The identifier of the school to which the announcement was addressed.
  3. title: The title of an announcement.
  4. description: The body of the announcement.
  5. date: The date of an announcement.

We will first create an entity to represent this object in storage. Navigate to the SSDataModel.xcdatamodeld file and click on it. You should see either a graph with grid-backdrop, or a table-like screen that resembles a plist editor. You can toggle between these two views in the lower right corner by selecting the "Editor Style" tab-button. For our purposes, we want the table-like screen (shown below).

  1. In order to add an entity, select the button with the "1" label in the attached diagram. This should create a new entity with no attributes. Rename it to "AnnouncementEntity" as shown above.

  2. To add attributes, select the button labeled with "2" in the diagram. This should allow you to create a new attribute with a name and type. For strings and dates, select the "String" type. It's easy to work with strings than date objects. So dates are always serialized to string form before being archived.

If any attributes are non-optional, be sure to uncheck the optional field as indicated.

This concludes the creation of simple entities. Some more complex entities can have to-one or to-many relations. This can allow you to grab related entities in a convenient way (I.E: Comment entities associated with a Thread entity). However, this is out of scope (comments aren't archived in this application) and won't be covered.

Loading and Saving Entities

The DataManager.swift singleton is the Apps central location for all persistent data management. It contains the NSManagedObjectContext and provides the means to access the Core Data database. However, all data loading and saving for a specific object/class is done within it's own data model.

Loading

If you're following the proper workflow for adding new content to this application. You'll have created a data model class that conforms to the RGSDataModelDelegate protocol. This only means that you promise your data model will contain certain loading methods. In much the same manner, you should have implemented the standard loadDataModel class method. It will be below here for reference:

    /// Retrieves all model entities from Core Data, and returns them in an array
    /// sorted using the provided sort method.
    /// - context:  The managed object context.
    /// - sort:     The mandatory sorting method.
    static func loadDataModel (context: NSManagedObjectContext, sort: (RGSAnnouncementDataModel, RGSAnnouncementDataModel) -> Bool) -> [RGSAnnouncementDataModel]? {
        let entityKey = RGSAnnouncementDataModel.entityKey
        var entities: [NSManagedObject]
        
        // Construct request, extract entities.
        do {
            let request = NSFetchRequest<NSFetchRequestResult>(entityName: entityKey["entityName"]!)
            entities = try context.fetch(request) as! [NSManagedObject]
        } catch {
            print("Error: loadDataModel: Couldn't extract announcement data!")
            return nil
        }
        
        // Convert entities to models.
        let models = entities.map({(object: NSManagedObject) -> RGSAnnouncementDataModel in
            return RGSAnnouncementDataModel(from: object)!
        })
        
        // Return sorted models.
        return models.sorted(by: sort)
    }

Loading the entity requires that you've defined your entity key (a dictionary mapping your own choice for key names to those actually used in the Entity model you made earlier (more specifically, it maps a name to an attribute name). The reason this mapping exists at all is because there are some names you can't use as entity.

The entity key for this model should look as follows:

    /// The model entity key.
    static var entityKey: [String : String] = [
        "entityName"    : "AnnouncementEntity",
        "title"         : "title",
        "description"   : "announcementDescription",
        "dateString"    : "dateString",
        "schoolId"      : "schoolId",
        "id"            : "id"
    ]

Notice that in the loading method, we provide the NSFetchRequest with entityKey["entityName"]! as the entity name. This maps to "AnnouncementEntity", which is exactly what we named our entity in SSDataModel.xcdatamodeld. The same use of the entityKey should follow in your data model's required init(from managedObject: NSManagedObject) method:

    /// Initializes the data model from NSManagedObject.
    /// - managedObject: NSManagedObject instance.
    required init? (from managedObject: NSManagedObject) {
        let entityKey = RGSAnnouncementDataModel.entityKey
        
        // Mandatory fields.
        guard
            let id          = managedObject.value(forKey: entityKey["id"]!) as? String,
            let title       = managedObject.value(forKey: entityKey["title"]!) as? String,
            let description = managedObject.value(forKey: entityKey["description"]!) as? String,
            let schoolId    = managedObject.value(forKey: entityKey["schoolId"]!) as? String,
            let dateString  = managedObject.value(forKey: entityKey["dateString"]!) as? String
        else { return nil }
        
        self.id             = id
        self.title          = title
        self.description    = description
        self.schoolId       = schoolId
        self.date           = DateManager.sharedInstance.ISOStringToDate(dateString, format: .JSONGeneralDateFormat)
    }

And that is pretty much all there is to loading. When saving an entity, things get a bit tricker.

Saving

When saving an entity, you should begin by implementing the mandatory saveTo(managedObject: NSManagedObject) method required by the protocol for your data model class. An example is as follows:

    /// Saves all fields to the given NSManagedObject.
    /// - managedObject: The NSManagedObject representation.
    func saveTo (managedObject: NSManagedObject) {
        let entityKey = RGSAnnouncementDataModel.entityKey
        managedObject.setValue(title, forKey: entityKey["title"]!)
        managedObject.setValue(description, forKey: entityKey["description"]!)
        managedObject.setValue(schoolId, forKey: entityKey["schoolId"]!)
        managedObject.setValue(id, forKey: entityKey["id"]!)
        let dateString = DateManager.sharedInstance.dateToISOString(date, format: .JSONGeneralDateFormat)
        managedObject.setValue(dateString, forKey: entityKey["dateString"]!)
    }

Here we use the setValue method of the NSManagedObject class to save attributes of our model to an entity. When serializing dates, you can see that I first use the DataManager.swift singleton to convert them to a suitable ISO string format.

Finally, we write a saveDataModel (_ model: [<T>], context: NSManagedObjectContext) class method within our data model to handle the fetching and individual loading of all our announcement entities from the database.

    /// Saves all given model representations in Core Data. All existing entries are
    /// removed prior.
    /// - model:    The array of data models to be archived.
    /// - context:  The managed object context.
    static func saveDataModel (_ model: [RGSAnnouncementDataModel], context: NSManagedObjectContext) {
        let entityKey = RGSAnnouncementDataModel.entityKey
        var entities: [NSManagedObject]
        
        // Extract all existing entities.
        do {
            let request = NSFetchRequest<NSFetchRequestResult>(entityName: entityKey["entityName"]!)
            entities = try context.fetch(request) as! [NSManagedObject]
        } catch {
            print("Error: saveDataModel: Couldn't extract announcement data!")
            return
        }
        
        // Delete all existing entities.
        for entity in entities {
            let objectContext = entity.managedObjectContext!
            objectContext.delete(entity)
        }
        
        // Insert new entities.
        for object in model {
            let entity = NSEntityDescription.insertNewObject(forEntityName: entityKey["entityName"]!, into: context) as NSManagedObject
            object.saveTo(managedObject: entity)
        }
        
        // Save context.
        DataManager.sharedInstance.saveContext()
    }

The saving process can be best described in three steps.

  1. Perform a fetch request for all entities matching the name of our announcement entity.
  2. Delete all extracted entries from the database (we're about to save the latest data to it).
  3. Save all given announcement instances and insert them in the database.

When all of this is done, we ask the DataManager singleton to save our changes.