Alexander Farber
Alexander Farber

Reputation: 22958

Fetching, parsing JSON objects, then saving to Core Data produces duplicated records

Being a Swift newbie I am trying to fetch a list of JSON objects, save them to Core Data, then display in a SwiftUI List:

screenshot

The JSON objects have a unique numeric uid (acting as PRIMARY KEY in my PostgreSQL backend).

I map them to id in my TopModel.swift:

struct Top: Codable, Identifiable {
    var id: Int { uid }
    let uid: Int
    let elo: Int
    let given: String
    let photo: String?
    let motto: String?
    let avg_score: Double?
    let avg_time: String?
}

Then I have a singleton DownloadManager.swift, which downloads the JSON list and stores the objects into the Core Data:

func getTopsCombine() {
    guard let url = URL(string: "https://wordsbyfarber.com/ru/top-all") else { return }
    URLSession.shared.dataTaskPublisher(for: url)
        .subscribe(on: DispatchQueue.global(qos: .background))
        .receive(on: DispatchQueue.main)
        .tryMap(handleOutput)
        .decode(type: TopResponse.self, decoder: JSONDecoder())
        .sink{ ( completion ) in
            print(completion)
        } receiveValue: { [weak self] (returnedTops) in
            for top in returnedTops.data {
                print(top)
                let topEntity = TopEntity(context: PersistenceController.shared.container.viewContext)
                topEntity.uid = Int32(top.id)
                topEntity.elo = Int32(top.elo)
                topEntity.given = top.given
                topEntity.motto = top.motto
                topEntity.photo = top.photo
                topEntity.avg_score = top.avg_score ?? 0.0
                topEntity.avg_time = top.avg_time
            }
            self?.save()
        }
        .store(in: &cancellables)
}

Unfortunately, this results in multiple identical records being saved in the Core Data (and as you can see in the above screenshot).

What is the standard iOS solution here?

How to enforce the uniqueness (in my original Android/Room app I use @PrimaryKey public int uid; for that)? I do not see such an option in Xcode:

Xcode screenshot

And how to merge fetched list of JSON objects (in my Android app I use DiffUtils)?

Upvotes: 3

Views: 1240

Answers (2)

vadian
vadian

Reputation: 285059

In Core Data you have to check if the entry already exists, create a predicate and fetch the objects with the unique id. If there is no item create one.

for top in returnedTops.data {
    let context = PersistenceController.shared.container.viewContext
    let request : FetchRequest<TopEntity> = TopEntity.fetchRequest()
    request.predicate = NSPredicate(format: "uid == %ld", top.id)
    if try? context.fetch(request)?.first == nil {
        print(top)            
        let topEntity = TopEntity(context: context)
        topEntity.uid = Int32(top.id)
        topEntity.elo = Int32(top.elo)
        topEntity.given = top.given
        topEntity.motto = top.motto
        topEntity.photo = top.photo
        topEntity.avg_score = top.avg_score ?? 0.0
        topEntity.avg_time = top.avg_time
    }
}

You can even update the item if it exists

for top in returnedTops.data {
    let context = PersistenceController.shared.container.viewContext
    let request : FetchRequest<TopEntity> = TopEntity.fetchRequest()
    request.predicate = NSPredicate(format: "uid == %ld", top.id)
    let topEntity : TopEntity
    if let result = try? context.fetch(request)?.first {
       topEntity = result
    } else {
        print(top)            
        topEntity = TopEntity(context: context)
    }
    topEntity.uid = Int32(top.id)
    topEntity.elo = Int32(top.elo)
    topEntity.given = top.given
    topEntity.motto = top.motto
    topEntity.photo = top.photo
    topEntity.avg_score = top.avg_score ?? 0.0
    topEntity.avg_time = top.avg_time
    
}

Upvotes: 1

Yrb
Yrb

Reputation: 9665

If you are NOT using CloudKit, you can enforce a unique constraint on Core Data. If you are, you will have to make the comparison yourself based off the Id.

For using unique constraints, you simply add them in to the "Constraints" section of the Data Model Inspector for the entity constrained. Note this image shows the prior icon for the inspector, but it is still the far right icon:

Constraints in Data Model Inspector

If you are planning on using CloudKit, you can't use constraints because they are not allowed in CloudKit. Therefore, you need to write a function that uniques your data and call it after the data has been written to storage. It sounds like since you are cross platforming that you won't be using CloudKit, so the above should work fine.

Upvotes: 1

Related Questions