How to store and load data properly with CoreData?

I am a beginner and never worked close with CoreData. I have a JSON response, which results need to be shown in a Table View. I want implement a CoreData to my project.

JSON parsing (in Separate Swift file)

 func parseJSON(with currencyData: Data){
    let decoder = JSONDecoder()

    do {
        let decodedData = try decoder.decode(CurrencyData.self, from: currencyData)
        
        for valute in decodedData.Valute.values {
            if valute.CharCode != "XDR" {
                 let currency = Currency(context: self.context)
                currency.shortName = valute.CharCode
                currency.currentValue = valute.Value
            }
        }

         do {
             try context.save()
         } catch {
             print("Error saving context, \(error)")
         }
    } catch {
        self.delegate?.didFailWithError(self, error: error)
        return
    }
}

And in my VC I want to load it in my tableView which takes data from currencyArray:

    func loadCurrency() {
    let request: NSFetchRequest<Currency> = Currency.fetchRequest()
    do {
        currencyArray = try context.fetch(request)
    } catch {
        print(error)
    }
    tableView.reloadData()
}

I start the parsing and load currency data in my VC:

 override func viewDidLoad() {
    super.viewDidLoad()
    currencyNetworking.performRequest()
    loadCurrency()
}

But as a result when app first launch my tableView is empty. Second launch - I have doubled data. Maybe this is because loadCurrency() starts before performRequest() able to receive data from JSON and save it to context.

I tried also to not save context in parseJSON() but first through delegate method send it to VC and perform save to context and load from context from here. Then tableView loads from the first launch. But then every time app starts in my CoreData database I see an increase of the same data (33, 66, 99 lines).

My goal is to save parsed data to CoreData once (either where I parseJSON or in VC) and change only currentValue attribute when user want to update it.

How to make it correct?


Solution 1:

You need to first choose which is your source of truth where you are showing data from. In your case it seems to be your local database which is filled from some external source.

If you wish to use your local database with data provided from remote server then you need to have some information to keep track of "same" entries. This is most usually achieved by using an id, an identifier which is unique between entries and persistent over changes in entry. Any other property may be used that corresponds to those rules. For instance CharCode may be sufficient in your case if there will always be only one of them.

Next to that you may need a deleted flag which means that you also get deleted items from server and when you find entry with deleted flag set to true you need to delete this object.

Now your pseudo code when getting items from server you should do:

func processEntries(_ remoteEntries: [CurrencyEntry]) {
    remoteEntries.forEach { remoteEntry in
        if remoteEntry.isDeleted {
            if let existingLocalEntry = database.currencyWithID(remoteEntry.id) {
                existingLocalEntry.deleteFromDatabase()
            }
        } else {
            if let existingLocalEntry = database.currencyWithID(remoteEntry.id) {
                database.updateCurrency(existingLocalEntry, withRemoteEntry: remoteEntry)
            } else {
                database.createCurrency(fromRemoteEntry: remoteEntry)
            }
        }
    }
}

And this is just for synchronization approach.


Now going to your view controller. When it appears you call to reload data from server and at the same time display whatever is in your database. This is all fine but you are missing a re-display once new data is available.

What you optimally need is another hook where your view controller will be notified when database has changes. So best thing to do is add event when your database changes which is whenever you save your database. You probably looking at something like this:

extension Database {
    
    func save() {
        guard context.hasChanges else { return }
        try? context.save()
        self.notifyListenersOfChangesInDatabase()
    }

and this method would be called within your parseJSON and everywhere else that you save your database. Then you would add your view controller as a listener on your database as

override func viewDidLoad() {
    super.viewDidLoad()
    database.addListener(self)
    currencyNetworking.performRequest()
    loadCurrency()
}

How exactly will listener and database be connected is up to you. I would suggest using delegate approach but it would mean a bit more work. So another simpler approach is using NotificationCenter which should have a lot of examples online, including on StackOverflow. The result will be

  • Instead of database.addListener(self) you have NotificationCenter.default.addObserver...
  • Instead of notifyListenersOfChangesInDatabase you use NotificationCenter.default.post...

In any of the cases you get a method in your view controller which is triggered whenever your database is changed. And in that method you call loadCurrency. This is pretty amazing because now you don't care who updated the data in your database, why and when. Whenever the change occurs you reload your data and user sees changes. For instance you could have a timer that pools for new data every few minutes and everything would just work without you needing to change anything.


The other approach you can do is simply add a closure to your performRequest method which triggers as soon as your request is done. This way your code should be like

override func viewDidLoad() {
    super.viewDidLoad()
    
    loadCurrency()
    currencyNetworking.performRequest {
        self.loadCurrency()
    }        
}

note that loadCurrency is called twice. It means that we first want to show whatever is stored in local database so that user sees his data more or less instantly. At the same time we send out a request to remote server which may take a while. And once server response finished processing a reload is done again and user can view updated data.

Your method performRequest could then look like something as the following:

func performRequest(_ completion: @escaping () -> Void) {
    getDataFromRemoteServer { rawData in
        parseJSON(with: rawData)
        completion()
    }
}