NEW: Subscribe to Hacking with Swift+ and accelerate your learning! >>

Day 61 - My solution to make Core Data conform to Codable

Forums > 100 Days of SwiftUI

Not sure how many people really struggled with it, but I personally did and I couldn't really find a topic that had a good solution to load the JSON data directly into Core Data, so I thought I'd present my solution and get some feedback :)

First of, the entities and attributes. Those should be pretty straigh-forward, the only thing I struggled with was the relationship between User and Friend. Ideally I would've like having just an array of links to other Users and I'm sure it would've been possible to decode it that way, but I'm really trying to do at least one part a day and that was just not possible for me to figure out in the time I gave myself.

For the constraints I simply used id for both and as a relationship I went with:

  • User
    • name: friends
    • destination: Friend
    • inverse: friendOf
    • type: toMany
  • Friend
    • name: friendOf
    • destination: User
    • inverse: friends
    • type: toOne

Then I set the code generation to manual for both and created the NSManagedObject subclasses. I didn't really make use of the friendOf relationship and just commented the property out.

Adding conformance to Identifiable Both classes needed to conform to Identifiable so I could use the restults from the @FetchRequest in a List, this is trivial since they both already have an id variable just adding the protocol to the classes was enough.

Making them conform to Decodable Since there is no use of encoding the data again I'm not gonna go into that here, but it shouldn't be a problem to add it.

After adding the protocol I needed to add the initializer, the problem was that I somehow needed to call it's parent initializer and give it a context and entity to use, so how could I provide that context? by using CodingUserInfoKey, whose decription reads (funnily enough)

A user-defined key for providing context during encoding and decoding.

So first I created an extension on CodingUserInfoKey, and added a static property so I could assign the context to it which is then used when calling self.init(entity:, insertInto)

    extension CodingUserInfoKey {
       static let context = CodingUserInfoKey(rawValue: "context")
    }

now inside the ContentView.swift we can do the following

    @Environment(\.managedObjectContext) var moc
    @FetchRequest( ... ) var users: FetchedResults<User>

    /*
    Views and stuff here
    the loadData function is called when the initial view appears by using .onAppear(perform:)
    */

    func loadData() {
        // I'm omitting all the code to load data and focus on just how to encode it into Core Data
        // before doing anything here I recommend checking if there's already data (users.isEmpty)
        let decoder = JSONDecoder()

        // add context to the decoder so the data can decode itself into Core Data
        // since we added the 'CodingUserInfoKey.context' property we know it's not nil, so force-unwrapping is fine
        decoder.userInfo[CodingUserInfoKey.context!] = self.moc

        // in case anyone struggled with the dates, didn't want to leave this out here
        decoder.dateDecodingStrategy = .iso8601

        // we don't actually need to save the result anywhere since we use a @FetchRequest to display the data 
        // that gets decoded right into the Core Data entities
        // though I suppose it would be possible to skip the fetchrequest and just use a @State variable, but that
        // would kind of defeat the purpose of using Core Data
        _ = try? decoder.decode([User].self, from: data)
    }

so all that's left is to add the actual conformance to the classes of our entities, it's the same for both

    // 'required' is needed for Decodable conformance
    // 'convenice' to call self.init(entity:, insertInto)
    required convenience public init(from decoder: Decoder) throws {

        // first we need to get the context again
        guard let context = decoder.userInfo[CodingUserInfoKey.context!] as? NSManagedObjectContext else { /* ... */ }

        // then the entity we want to decode into, in this example it's the 'User' entity
        guard let entity = NSEntityDescription.entity(forEntityName: "User", in: context) else { /* ... */ }

        // init self with the entity and context we just got
        self.init(entity: entity, insertInto: context)

        // as usual we need a container, I skipped creating the CodingKeys enum since that should be trivial
        let container = try decoder.container(keyedBy: CodingKeys.self)

        // the rest is just doing the actual decoding, finally
        // I decided to remove the '?' from the properties in the classes of my entities since I know the data will be there
        self.id = try container.decode(UUID.self, forKey: .id)
        // if you wanted to leave them option and use wrappers, just use 'decodeIfPresent' instead of just 'decode'

        // the two not completely trivial properties are 'tags' and 'friends'
        // for tags I just decode the data first, then save it into the entities property
        let tagArray = try container.decode([String].self, forKey: .tags)
        self.tags = tagArray.joined(separator: ", ")

        // for friends I decode it as an array, then create an NSSet from that array
        // of course this requires that 'Friend' conforms to decodable too, but you should know how to do that now ;)
        let friendArray = try container.decode([Friend].self, forKey: .friends)
        self.friends = NSSet(array: friendArray)

Hopefully some people will find this when they struggle with the challenge and find this useful. If you have some feedback please feel free to post it here, I'm sure there's stuff I could improve :)

