UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

FriendFace with CoreData: Day 61

Forums > 100 Days of SwiftUI

So, I followed along with the HWS+ video for the day 61 challenge after I got stuck with a non-helping error message from Xcode. Afterward, I did a few changes and moved the logic from the ContentView file to a Network and Data Manager class (which is using a singleton). I put it into the environment as a @StateObject in the App.Swift file.

Anyway, as I was tinkering away, I made changed the List in ContentView to show a badge with the number of friends each user has. And here the weird behavior started. When I run the app in the simulator or on my phone, the number of friends changes between restarting the app. What is going on? The JSON file doesn’t change. And I'm not getting any errors when decoding or saving

    func fetchUsers() async {
        guard hasNotFetched else { return }

        do {
            let url = URL(string: "https://www.hackingwithswift.com/samples/friendface.json")!
            let (data, _) = try await URLSession.shared.data(from: url)

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

            let users = try decoder.decode([User].self, from: data)
            await MainActor.run {
                updateCache(with: users)
            }
        } catch {
            print(error)
        }
    }

    func updateCache(with downloadedUsers: [User]) {

        let moc = DataController.shared.container.viewContext

        for user in downloadedUsers {
            let cachedUser = CachedUser(context: moc)

            cachedUser.id = user.id
            cachedUser.isActive = user.isActive
            cachedUser.name = user.name
            cachedUser.age = Int16(user.age)
            cachedUser.company = user.company
            cachedUser.email = user.email
            cachedUser.address = user.address
            cachedUser.about = user.about
            cachedUser.registered = user.registered
            cachedUser.tags = user.tags.joined(separator: ", ")

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

                cachedUser.addToFriends(cachedFriend)
            }
        }

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

    }

3      

Very good point @fi20100!!! I think many of us did not pay attention to that bug. Even without moving logic to data manager or observable object it messes up with cachedFriend.

So the reason I suppose is with concurrency. Core Data has a function to help guarantee Core Data tasks always happen one after another in a serial queue, no matter where that task is added from. This function is called perform.

After modifying the udpateCache to below, it fixes the issue. Please note that I did not extract that part to observable object, however I think it should solve the issue with your code.

    func updatedCache(with downloadedUsers: [User]) {
        for user in downloadedUsers {
            let cachedUser = CachedUser(context: moc)

            moc.perform {
                cachedUser.id = user.id
                cachedUser.isActive = user.isActive
                cachedUser.name = user.name
                cachedUser.age = Int16(user.age)
                cachedUser.company = user.company
                cachedUser.email = user.email
                cachedUser.address = user.address
                cachedUser.about = user.about
                cachedUser.registered = user.registered
                cachedUser.tags = user.tags.joined(separator: ",")

                for friend in user.friends {
                    let cachedFriend = CachedFriend(context: moc)
                    cachedFriend.id = friend.id
                    cachedFriend.name = friend.name
                    cachedUser.addToFriends(cachedFriend)
                }
                print("\(user.friends.count) friends for \(cachedUser.name!)")
            }
        }

        try? moc.save()
    }

3      

So also just updated logic to have observable object and can confirm that it works as we expect it to work.

I have created observable object as follows:

import Foundation
import CoreData

class UserDataModel: ObservableObject {
    @Published var users: [CachedUser] = []

    func fetchUsers(using moc: NSManagedObjectContext) async {
        guard users.isEmpty else { return }

        do {
            let url = URL(string: "https://www.hackingwithswift.com/samples/friendface.json")!
            let (data, _) = try await URLSession.shared.data(from: url)
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .iso8601
            let users = try decoder.decode([User].self, from: data)
            await MainActor.run {
                updatedCache(with: users, in: moc)
            }

            await MainActor.run {
                fetchFromCoreData(in: moc)
            }

        } catch {
            print("Download failed")
        }
    }

    func updatedCache(with downloadedUsers: [User], in moc: NSManagedObjectContext) {
        for user in downloadedUsers {
            let cachedUser = CachedUser(context: moc)

            moc.perform {
                cachedUser.id = user.id
                cachedUser.isActive = user.isActive
                cachedUser.name = user.name
                cachedUser.age = Int16(user.age)
                cachedUser.company = user.company
                cachedUser.email = user.email
                cachedUser.address = user.address
                cachedUser.about = user.about
                cachedUser.registered = user.registered
                cachedUser.tags = user.tags.joined(separator: ",")

                for friend in user.friends {
                    let cachedFriend = CachedFriend(context: moc)
                    cachedFriend.id = friend.id
                    cachedFriend.name = friend.name
                    cachedUser.addToFriends(cachedFriend)
                }
                print("\(user.friends.count) friends for \(cachedUser.name!)")
            }
        }

        try? moc.save()
    }

    func fetchFromCoreData(in moc: NSManagedObjectContext) {
        let request = CachedUser.fetchRequest()
        request.sortDescriptors = [NSSortDescriptor(keyPath: \CachedUser.name, ascending: true)]

        if let users = try? moc.fetch(request) {
            self.users = users
        }
    }
}

and then we can adjust to our content view to hanlde only UI stuff.

struct ContentView: View {
    @Environment(\.managedObjectContext) var moc
    @StateObject var viewModel = UserDataModel()

    var body: some View {
        NavigationView {
            List(viewModel.users) { user in
                NavigationLink {
                    UserView(user: user)
                } label: {
                    HStack {
                        Circle()
                            .fill(user.isActive ? .green : .red)
                            .frame(width: 30)
                        Text(user.wrappedName)
                    }
                }
            }
            .navigationTitle("Friendface")
            .task {
                await viewModel.fetchUsers(using: moc)
            }
        }
    }
}

3      

Thank you. Only adding the moc.perform fixed the friends problem, but had the adverse effect of empty users with nil values being created on subsequent runs. I have to look into this a bit more when I have a little bit more time. Thank you for your help!

3      

If this is still relevant or maybe some other people out there may, and most probably will have similar issues with that.

Anyway, as I was tinkering away, I made changed the List in ContentView to show a badge with the number of friends each user has. And here the weird behavior started. When I run the app in the simulator or on my phone, the number of friends changes between restarting the app. What is going on?

This behavior is the result of id property we have in User and Friend structures as well attribute with the name "id" in datamodel file of core data. CoreData entities by themselves have id generated, and this somehow affects it if we create our own id, maybe if we name it differently it will work just fine but i haven't check it. By removing id attributes and id properties from datamodel and above mentioned structs, project seems to be working just fine.

3      

Hacking with Swift is sponsored by Superwall

SPONSORED Superwall lets you build & test paywalls without shipping updates. Run experiments, offer sales, segment users, update locked features and more at the click of button. Best part? It's FREE for up to 250 conversions / mo and the Superwall team builds out 100% custom paywalls – free of charge.

Learn More

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.