1. Introduction
Earlier in this series, we created Done, a simple application to learn more about the NSFetchedResultsController
class. In that project, we used key value coding (KVC) and key value observing (KVO) to create and update records. This works fine, but from the moment your project has any kind of complexity, you'll quickly run into issues. Not only is the KVC syntax verbose, valueForKey(_:)
and setValue(_:forKey:)
, it may also introduce errors that are the result of typos. The following code snippet illustrates this problem well.
record.setValue(NSDate(), forKey: "createdat") record.setValue(NSDate(), forKey: "CreatedAt") record.setValue(NSDate(), forKey: "createdAt") record.setValue(NSDate(), forKey: "CREATEDAT")
Each statement in the above code snippet returns a different result. In fact, every statement will result in an exception apart from the third statement, which uses the correct key as specified in the data model.
The above problem is easily solved by using string constants, but that's not the point I'm trying to make. Key value coding is great, but it is verbose and difficult to read if you're used to Swift's dot syntax. To make working with NSManagedObject
instances easier, it's better to create an NSManagedObject
subclass for each entity of the data model and that's what you'll learn in this article.
2. Subclassing NSManagedObject
To save some time, we're going to revisit Done, the application we created earlier in this series. Download it from GitHub and open it in Xcode.
Creating an NSManagedObject
subclass is very easy. While it's possible to manually create an NSManagedObject
subclass for an entity, it's easier to let Xcode do the work for you.
Open the data model of the project, Done.xcdatamodeld, and select the Item entity. Select New > File... from Xcode's File menu, choose the NSManagedObject subclass template from the Core Data section, and click Next.
Check the checkbox of the correct data model, Done, from the list of data models and click Next.
In the next step, you are asked to select the entities for which you want to create an NSManagedObject
subclass. Check the checkbox of the Item entity and click Next.
Choose a location to store the class files of the NSManagedObject
subclass and make sure that Use scalar properties for primitive data types is checked. When working with Objective-C, this option is an important consideration. But for Swift, it's best to check it as it makes your code more readable and less complex. Don't forget to set Language to Swift and click Create to create the NSManagedObject
subclass for the Item entity.
3. NSManagedObject
Anatomy
Navigate to the files Xcode created for you and take a look at their contents. Xcode should have create two files for you:
- Item.swift
- Item+CoreDataProperties.swift
If you're using an earlier version of Xcode, then Xcode may have created only one file. I recommend using Xcode 7—preferably Xcode 7.1 or higher—to follow along. The contents of Item.swift should be pretty easy to understand.
// // Item.swift // Done // // Created by Bart Jacobs on 24/10/15. // Copyright © 2015 Envato Tuts+. All rights reserved. // import Foundation import CoreData class Item: NSManagedObject { // Insert code here to add functionality to your managed object subclass }
As you can see, Xcode has created a class for us, Item
, that inherits from NSManagedObject
. The comment Xcode has added to the class implementation is important. If you wish to add functionality to the class, you should add it here. Let's now take a look at the contents of Item+CoreDataProperties.swift.
// // Item+CoreDataProperties.swift // Done // // Created by Bart Jacobs on 24/10/15. // Copyright © 2015 Envato Tuts+. All rights reserved. // // Choose "Create NSManagedObject Subclass…" from the Core Data editor menu // to delete and recreate this implementation file for your updated model. // import Foundation import CoreData extension Item { @NSManaged var createdAt: NSTimeInterval @NSManaged var done: Bool @NSManaged var name: String? }
There are a number of important details and a few new concepts. The first thing to notice is that this file defines an extension on the Item
class. The extension declares three properties that correspond to the attributes of the Item entity, which we defined in the data model. The @NSManaged
attribute is similar to the @dynamic
attribute in Objective-C. The @NSManaged
attribute tells the compiler that the storage and implementation of these properties will be provided at runtime. While this may sound great, Apple's documentation clearly states that @NSManaged
should only be used in the context of Core Data.
If this sounds a bit confusing, then remember that @NSManaged
is required for Core Data to do its work and the @NSManaged
attribute should only be used for NSManagedObject
subclasses.
What happens if you modify an entity and generate the files for the entity's NSManagedObject
subclass again? That's a good question and Xcode warns you about this scenario. At the top of Item+CoreDataProperties.swift, below the copyright statement, Xcode has added the a comment for clarification.
// Choose "Create NSManagedObject Subclass…" from the Core Data editor menu // to delete and recreate this implementation file for your updated model.
Whenever you generate the files for an entity, Xcode will only replace the files of the extension. In other words, if you add an attribute to the Item entity and generate the files for the NSManagedObject
subclass, Xcode replaces Item+CoreDataProperties.swift, leaving Item.swift untouched. That's why Xcode tells you to add functionality in Item.swift. This is very important to keep in mind.
The types of the properties may be a bit surprising. The name
property is of type NSString?
, because the attribute is marked as optional in the data model. The createdAt
property is of type NSTimeInterval
, because we checked the checkbox Use scalar properties for primitive data types. The same is true for the done
property, which is of type Bool
.
If we hadn't checked the checkbox, the createdAt
property would be of type NSDate?
and the done
property would be of type NSNumber?
. While it may be easier to use NSDate
instances, it's certainly less practical to work with NSNumber
objects if all you want is get and set boolean values.
4. Updating the Project
With the Item
class ready to use, it's time to update the project by replacing any occurrences of valueForKey(_:)
and setValue(_:forKey:)
.
ViewController
Open the implementation file of the ViewController
class, navigate to the configureCell(_:atIndexPath:)
, and update the implementation as shown below.
func configureCell(cell: ToDoCell, atIndexPath indexPath: NSIndexPath) { // Fetch Record let record = fetchedResultsController.objectAtIndexPath(indexPath) as! Item // Update Cell if let name = record.name { cell.nameLabel.text = name } cell.doneButton.selected = record.done cell.didTapButtonHandler = { record.done = !record.done } }
We've made four changes. We first changed the type of the record
variable to Item
. The objectAtIndexPath(_:)
method of the NSFetchedResultsController
class returns an AnyObject
instance. However, because we know the fetched results controller returns an Item
instance we force downcast the result using the as!
operator.
We also substitute the valueForKey(_:)
calls. Instead, we use the properties of the record
object, name
and done
. Thanks to Swift's dot syntax, the result is very legible.
To update the record, we no longer call setValue(_:forKey:)
. Instead, we use the dot syntax to set the record
's done
property. I'm sure you agree that this is much more elegant than using straight key value coding. We also don't need optional binding for the done
property since the done
property is of type Bool
.
Remember that KVC and KVO remain an integral part of Core Data. Core Data uses valueForKey(_:)
and setValue(_:forKey:)
under the hood to get the job done.
AddToDoViewController
We also need to make a few changes in the AddToDoViewController
class. In the save(_:)
method, we first need to update the initialization of the NSManagedObject
instance. Instead of initializing an NSManagedObject
instance, we create a Item
instance.
// Create Entity let entity = NSEntityDescription.entityForName("Item", inManagedObjectContext: self.managedObjectContext) // Initialize Record let record = Item(entity: entity!, insertIntoManagedObjectContext: self.managedObjectContext)
To populate the record, we use the dot syntax instead of the setValue(_:forKey:)
method as shown below.
// Populate Record record.name = name record.createdAt = NSDate().timeIntervalSince1970
UpdateToDoViewController
The last class we need to update is the UpdateToDoViewController
class. Let's start by changing the type of the record
property to Item!
.
import UIKit import CoreData class UpdateToDoViewController: UIViewController { @IBOutlet weak var textField: UITextField! var record: Item! var managedObjectContext: NSManagedObjectContext! ... }
This change will result in a warning in the ViewController
class. To see what's wrong, open ViewController.swift and navigate to the prepareForSegue(_:sender:)
method.
We ask the fetched results controller for the record at the selected index path. The type of the record
variable is NSManagedObject
, but the UpdateToDoViewController
class expects an Item
instance. The solution is very simple as you can see below.
if let indexPath = tableView.indexPathForSelectedRow { // Fetch Record let record = fetchedResultsController.objectAtIndexPath(indexPath) as! Item // Configure View Controller viewController.record = record viewController.managedObjectContext = managedObjectContext }
Head back to the UpdateToDoViewController
class and update the viewDidLoad()
method as shown below.
override func viewDidLoad() { super.viewDidLoad() if let name = record.name { textField.text = name } }
We also need to update the save(_:)
method, in which we replace setValue(_:forKey:)
with the dot syntax.
// Update Record record.name = name
Build the project and run the application in the simulator to see if everything is still working as expected.
5. Relationships
The current data model doesn't contain any relationships, but let's add a few to see how an NSManagedObject
subclass with relationships looks like. As we've seen in the previous article of this series, we first need to create a new version of the data model. Select the data model in the Project Navigator and choose Add Model Version... from the Editor menu. Set Version name to Done 2 and base the model on the current data model, Done. Click Finish to create the new data model version.
Open Done 2.xcdatamodel, create a new entity named User, and add an attribute name of type String. Add a relationship items and set the destination to Item. Leave the inverse relationship blank for now. With the items relationship selected, open the Data Model Inspector on the right and set the relationship type to To Many. A user can have more than one item associated with them.
Select the Item entity, create a relationship user, and set the destination to User. Set the inverse relationship to items. This will automatically set the inverse relationship of the items relationship of the User entity. Note that the user relationship is not optional.
Before we create the NSManagedObject
subclasses for both entities, we need to tell the data model which data model version it should use. Select Done.xcdatamodeld, open the File Inspector on the right, and set the current model version to Done 2. When you do this, double-check that you've selected Done.xcdatamodeld, not Done.xcdatamodel.
Select New > File... from the File menu and choose the NSManagedObject subclass template from the Core Data section. From the list of data models, select Done 2.
Select both entities from the list of entities. Because we've modified the Item entity, we need to regenerate the extension for the corresponding NSManagedObject
subclass.
Item
Let's first take a look at the changes of the newly generated Item
class. As you can see below, the extension on the Item
class (Item+CoreDataProperties.swift) contains one additional property, user
of type User?
.
import Foundation import CoreData extension Item { @NSManaged var createdAt: NSTimeInterval @NSManaged var done: Bool @NSManaged var name: String? @NSManaged var user: User? }
This is what a To One relationship looks like in an NSManagedObject
subclass. Xcode is smart enough to infer that the type of the user
property is User?
, the NSManagedObject
subclass we created a moment ago. Even though the user relationship is marked as not optional in the data model, the type of the user
property is User?
. It's not clear whether this is intentional or a bug in Xcode.
The implementation of the Item
class (Item.swift) hasn't been updated by Xcode. Remember that Xcode doesn't touch this file if it already exists.
User
With what we've learned so far, the User
class is easy to understand. Take a look at the extension of the User
class (User+CoreDataProperties.swift). The first thing you'll notice is that the type of the items
property is NSSet?
. This shouldn't be a surprise, because we already knew that Core Data uses sets, instances of the NSSet
class, for storing the members of a To Many relationship. Because the relationship is marked as optional in the data model, the type is NSSet?
.
import Foundation import CoreData extension User { @NSManaged var name: String? @NSManaged var items: NSSet? }
That's how easy it is to work with relationships in NSManagedObject
subclasses.
6. Migrations
If you build the project and run the application in the simulator, you'll notice that the application crashes. The output in Xcode's console tells us that the data model used to open the persistent store is not compatible with the one that was used to create it. The reason of this problem is explained in detail in the previous article of this series. If you want to learn more about migrations and how to safely modify the data model, then I suggest you read that article.
Conclusion
Subclassing NSManagedObject
is very common when working with Core Data. Not only does it add type safety, it also makes working with relationships much easier.
In the next installment of this series, we take a closer look at Core Data and concurrency. Concurrency is a tricky concept in almost any programming language, but knowing which pitfalls to avoid, makes it much less scary.
Comments