2      

Firstly, thank you, the one headache I have had was the reload of the data causing multiple instances of Friends for each User, saw your commented code for the if (users.isEmpty) which was the missing piece to my puzzle.

Secondly, just wanted to add the following things I did to simplify the code (albeit it by 1 line) for the tags array

In the User Entity I made the tags (Attribute) Transformable (Type) and added a Custom Class of Array<Any>, this mean in the User+CoreDataProperties.swift file I could utilise the following

@NSManaged public var tags: [String]?

In the User+CoreDataClass.swift I could tehn utilise the following:

self.tags = try container.decode([String]?.self, forKey: .tags)

To access in my view all I need to do was create a wrappedTags variable and use a ForEach

1      

Thanks for the feedback!

I didn't look too much into Core Data aside from what I read in the 100 days (or maybe I missed Transformable), definitely good to know.

   

Really nice way with decoding JSON directly to into CoreData entities, very inspiring.

In my solution I made a little change to data model thou - list of friends is just regular array of UUIDs, got from mapped array of Friend struct items. Later on I get it simply with another request with predicate like NSPredicate(format: "id IN %@", arrayOfUUIDs)

On the other hand Tags can have dedicated entity, with many to many relationship, as many users have same tags.

One thing made me confused too - while calling try? self.moc.save() the app is crashing, but even without calling it data decoded into entities from json is saved. Verified with force closing the app, repoening in plane mode with WiFi disabled... The data still was in there...

Best, Kris.

   

Thank you! I am now starting this challenge and your post is a very helpful and clear summary on CoreData and Codable.

This is my first time dealing with data models and entity relationships but I wonder if the challenge data wouldn’t fit a many-to-many data model better. From the JSON it’s clear that a user can have many friends. However, given that on the challenge instructions nothing is said about the uniqueness of friends among different users, I think it is possible that some users share the same friends. In this case, shouldn’t the Friend entity point to (possibly) many users?

If this is true, when a shared friend is decoded a second time into your friendArray for a different user, wouldn’t CoreData detect the repeated friend id and just move the existing Friend object from the friends set of the previous user into the new user’s?

I did a simple test project with this model:

  • John
    • friends = [Mary, Peter]
  • Mary
    • friends = [John]
  • Peter
    • friend = [John]

like this

let john = User(context: moc)
john.id = 1 // I used Int here for simplicity
john.name = "John"

let mary = User(context: moc)
mary.id = 2
mary.name = "Mary"

let peter = User(context: moc)
peter.id = 3
peter.name = "Peter"

let johnF = Friend(context: moc)
johnF.id =  1
johnF.name = "John"

let maryF = FriendOne(context: moc)
maryF.id = 2
maryF.name = "Mary"

let peterF = FriendOne(context: moc)
peterF.id = 3
peterF.name = "Peter"

// I used the generated accessors but I think the results are similar with direct assignment
john.addToFriends(NSSet(array: [maryF, peterF]))
mary.addToFriends(johnF)
peter.addToFriends(johnF)

When I used the toOne relationship from Friend to User I got this output:

  • John
    • friends = [Mary, Peter]
  • Mary
    • friends = [ ]
  • Peter
    • friend = [John]

And if I flip the two last lines of the Friend assignment order for Mary and Peter I get John with the friends set empty, which seems to corroborate that the repeated object is moved from the friends set of one use to another's.

When I use the toMany relationship, the output is as expected.

   

Subscribe to Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

Not logged in

Log in
 

Link copied to your pasteboard.