NEW: My new book Pro SwiftUI is out now – level up your SwiftUI skills today! >>

Day 61: Many questions

Forums > 100 Days of SwiftUI

I have completed day 60 and have a functioning app, but now I don't really know how to throw CoreData into the mix.

I have created a xcdatamodeld file, added entities and properties to it, manually added wrappers for the properties to my Entities, added a Persistence.swift file, and added the lines of code needed to make my managedObjectContext available in content view. So, at this point, I should be ready to start using CoreData in my app. However, I have some questions.

  1. Rather than adding 'friends' and 'tags' as properties for the 'User' entity, I have added them as relationships, and created 'Tag' as an entity in my data model as well. That seems to be the only way that we have learned to link an array of items to an entity, so I'm hoping that is correct, but is there another way to add an Array of items as a property for an Entity? If so, what do you select for the propery's type?

  2. If it was correct to create relationships instead of properties for 'friends' and 'tags', should I create them as "Many to Many" relationships? I don't know if I understand the types of relationships well enough, but it seems to me that one Friend can be assigned to many Users and one User can have many Friends. Similarly, one Tag can be assigned to many Users, and one User can have many Tags. Is "Many to Many" the correct type of relationship, or am I misunderstanding something?

  3. Previously, I was decoding the JSON data into an [User] array of Users as User was a struct that I had defined. But now that I already have User as an Entity in my data model, it seems redundant to also define a struct named User. Do I still need to have the User struct defined in my code, or should I just delete it?

  4. I don't think Paul has shown us how to load data from a file directly into CoreData before, or even how to store a whole collection of instances of an Entity into CoreData before, rather than just adding one instance at a time, so I'm not sure what is the best approach for doing this. I was able to find this older forum post https://www.hackingwithswift.com/forums/100-days-of-swiftui/day-61-postmortem/5635 with what looks like a great solution for being able to load JSON directly into CoreData by making the modelObjectContext conform to Codable. But this seems very advanced for where we are at right now, and I wonder if there is a simpler approach that can be taken?

3      

My biggest problem at this point is this...

This was my ContentView before

import SwiftUI
import CoreData

struct ContentView: View {
    @State private var users = [User]()

    var body: some View {
        NavigationView {
            List(users) { user in
                NavigationLink(destination: UserView(user: user, allUsers: users)) {
                    VStack {
                        HStack {
                            Text("ID:")
                                .fontWeight(.bold)
                            Spacer()
                            Text("\(user.id)")
                        }
                    }
                }
            }
            .navigationBarTitle("All Users")
            .onAppear(perform: loadUserData)
        }
    }

