As I continue my work with Core Data and Swift, I have been trying to find ways to make Core Data better. Among my goals are clarity and safety, specifically regarding types. Luckily, we can harness Swift’s optionals, enums, and other features to make managed objects more robust and more clear. But even with the improvements that Swift brings, there are still some drawbacks and limitations with Xcode’s current toolset.
A great case for optionals
There’s been plenty of feedback in the community about optionals and how they are often cumbersome to use. However, this relatively simple construct brings a welcoming clarity to managed objects when working with Core Data. When defining entities in Core Data, it is possible to set some basic validation rules for an entity’s attributes in the Data Model Inspector.
You can define minimum and maximum values, provide default values, mark attributes as optional, and more. This is nothing new. But in Objective-C, the optionality of a property on a managed object could only be discovered by opening the .xcdatamodeld
file in Xcode, then selecting the entity, then selecting the attribute, and then opening the Data Model Inspector in the sidebar. Or, at runtime you find out that your NSManagedObjectContext
fails to save because of Cocoa error 1570
. Neither of these experiences are enjoyable.
For example, imagine we have the following Employee
class. What fields are required for a save to succeed?
@interface Employee : NSManagedObject
@property (nonatomic, retain) NSString * address;
@property (nonatomic, retain) NSDate * dateOfBirth;
@property (nonatomic, retain) NSString * email;
@property (nonatomic, retain) NSString * firstName;
@property (nonatomic, retain) NSString * lastName;
@property (nonatomic, retain) NSString * middleName;
@property (nonatomic, retain) NSDecimalNumber * salary;
@property (nonatomic, retain) NSNumber * status;
@end
By contrast, when using Swift we immediately know what properties are optional simply by looking at the code.
class Employee: NSManagedObject {
@NSManaged var address: String?
@NSManaged var dateOfBirth: NSDate
@NSManaged var email: String?
@NSManaged var firstName: String
@NSManaged var lastName: String
@NSManaged var middleName: String?
@NSManaged var salary: NSDecimalNumber
@NSManaged var status: Int32
}
Note: Xcode does not generate Swift classes accurately when they have optional attributes. You must manually add the ?
for optional values. Further, though The Swift Programming Language guide recommends using Int
for all integer variables, Core Data recommends using specific integer sizes and complains if you attempt to do otherwise, thus the use of Int32
for the status
property.
This seems like a minor detail, but it’s a huge win — especially when you begin to work with your models throughout the rest of your app. As with optionals in general, you will need to explicitly handle nil
(which I think is a positive side effect). But even with the pyramid of doom behind us, this may not be pleasant if you have a lot of optionals. If this is the case, hopefully it will encourage you to reconsider your design. Is this field really necessary? Could this property be derived from other data? Should this property be required instead? These are questions one should always be asking when designing model classes, but perhaps the leniency of Objective-C allowed them to be dismissed before. Optionals complicate your model, which is a great motivation to use as few as possible and keep things simple.
Given this, let’s review our Employee
class. Is middleName
important? No, let’s remove it. Suppose that we know that all employees have an email address with the form: <firstName><LastName>@<companyName>.com
. Do we really need to store it? No, we can write a function or computed property to generate that. Finally, let’s assume that employees should be required to have an address
. Ahh, this is looking much better now.
class Employee: NSManagedObject {
@NSManaged var address: String
@NSManaged var dateOfBirth: NSDate
@NSManaged var firstName: String
@NSManaged var lastName: String
@NSManaged var salary: NSDecimalNumber
@NSManaged var status: Int32
}
Taking advantage of typealias
Another Swift feature we can use is a type alias declaration, which allows a new name to refer to an existing type. For an Employee
, it could be very helpful to work with a Salary
type instead of an NSDecimalNumber
type. In the depths of the codebase, there may be operations on NSDecimalNumber
values where it is not clear what those values represent. A typealias
makes our model much more descriptive and allows us to operate on values of a much more expressive Salary
type.
class Employee: NSManagedObject {
// other properties...
typealias Salary = NSDecimalNumber
@NSManaged var salary: Salary
}
We can then write functions that receive and return an Employee.Salary
type. Such functions can retain their brevity, while maximizing their clarity.
func computeRaise(salary: Employee.Salary) -> Employee.Salary
As noted in objc.io, we can take this one step further by using a wrapper type. To do this with an NSManagedObject
subclass, we’ll need to do some wrapping and unwrapping (no pun intended). First, the original property in Core Data should be marked as private
. Then we can use a computed property for the new wrapper type that transforms the private property value to and from the wrapper value. This is a bit of work, but the clarity and safety we receive in return are well worth it.
struct Salary {
let amount: NSDecimalNumber
}
class Employee: NSManagedObject {
// other properties...
@NSManaged private var salaryAmount: NSDecimalNumber
var salary: Salary {
get {
return Salary(amount: self.salaryAmount)
}
set {
self.salaryAmount = newValue.amount
}
}
}
// Usage
employee.salary = Salary(amount:10000.0)
Unfortunately, for fetch requests you still need to use the underlying private property name, salaryAmount
. This is because Core Data doesn’t know about the salary
computed property, nor the Salary
type. However, I think the naming conventions used here minimize confusion. That is, salary.amount
corresponds to the private salaryAmount
.
Using Swift enumerations
It is not uncommon for a model object to encode some type of state as an enum
. With Objective-C, you could define an NS_ENUM
and store an integer property in your managed object. But an enum
in Objective-C is little more than a glorified integer. By adopting an approach similar to the wrapper type above, we can get all the power of a Swift enum
directly in our managed object. This is incredibly useful.
Let’s see what this would look like for the status
property in the Employee
class.
enum EmployeeStatus: Int32 {
case ReadyForHire, Hired, Retired, Resigned, Fired, Deceased
}
class Employee: NSManagedObject {
// other properties...
@NSManaged private var statusValue: Int32
var status: EmployeeStatus {
get {
return EmployeeStatus(rawValue: self.statusValue)!
}
set {
self.statusValue = newValue.rawValue
}
}
}
// Usage
employee.status = .Hired
Note: Just as in the previous example, for fetch requests you would still need to use the private property name, statusValue
.
Furthermore, this is not limited to integers. You can apply this strategy with an enumeration of any type that Core Data supports. For example, for an Employee
there could be fixed salary amounts that correspond to an employee’s role. With slightly more effort, you can even support an enumeration with associated values. I’ll leave that as an exercise for the reader.
Clarity, or something slightly less terrible
Swift has a lot of potential to improve Core Data, but it does require more effort for developers and has some inconvenient workarounds and shortcomings. While I think it’s worth the time, the wrapping and unwrapping of values described above can be tedious to implement. And having to use the underlying private property names for fetch requests feels dirty. On the bright side, we get optionals and type aliases for free — a great step forward.
In any case, I do think this is better than what we had before. Sometimes it seems like Swift is bringing out the worst in Cocoa and Objective-C. Here’s to hoping the toolset will improve — and when Cocoa finally dies, I’ll be cheering for a Swift re-implementation of Core Data.