CoreValue: Lightweight Framework for using Core Data with Value Types
I released a new Swift framework that makes it reasonably easy to use CoreData with Value types such as Swift's struct
.
Features
- Uses Swift Reflection to convert value types to NSManagedObjects
- iOS and Mac OS X support
- Use with
structs
- Works fine with
let
andvar
based properties - Swift 5.0
Rationale
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 boxing
value types into Core Data objects and unboxing
Core Data objects into value types. It also contains simple abstractions for easy querying, updating, saving, and deleting.
Usage
The following struct supports boxing, unboxing, and keeping object state:
}
That's it. Everything else it automated from here. Here're some examples of what you can do with Shop
then:
// Get all shops (`[Shop]` is required for the type checker to get your intent!)
let shops: = Shop.query
// Create a shop
let aShop = Shop
// Store it as a managed object
aShop.save
// Change the age
aShop.age = 40
// Update the managed object in the store
aShop.save
// Delete the object
aShop.delete
// Convert a managed object into a shop (see below)
let nsShop: Shop? = try? Shop.fromObject
// Convert a shop into an nsmanagedobject
let shopObj = nsShop.mutatingToObject
Querying
There're two ways of querying objects from Core Data into values:
// With Sort Descriptors
public static func query // Without sort descriptors
public static func query
If no NSPredicate
is given, all objects for the selected Entity are returned.
Usage in Detail
CVManagedPersistentStruct
is a typealias
for the two primary protocols of CoreValue: BoxingPersistentStruct
and UnboxingStruct
.
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's Reflection
feature. See for yourself:
That's it. Your value type is now CoreData compliant. Just call aCounter.toObject(context)
and you'll get a properly encoded NSManagedObject
!
If you're interested, have a look at the internalToObject
function 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 BoxingPersistentStruct
protocol, but instead something different called BoxingStruct
, 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 NSManagedObject
from CoreData, we need a way to store the connection to the original NSManagedObject
in the value type. Otherwise, calling save
again (say after updating the value type) would not update the NSManagedObject
in question, but instead insert a new NSManagedObject
into 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:
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 BoxingStruct
protocol then, you may ask. The advantage is that BoxingStruct
does 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 .toObject
to convert a value type into an NSManagedObject
, 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 the NSManagedObject
itself (.valueForKey
, .save
).
Boxing and Sub Properties
A word of advice: If you have value types in your value types, like:
Then you have to make sure that all value types conform to the same boxing protocol, either BoxingPersistentStruct
or BoxingStruct
. The type checker cannot check this and report this as an error.
Ephemeral Objects
Most protocols in CoreValue mark the NSManagedObjectContext
as an optional, which means that you don't have to supply it. Boxing will still work as expected, only the resulting NSManagedObject
s 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 a NSManagedObjectContext
will not result in an error.
UnboxingStruct
In CoreValue, boxed
refers to values in an NSManagedObject
container. I.e. NSNumber
is boxing an Int
, NSOrderedSet
an Array
, and NSManagedObject
itself is boxing a value type (i.e. Shop
).
UnboxingStruct
can be applied to any struct or class that you intend to initialize from a NSManagedObject
. It only has one requirement that needs to be implemented, and that's fromObject
which takes an NSManagedObject
and should return a value type. Here's a very simple and unsafe example:
throws
}
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:
throws
}
This 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) -> T
into A -> B -> C
so that it can be called step by step
<^>
Map the following operations over the A -> B -> fn
that we just created
object <| "count"
First operation: Take object
, call valueForKey
with the key "count"
and assign this as the value for the first type of the curryed init function A
object <| "name"
Second operation: Take object
, call valueForKey
with the key "count"
and assign this as the value for the second type of the curryed init function B
Other 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. combine map operations) |
<\| | 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, CVManagedStruct
and CVManagedPersistentStruct
which contain Boxing and Unboxing in one type.
RawRepresentable
Enum support
By extending RawRepresentable
, you can use Swift enums
right away without having to first make sure your enum conforms to CVManagedStruct
.
: Boxing, Unboxing
}
extension CarType
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:
// One year has passed, update the age of our shops and employees by one
let shops: = Shop.query
for shop in shops
}
}
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 NSManagedObject
or struct is dicated by the NSManagedObjectID
unique 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. CVManagedUniqueStruct
allows you to force CoreValue / CoreData to use this external unique identifier in NSManagedObjectID
's stead. The implementation is easy. You just have to conform to the BoxingUniqueStruct
protocol which requires the implementation of a var
naming 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
/// Value of the Identifier for the current struct (e.g: 'self.id')
func IdentifierValue
Here's a complete & simple example:
let id: String
let name: String
static func fromObject throws
}
Please not that CVManagedUniqueStruct
adds an (roughly) O(n) overhead on top of NSManagedObjectID
based solutions due to the way object lookup is currently implemented.