BLACK FRIDAY SALE: Save big on all my Swift books and bundles! >>

Why am I failing the uniqueness constraint when trying to save to CoreData in Friendface (Day 61)?

Forums > 100 Days of SwiftUI

I'm successfully fetching friendface.json and decoding it into an array of User structs. I'm then iterating over array and building up an array of CachedUser objects (my CoreData entity). But when I call try moc.save() I get the following error: "operation couldn't be completed cocoa error 133021". From searching around it appears to be caused by violating the uniqueness constraint on id. Assuming the json file does not contain duplicate ids, what would cause this? I've setup the application with a load button that tries to load the data from the web and if that fails reverts to loading from the cache (CoreData). I simulate a failure by passing a bad URL. getAsUser and getAsFriend are just computed properties that create a user or friend struct populated with the values from the cachedUser or cachedFriend object.

ContentView:

struct ContentView: View {
    @Environment(\.managedObjectContext) var moc
    @FetchRequest(sortDescriptors: []) var cachedUsers: FetchedResults<CachedUser>

    @State private var users: [User] = [User]()
    @State private var showingError: Bool = false
    @State private var errorMessage: String = ""

    var body: some View {
        VStack {
            NavigationView {
                List {
                    ForEach(users) { user in
                        NavigationLink {
                            UserDetailsView(user: user)
                        } label: {
                            Text(user.name + (user.isActive ? " (Active)" : " (Inactive)"))
                        }
                    }
                }
                .navigationTitle("User List")
                .alert("Error!", isPresented: $showingError) {
                    Button("OK") {}
                } message: {
                    Text(errorMessage)
                }

            }
            Button(users.isEmpty ? "Load" : "Refresh") {
                Task {
                    await loadData()
                }
            }

        }
    }

    func loadData() async {
        let url = URL(string: "https://www.hackingwithswift.com/samples/friendface.json")!
        //let url = URL(string: "friendface.json")!

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

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

                users = try decoder.decode([User].self, from: data)

                for u in users {
                    var cachedUsers = [CachedUser(context: moc)]
                    let cu = CachedUser(context: moc)
                    cu.id = u.id
                    cu.isActive = u.isActive
                    cu.name = u.name
                    cu.age = Int16(u.age)
                    cu.company = u.company
                    cu.email = u.email
                    cu.address = u.address
                    cu.about = u.about
                    cu.registered = u.registered
                    cu.tags = u.tags.joined(separator: ",")

                    for f in u.friends {
                        let cf = CachedFriend(context: moc)
                        cf.id = f.id
                        cf.name = f.name
                        cu.addToFriend(cf)
                    }
                    cachedUsers.append(cu)
                }

                try moc.save()
            }
        } catch {
            errorMessage = "\(error.localizedDescription)"

            if users.isEmpty {
                for cu in cachedUsers {
                    users.append(cu.getAsUser)
                }
                errorMessage = "Could not load from URL. Loading from cache. Cache count: \(cachedUsers.count)"
            }
            showingError = true
        }
    }
}

User struct:

public struct User: Codable, Identifiable {
    public var id = UUID()
    let isActive: Bool
    let name: String
    let age: Int
    let company: String
    let email: String
    let address: String
    let about: String
    let registered: Date
    let tags: [String]
    var friends: [Friend]
}

Friend struct:

public struct Friend: Codable, Identifiable {
    public var id = UUID()
    let name: String
}

CachedUser Class:

extension CachedUser {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<CachedUser> {
        return NSFetchRequest<CachedUser>(entityName: "CachedUser")
    }

    @NSManaged public var id: UUID?
    @NSManaged public var isActive: Bool
    @NSManaged public var name: String?
    @NSManaged public var age: Int16
    @NSManaged public var company: String?
    @NSManaged public var email: String?
    @NSManaged public var address: String?
    @NSManaged public var about: String?
    @NSManaged public var registered: Date?
    @NSManaged public var tags: String?
    @NSManaged public var friend: NSSet?

    public var wrappedId: UUID {
        id ?? UUID()
    }

    public var wrappedName: String {
        name ?? "Unknown Name"
    }

    public var wrappedCompany: String {
        company ?? "Unknown Company"
    }

    public var wrappedEmail: String {
        email ?? "Unknown Email"
    }

    public var wrappedAddress: String {
        address ?? "Unknown Address"
    }

    public var wrappedAbout: String {
        about ?? "Unknown About"
    }

    public var wrappedRegistered: Date {
        registered ?? Date.now
    }

    public var wrappedTags: String {
        tags ?? "Unknown tags"
    }