    func loadUserData() {
        guard let url = URL(string: "https://www.hackingwithswift.com/samples/friendface.json") else {
            print("Invalid URL")
            return
        }

        let request = URLRequest(url: url)

        URLSession.shared.dataTask(with: request) { data, response, error in
            guard let userData = data else {
                print("No data in response: \(error?.localizedDescription ?? "Unknown Error")")
                return
            }

            let userDecoder = JSONDecoder()

            userDecoder.dateDecodingStrategy = .iso8601

            do {
                users = try userDecoder.decode([User].self, from: userData)
                return
            } catch {
                print("Decoding Failed: \(error.localizedDescription)")
            }

        }.resume()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

But now, I need to use

@Environment(\.managedObjectContext) private var viewContext

instead of

@State private var users = [User]()

so users no longer exists in my loadUserData() function, and I don't know what to replace users with at this point. I need to load my data from the URL into viewContext but I don't know how to do it.

3      

Ok, I decided that it was probably best to make my data model entities conform to codable, so I followed the instructions on this page

https://www.donnywals.com/using-codable-with-core-data-and-nsmanagedobject/

It actually wasn't too hard to follow if you just take it one step at a time. I think I just got overwhelmed by it when I first looked at it, and didn't know if I should trust this random guy who was not Paul telling me what to do. But he seems to know what he is doing.

After doing that, I basically just had to create my @FetchRequest property named users, and it worked in all of the places where I was previously using my [User] named users. I just had to make sure to replace all of my User.property code with User.wrappedProperty, and make sure to pass along the ManagedObjectContext to the UserView in the NavigationLinks and boom my code would compile again!

However, I am still having problems.

My ContentView list only ends up having one User in it. When I tap on the user to see the UserView, most of their information is there, but the Tags and Friends list are completely empty.

Even worse, when I tap the 'Back' button and go back to my list of Users, it now has two of the same user in the list. Each time I go back and forth between the UserView and ContentView it adds another copy of that one user. I don't know how this is happening, because I have added constraints to the id property of all of my entities, and added

container = NSPersistentContainer(name: "FriendFace")
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

to my PersistenceController initializer, so I thought that should stop it from adding duplicates of my entities.

This is all getting very confusing, and I'm not even sure what code would be helpful for you to see at this point, but I have about 13 different files in my project now, and I don't think that sharing them all in this forum would be super helpful.

This is what my content view looks like now though

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest(entity: User.entity(), sortDescriptors: []) var users: FetchedResults<User>

    var body: some View {
        NavigationView {
            List(users) { user in
                NavigationLink(destination: UserView(user: user, allUsers: users).environment(\.managedObjectContext, self.viewContext)) {
                    VStack {
                        HStack {
                            Text(user.wrappedName)
                            Spacer()
                            Text(user.wrappedCompany)
                        }
                    }
                }
            }
            .navigationBarTitle("All Users", displayMode: .inline)
            .onAppear(perform: loadUserData)
        }
    }

    func loadUserData() {
        guard let url = URL(string: "https://www.hackingwithswift.com/samples/friendface.json") else {
            print("Invalid URL")
            return
        }

        let request = URLRequest(url: url)

        URLSession.shared.dataTask(with: request) { data, response, error in
            guard let userData = data else {
                print("No data in response: \(error?.localizedDescription ?? "Unknown Error")")
                return
            }

            let userDecoder = JSONDecoder()

            userDecoder.dateDecodingStrategy = .iso8601
            userDecoder.userInfo[CodingUserInfoKey.managedObjectContext] = viewContext

            do {
                let _ = try userDecoder.decode([User].self, from: userData)

                if viewContext.hasChanges {
                    try? viewContext.save()
                }
            } catch {
                print("Decoding Failed: \(error.localizedDescription)")
            }

        }.resume()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

This line seems like it might be a problem to me, bud I didn't know what else to do with the decoder at this point. Since it should just be storing the results into CoreData, I didn't know what else to do with the return value except store it in a constant with no name.

let _ = try userDecoder.decode([User].self, from: userData)

But I am getting the error shown just below that in my output log

CoreData: warning: View context accessed for persistent container FriendFace with no stores loaded Decoding Failed: The data couldn’t be read because it is missing.

1      

Hacking with Swift is sponsored by MadMachine

SPONSORED Want to try Swift on microcontrollers? MadMachine provides ways to interact with the physical world in a Swift way. Join us and have fun!

Get it now

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

I just remembered the tip that @RoosterBoy gave me a few days ago, which is to use error rather than error.localizedDescription when catching JSON decoding errors. Now I can see this error.

Decoding Failed: keyNotFound(CodingKeys(stringValue: "users", intValue: nil), Swift.DecodingError.Context(codingPath: "[_JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "friends", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"users\", intValue: nil) (\"users\").", underlyingError: nil))

1      

and didn't know if I should trust this random guy who was not Paul telling me what to do.

You can trust Donny Wals. And his books Practical Core Data and Practical Combine are essential reading, IMO.

Decoding Failed: keyNotFound(CodingKeys(stringValue: "users", intValue: nil), Swift.DecodingError.Context(codingPath: "[_JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "friends", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"users\", intValue: nil) (\"users\").", underlyingError: nil))

This error is because the JSON you are trying to decode doesn't have a users key. You can see that by loading https://www.hackingwithswift.com/samples/friendface.json in a new browser window and searching for "users".

This is all getting very confusing, and I'm not even sure what code would be helpful for you to see at this point, but I have about 13 different files in my project now, and I don't think that sharing them all in this forum would be super helpful.

Got a github project you can share the link to?

2      

So, that error leads me to believe that I have a problem with one or more of these three files.

I wasn't sure if I was supposed to add 'friends', 'users', or 'tags' to my coding keys, because they are set up as relationships and not properties in my data model. Maybe that has something to do with this?

User+CoreDataClass.swift

import Foundation
import CoreData

@objc(User)
public class User: NSManagedObject, Codable {
    enum CodingKeys: CodingKey {
        case about, address, age, company, email, friends, id, isActive, name, registered, tags
    }

    enum DecoderConfigurationError: Error {
        case missingManagedObjectContext
    }

    public required convenience init(from decoder: Decoder) throws {
        guard let context = decoder.userInfo[.managedObjectContext] as? NSManagedObjectContext else {
            throw DecoderConfigurationError.missingManagedObjectContext
        }

        self.init(context: context)

        let container = try decoder.container(keyedBy: CodingKeys.self)

        about = try container.decode(String.self, forKey: .about)
        address = try container.decode(String.self, forKey: .address)
        age = try container.decode(Int16.self, forKey: .age)
        company = try container.decode(String.self, forKey: .company)
        email = try container.decode(String.self, forKey: .email)
        friends = try container.decode(Set<Friend>.self, forKey: .friends) as NSSet
        id = try container.decode(UUID.self, forKey: .id)
        isActive = try container.decode(Bool.self, forKey: .isActive)
        name = try container.decode(String.self, forKey: .name)
        registered = try container.decode(Date.self, forKey: .registered)
        tags = try container.decode(Set<Tag>.self, forKey: .tags) as NSSet
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(about, forKey: .about)
        try container.encode(address, forKey: .address)
        try container.encode(age, forKey: .age)
        try container.encode(company, forKey: .company)
        try container.encode(email, forKey: .email)
        try container.encode(friends as! Set<Friend>, forKey: .friends)
        try container.encode(id, forKey: .id)
        try container.encode(isActive, forKey: .isActive)
        try container.encode(name, forKey: .name)
        try container.encode(registered, forKey: .registered)
        try container.encode(tags as! Set<Tag>, forKey: .tags)
    }
}

Friend+CoreDataClass.swift

import Foundation
import CoreData

@objc(Friend)
public class Friend: NSManagedObject, Codable {
    enum CodingKeys: CodingKey {
        case id, name, users
    }

    enum DecoderConfigurationError: Error {
        case missingManagedObjectContext
    }

    public required convenience init(from decoder: Decoder) throws {
        guard let context = decoder.userInfo[.managedObjectContext] as? NSManagedObjectContext else {
            throw DecoderConfigurationError.missingManagedObjectContext
        }

        self.init(context: context)

        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(UUID.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        users = try container.decode(Set<User>.self, forKey: .users) as NSSet
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(id, forKey: .id)
        try container.encode(name, forKey: .name)
        try container.encode(users as! Set<User>, forKey: .users)
    }
}

Tag+CoreDataClass.swift

import Foundation
import CoreData

@objc(Tag)
public class Tag: NSManagedObject, Codable {
    enum CodingKeys: CodingKey {
        case id, users
    }

    enum DecoderConfigurationError: Error {
        case missingManagedObjectContext
    }

    public required convenience init(from decoder: Decoder) throws {
        guard let context = decoder.userInfo[.managedObjectContext] as? NSManagedObjectContext else {
            throw DecoderConfigurationError.missingManagedObjectContext
        }

        self.init(context: context)

        let container = try decoder.container(keyedBy: CodingKeys.self)

        id = try container.decode(String.self, forKey: .id)
        users = try container.decode(Set<User>.self, forKey: .users) as NSSet
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(id, forKey: .id)
        try container.encode(users as! Set<User>, forKey: .users)
    }
}

1      

Aha! Yeah that does make sense that the JSON wouldn't have a users key. Those are just inverse relationships that I set up in my Tag and Friend entities, but they don't actually exist in the JSON so I probably don't need to include those coding keys or those lines of code in the initializers in my Friend+CoreDataClass.swift and Tag+CoreDataClass.swift files.

After removing those, I end up with this error instead.

Decoding Failed: typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "tags", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "Expected to decode Dictionary<String, Any> but found a string/data instead.", underlyingError: nil))

I assume this has something to do with the fact that I created Tag as an Entity with a String property in my data model, and tags is just an [String] in the JSON file. I wasn't sure how to create an [String] as a property in my data model though, because I don't know what type to set the property to.

1      

Unfortunately, I haven't taken the time to learn to use Github yet. So I don't really know how to add my projects there at this time. But maybe it's about time I learn.

1      

I changed up my data model, so now Tag is no longer an entity, tags is just a property of the User entity. I set the type of the property to "Transformable" but then I was getting a warning in XCode telling me that 'tags' had an undefined transformer. So I tried to enter NSSet\<String> as the transformer, and that got my code to compile, although, I still don't think that is the correct way to do it because it is giving this warning in my output log now.

CoreData: warning: no NSValueTransformer with class name 'NSSet\<String>' was found for attribute 'tags' on entity 'User'

But it looks like it is loading all of the data from the JSON now, so that is bit of a relief.

But I still have other problems. For one thing, I can't save my modelObjectContext. When I try to put

try? viewContext.save()

into my loadData() function, it causes my app to crash with this error

Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSCFSet: 0x6000004c0060> was mutated while being enumerated.'

I think this is some kind of threading issue. Like, the data doesn't finish decoding before trying to save the modelObjectContext or something, but I don't know how to fix that.

Also, when I try to click on a user, it takes me to the User view, and then I click on one of their friends, and it takes me to another User view for the friend that I tapped on, but every time I do that it is saying this in the output log...

FriendFace[13539:2234741] [Assert] displayModeButtonItem is internally managed and not exposed for DoubleColumn style. Returning an empty, disconnected UIBarButtonItem to fulfill the non-null contract.

I think I might just have to give up on this project for now, but at least I got it pretty close to working. Maybe after I learn to use GitHub I'll be able to add my project there and somebody will be able to help me with it a little better. I know that this Forum has gotten long winded and is probably hard to follow and give answers to, so sorry about that.

1      

I have added this project to My GitHub Repository now, and am asking if anybody might be able to help me figure out why it isn't working.

If I remove the code from ContentView that tells my ModelObjectContext to try to save, the app seems to run alright. But, as soon as I try to save my data, the app crashes.

I'm not sure if there is just a problem with threading that I don't know how to fix, which is causing the MOC to try to save before the data finishes loading from the URL, or if I might have set up my relationships or constraints in my data model incorrectly, and that is causing a problem?

I know that my user interface is kind of ugly right now, but it is put together enough to see the data anyway.

1      

I've just started attempting Day 61 today. I must admit I'm feeling kind of overwhelmed in a way I've not felt in so far. Everywhere I look, I've got questions and there don't seem to be answers in what we've been taught to date.

I really am not a fan of Core Data!!

So far I've done the unwrapping code and (I think) set up the Core Data initialisation and moc etc.

I've gone the route of creating tags as a separate entity with a relationship linking it to users with a many to many connections.

Some of the questions I've got floating around in my head are:

  • How to save my Struct instances to the core data entities
  • As per the Candy / Country example project, the instances of each entity is manually named and saved. 'Candy 1', 'Candy2' etc. I haven't a clue how to programmatically name the instances of each user, friend and tag.
  • Once getting it all into Core Data (currently feels a long way off) I'm concerned about the data being duplicated each time the app is loaded. Perhaps avoided thanks to setting up constraints in the core data entities?

Right now, I'm considering making another core data entity that saves the array of Users so that I don't have to worry about the names of the instances. But that seems kind of nuts? Maybe not?

Once I've got the data into core data, I'm vaguely confident that I'll be able to refactor my code to access the core data instances rather than the JSON.

BTW, kind of worried that you're still battling with this a month later @Fly0strich

1      

Did you ever manage to solve this?

1      

No, I've pretty much just decided to move on from it and will probably try to remake the project from scratch afteri finish the 100 days and see if i can figure it out then.

1      

Hacking with Swift is sponsored by MadMachine

SPONSORED Want to explore your Swift skill outside of the Apple world? Join the MadMachine community and start to program microcontrollers in Swift.

Get it now

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

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.