comment 0

#35 Structs Alternative: Using Swift Protocols to Enhance Safety of Core Data Access

A previous article mentions the usage of a structs layer in accessing Core Data.

This approach has some very nice properties, like thread safety of the struct values and the structs are always well formed. However this approach became too bothersome in our code and had its own share of problems.

  1. To create a struct layer you have to implement the validation on them yourself. If you want to create a struct object in code and then transfer it to Core Data through a NSManagedObject it should be well formed, or you will be getting validation errors on context saves. This violates DRY, as we already have Core Data to do our validation.

  2. You also have to implement the code that translates structs to managed objects and managed objects to structs. This code is very repetitive and it is easy to miss one property (happened to us too many times).

  3. Entity relationships in structs are hard to do. You can’t nest structs if your object graph is not a tree, which is common, as Xcode issues a warning if the relations are not done in both sides. You can’t nest them, as the structs are stored as values and the code would take an infinite amount of space. You have to resort to storing identifiers in the relationship fields and fetching objects every time you access a relationship property, as you already don’t have access to the backing managed object. This is very bothersome, if you have to go through 3 or 4 relationship properties to get the data you need.

  4. The code to translate structs to managed objects is not as obvious as it sounds. Problems arise when you create a lot of related objects at once. You encounter a struct that has a relationship to an object that wasn’t saved yet. It will be created later, but at the time of the struct to managed object translation isn’t available. You have to resort to writing special code that will create stub managed objects based on the identifiers encountered and fill them in later, from other objects and save the context after all the objects are translated.

  5. Sometimes you just need reference semantics in your data. If you hold on to a struct, to use it later, you have to refetch the struct to work on current data, as other parts of the app may have modified the data already.

All that said, maybe Apple will create a new version of Core Data that will support structs and fix most of the problems with it.

Our Solution

We decided to ditch the structs for a different solution, but we still wanted to keep the thread safety of structs.

That said our context policy in using core data is to have a main context, which is used read only. We populate all the UI with the data obtained from the main context. We use the PersistentContainer.performBackgroundTask(_:) for all the writing, which creates ad-hoc contexts for every write and synchronize them on GCD queues if needed.

Our idea is to use protocols to enforce the Core Data access to be read only on the main thread, but still use managed objects everywhere. Saving changes on other threads will work with pure NSManagedObjects.

We create a protocol for our managed objects.

For every entity type in our code EntityMO , we create a separate read only protocol and call it EntityRO. Let’s look at an example implementation of the protocol.

We create the protocol, by copying the auto generated properties, removing @NSManaged public and adding { get } at the end and this gives us a protocol that prohibits us from modifying the object.

To actually use the read only protocols we add a helper method to NSManagedObjectContext:

This is just performing a fetch on the context, but the returned objects are not with the type [EntityMO] but [EntityRO]. Now we have to remember to only use fetchReadOnlyObjects while fetching objects. The static type of returned objects is EntityRO, so code which would modify the managed objects wouldn’t compile.

There are some downsides to this, but fortunately small enough, that it is still easier to use than rolling our own struct layer.

Ugly Exception #1: To-One Relationships

If you have a protocol that requires you to have a property p with protocol A, you can’t satisfy it, by a property p with type B, where B: A. This is unfortunate and we have to resort to creating new properties to make this work. For every property relationship, we create propertyRO requirement in the protocol and write a trivial implementation in the extension to make the types match up.

Ugly Exception #2: To-Many Relationships

To-Many relationships have the same problem as To-One and one additional problem if you still want to return a Set of objects from the relationship property. The problem is that Swift can’t handle a collection of objects described only by a non-trivial protocol like Hashable.

This hopefully will be fixed in future Swift version. To fix this we have to use the same trick that is used in the Swift standard library – type erasure. We create a struct container type, to keep objects conforming to our protocol inside.

This implements the Hashable protocol for us, so we can use a Set and it has a property base that we can use to access the wrapped object. We implement it like this:

And somewhere in the code, we do:

Summary

This is just a precaution to make modifying managed objects on the main thread impossible. Compile time safety is a nice thing, makes you sleep better. Hopefully a lot of the weird tricks in the above code will be removed with Swift 4 and above.

Links

Leave a Reply


*