Lightweight Framework for using Core Data with Value Types
structs
letand
varbased properties
Swift introduced versatile value types into the iOS and Cocoa development domains. They're lightweight, fast, safe, enforce immutability and much more. However, as soon as the need for CoreData in a project manifests itself, we have to go back to reference types and
@objc.
CoreValue is a lightweight wrapper framework around Core Data. It takes care of
boxingvalue types into Core Data objects and
unboxingCore Data objects into value types. It also contains simple abstractions for easy querying, updating, saving, and deleting.
The following struct supports boxing, unboxing, and keeping object state:
struct Shop: CVManagedPersistentStruct {// The name of the CoreData entity static let EntityName = "Shop" // The ObjectID of the CoreData object we saved to or loaded from var objectID: NSManagedObjectID? // Our properties let name: String var age: Int32 var owner: Owner? // Create a Value Type from an NSManagedObject // If this looks too complex, see below for an explanation and alternatives static func fromObject(_ o: NSManagedObject) throws -> XShop { return try curry(self.init) o o o o
That's it. Everything else it automated from here. Here're some examples of what you can do with
Shopthen:// Get all shops (`[Shop]` is required for the type checker to get your intent!) let shops: [Shop] = Shop.query(self.context, predicate: nil)// Create a shop let aShop = Shop(objectID: nil, name: "Household Wares", age: 30, owner: nil)
// Store it as a managed object aShop.save(self.context)
// Change the age aShop.age = 40
// Update the managed object in the store aShop.save(self.context)
// Delete the object aShop.delete(self.context)
// Convert a managed object into a shop (see below) let nsShop: Shop? = try? Shop.fromObject(aNSManagedObject)
// Convert a shop into an nsmanagedobject let shopObj = nsShop.mutatingToObject(self.context)
Querying
There're two ways of querying objects from Core Data into values:
// With Sort Descriptors public static func query(context: NSManagedObjectContext, predicate: NSPredicate?, sortDescriptors: [NSSortDescriptor]) -> Array// Without sort descriptors public static func query(context: NSManagedObjectContext, predicate: NSPredicate?) -> Array
If no
NSPredicateis given, all objects for the selected Entity are returned.Usage in Detail
CVManagedPersistentStructis atypealiasfor the two primary protocols of CoreValue:BoxingPersistentStructandUnboxingStruct.Let's see what they do.
BoxingPersistentStruct
Boxing is the process of taking a value type and returning an
NSManagedObject. CoreValue really loves you and that's why it does all the hard work for you via Swift'sReflectionfeature. See for yourself:struct Counter: BoxingStruct static let EntityName = "Counter" var count: Int let name: String }That's it. Your value type is now CoreData compliant. Just call
aCounter.toObject(context)and you'll get a properly encodedNSManagedObject!If you're interested, have a look at the
internalToObjectfunction in CoreValue.swift, which takes care of this.Boxing in Detail
Keen observers will have noted that the structure above actually doesn't implement the
BoxingPersistentStructprotocol, but instead something different calledBoxingStruct, what's happening here?By default, Value types are immutable, so even if you define a property as a var, you still can't change it from within except by declaring your function mutable. Swift also doesn't allow us to define properties in protocol extensions, so any state that we wish to assign on a value type has to be via specific properties on the value type.
When we create or load an
NSManagedObjectfrom CoreData, we need a way to store the connection to the originalNSManagedObjectin the value type. Otherwise, callingsaveagain (say after updating the value type) would not update theNSManagedObjectin question, but instead insert a newNSManagedObjectinto the store. That's obviously not what we want.Since we cannot implicitly add any state whatsoever to a protocol, we have to do this explicitly. That's why there's a separate protocol for persistent storage:
struct Counter: BoxingPersistentStruct let EntityName = "Counter"var objectID: NSManagedObjectID? var count: Int let name: String
}
The main difference here is the addition of
objectID. Once this property is there,BoxingPersistentStruct's bag of wonders (.save,.delete,.mutatingToObject) can be used.What's the usecase of the
BoxingStructprotocol then, you may ask. The advantage is thatBoxingStructdoes not require your value type to be mutable, and does not extend it with any mutable functions by default, keeping it a truly immutable value type. It still can use.toObjectto convert a value type into anNSManagedObject, however it can't modify this object afterwards. So it is still useful for all scenarios where you're only performing insertions (like a cache, or a log) or where any modifications are performed in bulk (delete all), or where updating will be performed on theNSManagedObjectitself (.valueForKey,.save).Boxing and Sub Properties
A word of advice: If you have value types in your value types, like:
struct Employee: BoxingPersistentStruct { let EntityName = "Employee" var objectID: NSManagedObjectID? let name: String }struct Shop: BoxingPersistentStruct { let EntityName = "Counter" var objectID: NSManagedObjectID? let employees: [Employee] }
Then you have to make sure that all value types conform to the same boxing protocol, either
BoxingPersistentStructorBoxingStruct. The type checker cannot check this and report this as an error.Ephemeral Objects
Most protocols in CoreValue mark the
NSManagedObjectContextas an optional, which means that you don't have to supply it. Boxing will still work as expected, only the resultingNSManagedObjects will be ephemeral, that is, they're not bound to a context, they can't be stored. There're few use cases for this, but it is important to note that not supplying aNSManagedObjectContextwill not result in an error.UnboxingStruct
In CoreValue,
boxedrefers to values in anNSManagedObjectcontainer. I.e.NSNumberis boxing anInt,NSOrderedSetanArray, andNSManagedObjectitself is boxing a value type (i.e.Shop).UnboxingStructcan be applied to any struct or class that you intend to initialize from aNSManagedObject. It only has one requirement that needs to be implemented, and that'sfromObjectwhich takes anNSManagedObjectand should return a value type. Here's a very simple and unsafe example:struct Counter: UnboxingStruct var count: Int let name: String static func fromObject(_ object: NSManagedObject) throws -> Counter { return Counter( count: object.valueForKey("count")!.integerValue, name: object.valueForKey("name") as! String ) } }Even though this example is not safe, we can observe several things from it. First, the implementation overhead is minimal. Second, the method can throw an error. That's because unboxing can fail in a multitude of ways (wrong value, no value, wrong entity, unknown entity, etc). If unboxing fails in any way, we throw an
NSError. The other benefit of unboxing, that it allows us to take a shortcut (which CoreValue deviously copied from Argo). Utilizing several custom operators, the unboxing process can be greatly simplified:struct Counter: UnboxingStruct var count: Int let name: String static func fromObject(_ object: NSManagedObject) throws -> Counter { return try curry(self.init) object objectThis code takes the automatic initializer, curries it and maps it over multiple incarnations of unboxing functions (
) until it can return a Counter (or throw an error).But what about these weird runes? Here's an in-detail overview of what's happening here:
Unboxing in Detail
curry(self.init)Convert
(A, B) -> TintoA -> B -> Cso that it can be called step by step Map the following operations over theA -> B -> fnthat we just createdobject First operation: Takeobject, callvalueForKeywith the key"count"and assign this as the value for the first type of the curryed init functionAobject Second operation: Takeobject, callvalueForKeywith the key"count"and assign this as the value for the second type of the curryed init functionBOther Operators
Custom Operators are observed as a critical Swift feature, and rightly so. Too many of those make a codebase difficult to read and understand. The following custom operators are the same as in several other Swift Frameworks (see Runes and Argo). They're basically a verbatim copy from Haskell, so while that doesn't make them less custom or even official, they're at least unofficially agreed upon.
is not the only operator needed to encode objects. Here's a list of all supported operators:| Operator | Description | |-|-|
| Map the following operations (i.e. combinemapoperations)| Unbox a normal value (i.e.var shop: Shop)| Unbox a set/list of values (i.e.var shops: [Shops])| Unbox an optional value (i.e.var shop: Shop?)CVManagedStruct
Since most of the time you probably want boxing and unboxing functionality, CoreValue includes two handy typealiases,
CVManagedStructandCVManagedPersistentStructwhich contain Boxing and Unboxing in one type.
RawRepresentableEnum supportBy extending
RawRepresentable, you can use Swiftenumsright away without having to first make sure your enum conforms toCVManagedStruct.enum CarType: String { case pickup case sedan case hatchback }extension CarType: Boxing, Unboxing {}
struct Car: CVManagedPersistentStruct { static let EntityName = "Car" var objectID: NSManagedObjectID? var name: String var type: CarType
static func fromObject(_ o: NSManagedObject) throws -> Car { return try curry(self.init) o o o
Docs
Have a look at CoreValue.swift, it's full of docstrings.
Alternatively, there's a lot of usage in the Unit Tests.
Here's a more complex example of CoreValue in use:
struct Employee: CVManagedPersistentStruct {static let EntityName = "Employee" var objectID: NSManagedObjectID? let name: String var age: Int16 let position: String? let department: String let job: String static func fromObject(_ o: NSManagedObject) throws -> Employee { return try curry(self.init) o o o o o o Shop { return try curry(self.init) o o o o
CVManagedUniqueStruct and REST / Serialization / JSON
All the examples we've seen so far resolve around a use case where data is contained within your app. This means that the unique identifier of an
NSManagedObjector struct is dicated by theNSManagedObjectIDunique identifier which CoreData generates. This is fine as long as you don't plan to interact with outside data. If your data is loaded from external sources (i.e. JSON from a Rest API) then it may already have a unique identifier.CVManagedUniqueStructallows you to force CoreValue / CoreData to use this external unique identifier inNSManagedObjectID's stead. The implementation is easy. You just have to conform to theBoxingUniqueStructprotocol which requires the implementation of avarnaming the unique id field and a function returning the current ID value:/// Name of the Identifier in the CoreData (e.g: 'id') static var IdentifierName: String { get }/// Value of the Identifier for the current struct (e.g: 'self.id') func IdentifierValue() -> IdentifierType
Here's a complete & simple example:
struct Author: CVManagedUniqueStruct {static let EntityName = "Author" static var IdentifierName: String = "id" func IdentifierValue() -> IdentifierType { return self.id } let id: String let name: String static func fromObject(_ o: NSManagedObject) throws -> Author { return try curry(self.init) o o
Please not that
CVManagedUniqueStructadds an (roughly) O(n) overhead on top ofNSManagedObjectIDbased solutions due to the way object lookup is currently implemented.State
All CoreData Datatypes are supported, with the following exceptions: - Transformable - Unordered Collections /
NSSet(Currently, only ordered collections are supported)Fetched properties are not supported yet.
Installation (iOS and macOS)
CocoaPods
Add the following to your Podfile:
pod 'CoreValue'You will also need to make sure you're opting into using frameworks:
use_frameworks!Then run
pod installwith CocoaPods 1.01 or newer.Carthage
Add the following to your Cartfile:
github "terhechte/CoreValue" ~> 0.3.0Then run
carthage update.Follow the current instructions in Carthage's README for up to date installation instructions.
The
import CoreValuedirective is required in order to use CoreValue.Manually
- Copy the
CoreValue.swiftandcurry.swiftfile into your project.- Add the
CoreDataframework to your projectThere is no need for
import CoreValuewhen manually installing.Contact
Benedikt Terhechte
Changelog
Version 0.4.0
Unboxedto Swift's native
throw. Big thanks to Adlai Holler for spearheading this!
CVManagedPersistentStructpublic
Included pull request from AlexanderKaraberov which includes a fix to the delete function
Updated to most recent Swift 2.0 b4 changes
Renamed NSManagedStruct and NSPersistentManagedStruct to CVManagedStruct and CVPersistentManagedStruct as NS is preserved prefix for Apple classes
Added CocoaPods support
Initial Release
CoreValue uses ideas and code from thoughtbot's Argo framework for JSON decoding. Most notably their
curryimplementation. Have a look at it, it is an awesome framework.
The CoreValue source code is available under the MIT License.