TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

SOLVED: Day 61 | Core Data / Codable Challenge | Friend Face

Forums > SwiftUI

Hello everyone! I hope you are all doing great!

I'm working on Day 61 "Time for Core Data" (https://www.hackingwithswift.com/100/swiftui/61)

So far I've managed to:

  1. Create CachedUser and CachedFriend entities with their relationships and constrains.
  2. Create their wrapped properties to return their underlying attributes.
  3. Retrieve the JSON Data and save part of it into Core Data.
  4. Use the CachedUser to update the MainView and pass it into the DetailView.

However, I can't find where to use Paul's MainActor recommendation to update the view AFTER the data is properly saved so the result is that some of the CachedUser's friends are loaded.

This is my ContentView:

struct ContentView: View {
    @Environment(\.managedObjectContext) var moc
    @FetchRequest(sortDescriptors: []) var usersCD: FetchedResults<CachedUser>
    @State private var users = [User]()

    var body: some View {

        NavigationView {
            List(usersCD, id: \.id) { user in

                NavigationLink {
                    DetailView(user: user)
                } label: {
                    HStack {
                        Text(user.wrappedName)
                        Spacer()
                        Text(user.isActive.description)
                    }
                }
            } 
            .task {
                if users.isEmpty {
                    await loadUsers()
                }
            }
            .navigationTitle("Codable Challenge")
        }
    }

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

        do {
            let (data, _) = try await URLSession.shared.data(from: url)

            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .iso8601

            if let decodedResponse = try? decoder.decode([User].self, from: data) {
                users = decodedResponse
                saveInCD()
            }
        } catch {
            print("Invalid data")
        }

    }

    func saveInCD() {
        users.forEach { user in
            let userToSave = CachedUser(context: moc)
            userToSave.id = user.id
            userToSave.isActive = user.isActive
            userToSave.name = user.name
            userToSave.age = Int16(user.age)
            userToSave.company = user.company
            userToSave.email = userToSave.email
            userToSave.address = user.address
            userToSave.about = user.about
            userToSave.registered = user.registered.formatted()

            for friend in user.friends {
                let cachedFriend = CachedFriend(context: moc)
                cachedFriend.id = friend.id
                cachedFriend.name = friend.name
                cachedFriend.friendOfUser = userToSave
            }

            if moc.hasChanges {
                try? moc.save()
            }

        }
    }

}

Any thoughts?

Thanks in advance for any help! KB

2      

Hi @KeyBarreto!

I think, we don't really need to use MainActor in this particular project. If you don't see any purple warning, I suppose we are fine to live without MainActor. In many cases we need to use it when we have some part of code running on background threads and then trying to update UI from that background thread. As we are aware all work with updating UI happens on the main thread so we have to switch to main thread in that case.

In this particular challenge we are downloading data using URLSession.shared.data(from: url) which is asynchronous so it can happen on the background thread. So if any piece of code doing some work that involves UI update happens in that block we need to switch it to main thread thus use MainActor.

On the other hand, .task modifier also runs code asynchronously, so if udating interface happens here then you will also need to pass it to MainActor. But the good indication for that will be purple warning saying that you are updating UI from the background thread.

In our particular case, we are fetching data from web, then store it in core data using moc. And as soon as data in moc @FetchRequest is run on the main thread, so the UI is updated on the main thread already.

Your code is fine, the only thing I would point is this part:

With every launch of your app this array will be empty.

@State private var users = [User]()

so every time below part will be triggered, and starts loading data from web and save it to core data. If this is what you want that is fine.

if users.isEmpty {
    await loadUsers()
}

If you want to load the data from web and then read it from core data then logic should be changed to something like this ->

  1. Remove @State private var users = [User]()
  2. In your loadUsers() before any line of code is run add this check guard usersCD.isEmpty else { return } thus checking if @FetchRequest brings you data from CoreData or not. If not, go and fetch that data from web first.
  3. And finally, in loadUsers() udpate your decodedResponse assignment.
    if let decodedResponse = try? decoder.decode([User].self, from: data) {
    users = decodedResponse // <- remove this part completely as you won't need it.
    saveInCD(from: decodedResponse) // <- pass decodedRespose aka [User] to save in core data, you will need to modify the 'saveInCD' func accordingly to accept [User]
    }

And finally, finally you can throw saveInCD(from: decodedResponse) into MainActore if you really want to use it. But I haven't noticed and difference.

if let decodedResponse = try? decoder.decode([User].self, from: data) {
   await MainActor.run {
      saveInCD(from: decodedResponse)
   }
}

PS in saveInCD(from: decodedResponse), in case you change the logic you will need to change this part

if moc.hasChanges {
    try? moc.save()
}

to that, to make sure data is saved properly.

do {
    try moc.save()
} catch let error {
    print(error)
}

Hopefully it will help :)

2      

Thank you so much @ygeras !!

I really appreciate your input, I had the feeling MainActor wasn't strictly necessary and it's nice to know I was on the right track.

I made all the changes you suggested but I'm still having problems showing all of the friends (I haven't had any purple warnings during the project)

Any thoughts?

I just realized I published my this question in the wrong forum. It should've been 100 Days of SwiftUI

2      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

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

I made all the changes you suggested but I'm still having problems showing all of the friends (I haven't had any purple warnings during the project)

