I'm caching data in CoreData within my app to reduce updating request when there's nothing new.
Here's my caching logic in pseudo code:
if cacheExistsInCoreData {
if cacheIsOutdated {
loadDataFromRemoteAndCacheItWithCurrentDate()
}else {
useCache()
}else {
loadDataFromRemoteAndCacheItWithCurrentDate()
}
How I check if cache is outdated:
func checkIfCacheIsOutdated {
if lastCachedDate isOrderThan selfDefinedCheckingDate {
return true // need to load new data
}else {
return false // just use cache
}
}
This mechanism works fine almost all the time.
While in rare situation I find my program caches the wrong data with a right date, which means user might see the older data and could not get update when new one is available.
If there's nothing wrong with my caching logic, I wonder if the reason could be that when the remote data is fetched by my app before it gets updated and then gets stored in core data with the latest date time.
A cache in core data includes:
data(provided by remote server) //nothing I can do with it...
date(provided by me using NSDate())
How can I make sure if the two objects are correctly connected (latest data with current time) before storing them?
Assuming you are calling a web api, most of the web apis would return cache headers. It is safe to cache the data based on cache header value, that way there wont be any stale cache.
The solution that I came across that works for me is to set the stalenessInterval on the Main Queue NSManagedObjectContext as follows inside of your extension:
objectContext.stalenessInterval = 0.0;
This tells the context in the extension to fetch new data every time and ignore the cache.
Related
I have a dashboard view that I'd like to display a few variables from my last entered CoreData entry. However, I can't figure out how to fetch only the last data entered into a variable so I can display it. Any ideas?
EDIT: I'm trying to setup a NSFetchRequest inside of a called function that is called only onappear. However, I'm getting errors and am lost.
func singleEntryPull() -> [Item] {
let request: NSFetchRequest<Item> = Item.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "todaysDate", ascending: false)]
request.fetchLimit = 1
let singleEntry = FetchRequest(fetchRequest: request)
return singleEntry
}
And then the return from the function should only show 1 result and I can then use the returned value to display the variables I need?
Well, not sure if this is the cleanest or best way to do this but I got it working like I want for now until a better solution comes up. I'm still using #FetchRequest which im now aware is pulling data live and updating it, but that might work as if someone keeps the app open overnight and updates it in the morning, I'd want it to display that latest entry. I used this:
ForEach(singleEntry.prefix(1)) { item in
A fetch limit of 1 on a fetch request will return you a single value. However, when you're setting up a #FetchRequest, you're doing more than this - you're making the initial fetch and then continuing to monitor the context for changes, so it live updates. This monitoring only uses the predicate of your fetch request.
Depending on your order of operations, you could be seeing the latest data, and then any new data inserted since you started that view. My experiments with the SwiftUI core data template project prove this out - on initial run you get the a single latest entry, but as you add newer ones, the fetch-limited screen picks up the new entries.
Depending on how this view is actually used, you have two choices - you can do an actual fetch request on appear of the view and store the result as an observable object, or you can make sure you only ever use the first record from the fetch request's results array, which will always be the latest record because of your sort ordering:
var body: some View {
if let latest = singleEntry.first {
// Some view describing the latest entry
} else {
Text("No record")
}
}
I have an iOS app structured like this
Main Application (the main iOS app)
Intents Extension (Siri integration)
Shared Framework (shared library for interacting with Core Data. This allows both the main application and the intents extension to use the same Core Data store)
My issue is that when I insert something into Core Data using the Intents Extension, it doesn't appear in the Main Application's UITableView until I manually refresh the fetchedResultsController like this:
NSFetchedResultsController<NSFetchRequestResult>.deleteCache(withName: "myCache")
try? fetchedResultsController.performFetch()
tableView.reloadData()
Is there a way to make the fetchedResultsController see the changes without having to manually refresh everything?
Note: If I insert something into core data from the Main Application, the fetchedResultsController automatically sees the change and updates the table (like expected)
To share a database between an app and extension you need to implement Persistent History Tracking. For an introduction see WWDC 2017 What's New in Core Data at 20:49 and for sample code see the documentation Consuming Relevant Store Changes.
The basic idea is to enable the store option NSPersistentHistoryTrackingKey, observe NSPersistentStoreRemoteChangeNotification, upon being notified you should fetch the changes using NSPersistentHistoryChangeRequest and then merge into the context using mergeChangesFromContextDidSaveNotification and transaction.objectIDNotification. Your NSFetchedResultsController will then update accordingly.
This is normal because the application extension and the main application are not working in the same process.
There are some ways to update the data in the main application
NSPersistentStoreRemoteChangeNotification
UserDefaults(suitename:)
Darwin Notifications
I'm using UserDefaults and refreshAllObjects function for the viewContext.
Example:
func sceneDidBecomeActive(_ scene: UIScene) {
let defaults = UserDefaults(suiteName:"your app group name")
let hasChange = defaults?.bool(forKey: "changes")
if hasChange ?? false {
refreshAllObjects()
defaults?.set(false, forKey: "changes")
}
}
refresh all objects function is like this:
viewContext.perform {
viewContext.stalenessInterval = 0.0
viewContext.refreshAllObjects()
viewContext.stalenessInterval = -1
}
I have a simple app that stores an array of my custom type (instances of a class called Drug) using NSCoding in the app’s documents folder.
The loading and saving code is an extension to my main view controller, which always exists once it is loaded.
Initialisation of array:
var drugs = [Drug]()
This array is then appended with the result of the loadDrugs() method below.
func saveDrugs() {
// Save to app container
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.ArchiveURL.path)
// Save to shared container (for iMessage, Spotlight, widget)
let isSuccessfulSaveToSharedContainer = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.SharedArchiveURL.path)
}
Here is the code for loading data.
func loadDrugs() -> [Drug]? {
var appContainerDrugs = NSKeyedUnarchiver.unarchiveObject(withFile: Drug.ArchiveURL.path) as? [Drug]
return appContainerDrugs
}
Data is also stored in iCloud using CloudKit and the app can respond to CK notifications to fetch changes from another device. Background fetch also triggers this same method.
// App Delegate
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
// Code to get reference to my main view controller
// This will have called loadDrugs() to populate local array drugs of type [Drug]
mainVC.getZoneChanges()
}
Finally, there is the getZoneChanges() method, which uses a stored CKServerChangeToken to get the changes from the private user database with CKFetchRecordZoneChangesOperation. The completion block calls saveDrugs().
The problem
All of this seems to work fine. However, sometimes all local data disappears between uses of the app, especially if it has not been used for some time. Deleting and reinstalling the app does pull the backed-up data from iCloud thankfully.
It seems to happen if the app has not been used for a while (presumably terminated by the system). Something has to have changed, so I presume it is the calling of a background fetch when the app is terminated that may be the problem. Everything works fine while debugging and when the app has been in foreground recently.
Possible causes
I’m guessing the problem is that I depend on background fetch (or receiving a CK notification) loading my main view controller in the background and then loading saved local data.
I have heard that UserDefaults does not work correctly in the background and there can be file security protections against accessing the documents directory in this context. If this is the case here, I could be loading an empty array (or rather initialising the array and not appending the data to it) and then saving it, overwriting existing data, all without the user knowing.
How can I circumvent this problem? Is there a way to check if the data is being loaded correctly? I tried making a conditional load with a fatal error if there is a problem, but this causes problems on the first run of the app as there is no data anyway!
Edit
The archive URLs are obtained dynamically as shown below. I just use a static method in my main data model class (Drug) to access them:
static let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("drugs")
The most common cause of this kind of issue is being awakened in the background when the device is locked and protected data are encrypted.
As a starting point, you can check UIApplication.isProtectedDataAvailable to verify that protected data is available. You can also lower the protection levels of data you require to .completeUntilFirstUserAuthentication (the specifics of how to do that depends on the how you create your files).
As a rule, you should minimize reducing data protection levels on sensitive information, so it's often best to write to some other location while the device is locked, and then merge that once the device is unlocked.
The problem is that you are storing the full path of the file and not just the fileName (or relative path). The document directory URL can change, and then if you stored that URL persistently you will not be pointing to the correct location of the file. Instead just store the filename and use NSFileManager URLsForDirectory to get the documents directly every time you need it.
I need to observe changes of an Entity after import occurred.
Currently I have next logic:
Save Entity with temp identifier (NSManagedObject.objectId) to local core data storage.
Send Entity to the server via Alamofire POST request.
Server generates JSON and reply with the almost the same Entity details but with modified identifier which was NSManagedObject.objectId previously. So the local one Entity id will be updated with server id.
Now when I received new JSON I do transaction.importUniqueObjects.
At this step I want to inform my datasource about changes. And refetch data with updated identifiers.
So my DataSource has some Entities in an array, and while I use this datasource to show data it's still static information in that array which I fetched before, but as you see on the step number 4 I already updated core data storage via CoreStore import and want DataSource's array to be updated too.
I found some information regarding ListMonitor in CoreStore and tried to use it. As I can see this method works when update comes
func listMonitorDidChange(_ monitor: ListMonitor)
but I try to refetch data somehow. Looks like monitor already contains some most up to date info.
but when I do this:
func listMonitorDidChange(_ monitor: ListMonitor<MyEntity>) {
let entities = try? CoreStore.fetchAll(
From<MyEntity>()
.orderBy(.ascending(\.name))
) // THERE IS STILL old information in database, but monitor instance shows new info.
}
And then code became like this:
func listMonitorDidChange(_ monitor: ListMonitor<MyEntity>) {
var myEntitiesFromMonitor = [MyEntity]()
for index in 0...monitor.numberOfObjects() {
myEntitiesFromMonitor.append(monitor[index])
}
if myEntitiesFromMonitor.count > 0 {
// HERE we update DataSource
updateData(with: myEntitiesFromMonitor)
}
}
not sure if I am on the right way.
Please correct me if I am wrong:
As I understood each time core data gets updated with new changes, monitor gets updated as well. I have not dive deep into it how this was made, via some CoreData context notification or whatever but after you do something via CoreStore transaction, such as create or update or delete object or whatever you want, monitor gets update. Also it has callback functions that you need to implement in your class where you want to observe any changes with data model:
Your classes such as datasource or some service or even some view controller (if you don't use any MVVP or VIPER or other design patterns) need to conform to ListObserver protocol in case you want to listen not to just one object.
here are that functions:
func listMonitorDidChange(monitor: ListMonitor<MyPersonEntity>) {
// Here I reload my tableview and this monitor already has all needed info about sections and rows depend how you setup monitor.
// So you classVariableMonitor which I provide below already has up to date state after any changes with data.
}
func listMonitorDidRefetch(monitor: ListMonitor<MyPersonEntity>) {
// Not sure for which purposes it. I have not received this call yet
}
typealias ListEntityType = ExerciseEntity
let classVariableMonitor = CoreStore.monitorSectionedList(
From<ListEntityType>()
.sectionBy(#keyPath(ListEntityType.muscle.name)) { (sectionName) -> String? in
"\(String(describing: sectionName)) years old"
}
.orderBy(.ascending(\.name))
.where(
format: "%K == %#",
#keyPath(ListEntityType.name),
"Search string")
)
All other thing documented here so you can find info how to extract info from monitor in your tableview datasource function.
Thanks #MartinM for suggestion!
In my app, I have a screen where the user can edit an object's attributes. The screen has 2 actions: save or revert.
When revert is selected, I want to revert the object's attributes to whatever is stored on the server and discard all local changes. I do this with the the fetch method:
println("1: \(localObject)")
if localObject.isDirty() {
localObject.fetchInBackgroundWithBlock { serverObject, error in
println("2: \(localObject)")
println("3: \(serverObject)")
}
}
isDirty() works correctly and only returns true if something was locally modified and not saved.
According to the docs, fetch: fetches the PFObject with the current data from the server. But what I see in the log is that all 3 print statements show the same local attributes. I would expect serverObject to be whatever is stored on the server, but it's not, serverObject also shows the locally modified attributes.
I'm not using the Local Datastore.
What is going on? Is this a bug with the Parse SDK? Or is there a caching issue?
If you call fetch without using the local datastore, Parse will give you a error in the console.
Since you're not getting an error from Parse when you call fetchInBackgroundWithBlock, it means that at some point you've called localObject.pinInBackground() and placed that object in the local datastore.
For locally stored objects, you can call revert() before fetchInBackgroundWithBlock to discard local changes before updating data from the server.
if localObject.isDirty() {
localObject.revert()
localObject.fetchInBackgroundWithBlock { serverObject, error in
println("2: \(localObject)")
println("3: \(serverObject)")
}
}
I haven't seen the docs for fetch, but I do know that you can accomplish what you're trying just by performing a query. Doing the following will query from the server (although you can adjust the cache policy if you've already used the query to look at cache or network first):
var query = PFQuery(className:"GameScore")
query.getObjectInBackgroundWithId("xWMyZEGZ") {
(gameScore: PFObject?, error: NSError?) -> Void in
if error == nil && gameScore != nil {
println(gameScore)
}else {
println(error)
}
}
Alternatively, you could just save a copy of the local object before any changes can be made, and then if it is dirty, you can just replace it with the copy. That's probably what I'd do to reduce the number of parse API calls.