Sharing passion in Swift

#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:

CoreDataWorker – has methods to get, remove, upsert, etc. It is generic and works with any pair of model struct and ManagedObject.

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:

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

1

2

3

4

5

6

protocolManagedObjectProtocol{

associatedtype Entity

functoEntity()->Entity?

}

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

1

2

3

4

5

6

protocolManagedObjectConvertible{

associatedtype ManagedObject

functoManagedObject(incontext:NSManagedObjectContext)->ManagedObject?

}

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:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

structUser{

letid:String

letusername:String?

varname:String?

varbirthday:Date?

}

extensionUser: ManagedObjectConvertible{

functoManagedObject(incontext:NSManagedObjectContext)->UserMO?{

guard letuser=UserMO.getOrCreateSingle(with:id,from:context)

else{returnnil}

user.identifier=id

user.username=username

user.name=name

user.birthday=birthday asNSDate?

returnuser

}

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

// Managed object properties & conversion

extensionUserMO{

// CoreData autogenerated

@nonobjc public classfuncfetchRequest()->NSFetchRequest<UserMO>{

returnNSFetchRequest<UserMO>(entityName:"UserMO")

}

@NSManaged public varidentifier:String

@NSManaged public varusername:String?

@NSManaged public varname:String?

@NSManaged public varbirthday:NSDate?

}

extensionUserMO: ManagedObjectProtocol{

functoEntity()->User?{

returnUser(id:identifier,

username:username,

name:name,

birthday:birthday as?Date)

}

}

Ready…, set…, fetch!

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

For the first approach you initialize your worker as follows:

1

2

3

4

letworker:CoreDataWorker<UserMO,User>=CoreDataWorker<UserMO,User>()

…and use it like that:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

funcfetchUser(completion:@escaping(User?)->Void){

worker.get{[weakself](result:Result<[User]>)in

switchresult{

case.success(letusers):

self?.currentUser=users.first

completion(users.first)

case.failure(leterror):

print("\(error)")

completion(nil)

}

}

}

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:

1

2

3

letworker:CoreDataWorkerProtocol=CoreDataWorker()

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 .

6 Comments

How could we convert managed object say ChildMO in relation to UserMO to child struct using this structure. UserMO ->> Child i.e. one to many relation.

To fetch the data of child relation I used following code but Faulting technique of core data does not seem to work as all data has to be structured during fetch request.
I have written below code to convert child relation to [Child]
extension UserMO: ManagedObjectProtocol {
func toEntity() -> User? {
return User(id: identifier!, username: username, name: name, birthday: birthday as Date?, self. hasChildren)
}

This article has been extremely helpful to me! I have implemented it and even extended it with CloudKit support. However, for databases with relationships there is a problem: Faulting cannot be applied here, which causes an infinite loop.

Usually, when working with CoreData, it keeps referenced items as faults until you access them. That cannot be applied here as you Always access those relationships instantly as you populate your ManagedObjectConvertible. This often results in an infinite loop if you have a database with objects that have relations to each other.

A proper solution for this would be to have a function called something like populateFromRelated(object: ManagedObjectConvertible, at key: String), in which you pass the related object. The destination ManagedObjectProtocol that would received this call would then be able to populate its ManagedObjectConvertible while skipping the related object that is already populated.
The problem, however, with this approach is that you have to type-check on your generic protocols which is not (yet) possible as they have associated types.

My question is: What would be a proper way to prevent this infinite loop from occurring with the current Swift version? I have seen a lot of suggestions of implementing a shadow protocol that would enable type-checking, or removing associatedTypes but all of these fixes are workarounds. Is there anyone who has a proper solution, or should we wait until Swift 5 arrives?