What kind of issues do you have?

2      

The "List of Friends" is not being displayed correctly, I believe the reason for this is that the JSON data isn't properly saved in CoreData.

I added a print() statement in my saveInCD() method to check that the JSON data is correct

https://drive.google.com/file/d/1fkwIb9XjcZo_Ltpf5ToTttc_Dyg8Fbbb/view?usp=share_link

And then, I added the count of the friendsArray to the ContentView, which is a property of the CachedUser entity to see if the size matches the JSON data:

https://drive.google.com/file/d/1ugGmuznGAZ0Fz9MPZ18JFLaTTdy3DYKq/view?usp=share_link

P.D.: I still don't know how to add images to the messages, that's why I added the link of the screenshots

Thanks again for your help @ygeras

2      

Ok, I see. Thought as much.

Try to remove any constraints in Entities in DataModel file.

2      

I believe you need to apply the @MainActor attribute to func saveInCD().

The reason is in Paul’s instruction for the Day 61 challenge: “So, when it comes to creating and saving all your Core Data objects, that’s definitely a task for the main actor”. “Creating” means that when you instantiate the userToSave and cachedFriend model objects, you need to do it on the main thread. Likewise, assigning properties to those model objects must be done on the main thread. Finally, “saving” via moc.save() also must be on the main thread.

I suspect Xcode does not display purple warnings because Core Data is based on Objective C and is not integrated with Swift Concurrency. The new Swift Data framework should be more Swifty.

2      

Can you try replacing : cachedFriend.friendOfUser = userToSave

with: cachedUser.addToFriends(cachedFriend)

This should make your friends appear, let me know if it worked for you.

2      

Hello everyone! I'm resuming this project.

@bobstern I having a hard time figuring out where to apply the @MainActor attribute. If I add it inside the function, then it becomes asynchronus because of the await before MainActor.run .

@alexbrossard do you mean userToSave.addToFriend(cachedFriend) ? If so, it didn't work either.

Thanks again for the help.

2      

await marks a point in your code where the execution may pause (be suspended) while "awaiting" the excecution of some other code.

Calling a function with await does not necessarily mean that the function is asynchronous. await also is required to access a property or method of an actor or to call a function marked as a MainActor.

I have not seen a concise, intuitive explanation of await, but here are some quotes that should help:

From the Concurrency chapter in the Swift Language Guide at swift.org:   "Because the actor allows only one task at a time to access its mutable state, if code from another task is already interacting with the logger, this code suspends while it waits to access the property.”   “When you access a property or method of an actor, you use await to mark the potential suspension point."

From Paul's "Concurrency by Example":  "the await keyword is as much for us as it is for the compiler – it’s a way of clearly marking which parts of the function might suspend"   “if you’re attempting to...call a method on an actor, and you’re doing it from outside the actor itself, you must do so asynchronously using await.”   “from outside the actor, await is required even for synchronous properties and methods.”

The main thread is executed synchronously, of course. A function marked to be run on the main thread may have to await (delay) its execution while, for example, a view is being updated or is responding to user input.

2      

Incidentally, I believe there is no difference between: await MainActor.run { saveInCD() }

and marking saveInCD as @MainActor and calling it using: await saveInCD()

2      

Without having viewed Paul's solution video, I belatedly thought of two problems with the relationships between the cachedUser and cachedFriend entities. It would help if you would insert a screenshot of the entities in the model editor.

(1) cachedFriend.friendOfUser = userToSave implies that you defined a 1-to-many relationship wherein no two users have any friends in common, i.e., each friend instance is related to only one user instance. if that is not true, then: (a) before instantiating a new cachedFriend, you need to test whether a cachedFriend already exists with the same id. (b) you need a many-to-many relationship, which would make cachedFriend.friendOfUser to be of type NSSet, so that you would have to replace the quoted line of code with: cachedFriend.friendOfUser = cachedFriend.friendOfUser.adding(userToSave) as NSSet

(2) Taking this logic one step further, it does not make sense to me that Paul suggested defining cachedUser and cachedFriend as separate entities. A more realistic scenario would be that a user's friends are other users. In that case, there should be only one entity called cachedUser or cachedPerson. A user's friends would be defined in a 1-to-many relationship called "friends" that is a "self relationship", a relationship with the same entity on both sides.

2      

Considering that Swift Data is intended to eventually supersede Core Data, and considering that Paul wrote an entire book on Swift Data but only a few brief lessons on Core Data, it seems sensible to not spend any more time on this challenge or on learning Core Data at this point. It seems more productive to finish the 100 Days of SwiftUI, then learn Swift Data rather than Core Data if the subject interests you, and then return to Core Data only if you need the more advanced capabilities of Core Data that Apple has yet implemented in Swift Data.

On the other hand, if you relish the challenge, please don’t let me dissuade you!

2      

Thank you so much for your time @bobstern !

I guess Relationships are't quite clear to me yet. Indeed I used One-To-Many for CachedUser (because each can have many friends) and To-One for friends, that might be the problem.

And I really appreciate your advice (and your time), I was actually considering it myself when I heard about SwiftData, but I wanted to have CoreDate in my skills set too.

Moving on now!

Thanks againa!

3      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

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

You are not logged in

Log in or create account
 

Link copied to your pasteboard.