    public var tagsAsArray: [String] {
        if let tags = tags {
            return tags.components(separatedBy: ",")
        } else {
            return [String]()
        }
    }

    public var friendArray: [CachedFriend] {
        let set = friend as? Set<CachedFriend> ?? []

        return set.sorted {
            $0.wrappedName < $1.wrappedName
        }
    }

    public var getAsUser: User {
        var u = User(isActive: isActive, name: wrappedName, age: Int(age), company: wrappedCompany, email: wrappedEmail, address: wrappedAddress, about: wrappedAbout, registered: wrappedRegistered, tags: tagsAsArray, friends: [Friend]())

        u.id = wrappedId

        for f in friendArray {
            u.friends.append(f.getAsFriend)
        }

        return u
    }
}

// MARK: Generated accessors for friend
extension CachedUser {

    @objc(addFriendObject:)
    @NSManaged public func addToFriend(_ value: CachedFriend)

    @objc(removeFriendObject:)
    @NSManaged public func removeFromFriend(_ value: CachedFriend)

    @objc(addFriend:)
    @NSManaged public func addToFriend(_ values: NSSet)

    @objc(removeFriend:)
    @NSManaged public func removeFromFriend(_ values: NSSet)

}

extension CachedUser : Identifiable {

}

Cached Friend Class:

extension CachedFriend {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<CachedFriend> {
        return NSFetchRequest<CachedFriend>(entityName: "CachedFriend")
    }

    @NSManaged public var id: UUID?
    @NSManaged public var name: String?
    @NSManaged public var user: CachedUser?

    public var wrappedId: UUID {
        id ?? UUID()
    }

    public var wrappedName: String {
        name ?? "Uknown Name"
    }

    public var getAsFriend: Friend {
        var f = Friend(name: wrappedName)
        f.id = wrappedId

        return f
    }
}

extension CachedFriend : Identifiable {

}

   

I can't remember what it was now... but I seem to remember running into a similar problem on this project. The error was telling me the ID or name that was causing the problem, and when I actually searched the JSON document for that ID or name, it was actually listed twice as a friend for one user or something like that. Maybe it was one of the tags actually that was listed twice for a user?

I can't remember exactly what it was now. But looking at the data and finding where the duplication actually was gave a me clue as to how to fix it.

   

@Bnerd  

A quick fix if you don't want to search around to find duplicates is to make the below change in your loadData func:

for u in users {
                    var cachedUsers = [CachedUser(context: moc)]
                    let cu = CachedUser(context: moc)
                    cu.id = UUID() //Now each user will get a new unique ID.
                    cu.isActive = u.isActive

   

I figured it out, kind of.

Needed to add this line to container.loadPersistentStores in DataController.

self.container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump

I say "kind of" because while it eliminates the error, clearly not all the data is being saved correctly as when fetched I get back a lot of records with "Unknown Name".

   

I've figured out the problem, but not a great solution. The issue is that I had linked both the cachedUsers array to the context and the individual cu elements that I was then appending to the array. This way, 100 records became 200 records.

The reason I was doing it this way was an attempt to batch the save, so instead of calling save 100 times, I call it once on the array of 100 CachedUser elements.

I tried declaring cu without passing in the context like this:

let cu = CachedUser()

I thought this would keep what would become the elements in the array detached from the context until being appended to cachedUsers but it fails at run time.

I'm sure there's a way to batch save, but I'm just not seeing it yet. I'm also still not clear on why the extra records were all nil.

   

@Legion shares a strategy:

This was an attempt to batch the save, so instead of calling save 100 times,
I call it once on the array of 100 CachedUser elements.

I tend to remember reading that this type of optimization is mostly unnecessary. Swift code is optimized internally to perform similar tasks. (I'll need more time to find a source.)

For example, you may have 100 view structs in your main view. You call code which may cause some of the view structs to redraw themselves. You may think it wise to write code that only identifies the few that actually change, so that all 100 views are not needlessly redrawn. However, SwiftUI is already ahead of you and will actually only update those views (and small parts of the screen) that are updated.

Similarly, I think (Donny Wals?) noted that while your app may "save" dozens of transactions, CoreData efficiently manages the actual write-to-disk strategy to minimize the impact on the system.

I wonder if our CoreData gurus have real world experience with this? Seems to me this is one of the test cases that would be great to expose with Xcode's Instruments application.

   

Hacking with Swift is sponsored by RevenueCat

SPONSORED In-app subscriptions are a pain to implement, hard to test, and full of edge cases. RevenueCat makes it straightforward and reliable so you can get back to building your app. Oh, and it's free if your app makes less than $10k/mo.

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.