comment 0

#28 Better CoreData with Swift Generics

In Issue #25 I have been talking about building a modern CoreData service using NSPersistentStoreContainer. This resulted in a lot of boilerplate code removed and a simpler API. Now, if you are using architectures like VIPER, VIP (see more in Issue #24) or simply ensuring immutability of your models, you are probably wrapping your CoreData's NSManagedObject subclasses into structs of some sort and use them in upper layers of your app.

I will not try convince you here of superiority of having additional layer of immutable struct based model. You can find proper articles in the references section. I will just say what convinced me – thread safety, data consistency and no shared state.

This post will introduce two approaches that tap into the power of Swift generics and protocols. First was the initially designed for use in our app while the second is an evolution of the first one. Second one just suits our current needs better.

How does it work?

In the past we have been trying various approaches. There were worker classes that did mapping for the entire model, there were extensions or methods added to NSManagedObject subclasses and so on. These were good solutions but sometimes we have had a feeling that we are repeating ourselves too often. With Swift generics we have decided to redo our stack.

Key components:

  1. CoreDataWorker – has methods to get, remove, upsert, etc. It is generic and works with any pair of model struct and ManagedObject.
  2. ManagedObjectProtocol with toEntity() method. It ensures mapping from CoreData's NSManagedObject to struct model (called Entity)
  3. ManagedObjectConvertible with toManagedObject() method. Provides reverse operation of converting struct model to NSManagedObject

Approach #1: CoreDataWorker per Entity – NSManagedObject pair

Enough talking. Time to see some code!

NOTE: CoreDataWorker uses CoreDataService from previous post as a dependency

There you go:

For simplicity we have stared with get method. Other actions are very similar.

What's up there? Let's go step by step through it:

#1

This worker is a crucial part of our app. We want it and components that depend on it tested. That is why we have a protocol for it. The associatedtype allows us to use a generic types as parameters. Result<T> is a simple enum that can be either .successs(T) or .failure(Error).

#2

In CoreDataWorker we are defining generic types for Entities and NSManagedObjects with conditions that they have to meet. In this way we ensure that ManagedObject is indeed a CoreData object and that two way conversion will be possible.

#3

Here we inject CoreDataService created in previous post. It is responsible for running foreground or background tasks and provides CoreData contexts.

#4

This starts CoreData context. All further actions are embedded into closure to ensure proper thread use. As we are returning simple structs here we will be safe to use them later on any thread.

#5

That's where all the magic kicks in. With Swift's flatMap we are converting each NSManagedObject to its corresponding model struct.

#6

Here, after successful operation we return converted model structs ready to use in further parts of app.

#7

Something may go wrong during fetch, so we can complete with failure and inform user what went wrong.

Approach #2: CoreDataWorker per any Entity – NSManagedObject pair

Previous approach seems pretty neat. You have generic worker that can be used for any entity. But usually we operate on more than one entity in our code. So, this initial version usually forced us to define multiple CoreDataWorkers in one scope.
Thanks to higheror from our team we have switched to more elegant version of CoreDataWorker. Now, just the methods of CoreDataWorker are generic, not the whole worker. In this way we can have one instance of worker reused for multiple entities.
Let's have a look:

As you can see most of the code is pretty much the same. It is just a place where generics are declared that makes so much difference in the end result.

At this point, I owe you one explanation. What the heck is Entity.ManagedObject ? Read on .

Converting entities with ManagedObjectConvertible and ManagedObjectProtocol protocols

These are two simple protocols that force Entity and NSManagedObject pair to implement conversion methods.

ManagedObjectProtocol is implemented by NSManagedObject to convert it to simple model struct.

ManagedObjectConvertible is implemented by model struct to convert it to NSManagedObject in a given context.
Note the associatedtype here and type of Entity generic parameter in get method of second approach. This ManagedObject associatedtype is accessible from Entity in form of Entity.ManagedObject and gives the type to fetch on.

Conversion protocols in practice

How this would look like in practice?
We will define simple user struct and corresponding NSManagedObject. User struct could look as follows:

NSManagedObjects are generated automatically, but we still have to extend them with ManagedObjectProtocol to enable two-way conversion:

Ready…, set…, fetch!

Finally we would like to see our CoreDataWorker in action, right?

For the first approach you initialize your worker as follows:

…and use it like that:

To fetch data for another entity, say AccountMO you would need another worker typed CoreDataWorker<AccountMO, Account>.

With the second approach you just need one worker:

The fetchUser call is exactly the same, but thanks to neat trick with type inference in this new worker, it knows by itself that for User it should fetch UserMO from CoreData.

Your eagle eye, has probably cought the protocol in second worker declaration. This is additional bonus that facilitates mocking in unit testing. You cannot have it in first example as we have there an abstract protocol, which cannot be used as variable type.

If you try to declare first worker not as CoreDataWorker<UserMO,User> but CoreDataWorkerProtocol, this is what happens:

Protocol 'CoreDataWorkerProtocol' can only be used as a generic constraint because it has Self or associated type requirements.

Hungry for more?
Take a look at our github repository and see both approaches in action.

Summary

There are as many supporters as opponents of using Core Data's NSManagedObject's throughout all layers of project. The approach with structs also has it's strengths and weaknesses.

We get immutability which is great (especially in big projects) and more importantly we have thread safety ensured. We have tried the approach with struct models also in or VIPER project.

On the con side there is slightly more work to manage additional layer and less flexibility in retrieving relations especially in complex graphs.

For us this extra work is worth it, especially that we run our projects with -com.apple.CoreData.ConcurrencyDebug 1 flag on and haven't seen any CoreData crashes. I hope one day CoreData will become more Swifty .

We are still eager to improve and fine-tune our approach, tell us what you think .

References

There are also some out of the box solutions like CoreValue or CoreStore – promising but haven't tried them yet. They may offer some good solutions.

Leave a Reply


*