NSFetchedResultsController not seeing inserts made by extension - ios

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
}

Related

Intermittent data loss with background fetch - could NSKeyedUnarchiver return nil from the documents directory?

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.

CoreStore how to observe changes in database

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!

How to prevent using cache when new data is available?

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.

Background Seeding/Loading iOS Core Data Store

My iOS 8.0 + app is essentially a dictionary app, presenting a read-only data set to the user in an indexed, easily navigable format. I have explored several strategies for loading the static data, and I have decided to ship the app with several JSON data files that are serialized and loaded into a Core Data store once when the app is first opened. The call to managedObjectContext.save(), therefore, will happen only once in the lifetime of the app, on first use.
From reading Apple's Core Data Programming Guide in the Mac Developer Library (updated Sept. 2015), I understand that Apple's recommended practice is to 1) separate the Core Data stack from the AppDelegate into a dedicated DataController object (which makes it seem odd that even in Xcode 7.2 the Core Data stack is still put in the AppDelegate by default, but anyway...); and
2) open (and, I assume, seed/load) the persistent store in a background thread with a dispatch_async block, like so :
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
//(get URL to persistent store here)
do {
try psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil)
//presumably load the store from serialized JSON files here?
} catch { fatalError("Error migrating store: \(error)") }
}
I'm just getting started learning about concurrency and GCD, so my questions are basic:
1) If the data set is being loaded in a background thread, which could take some non-trivial time to complete, how does the initial view controller know when the data is finished loading so that it can fetch data from the ManagedObjectContext to display in a UITableView ?
2) Along similar lines, if I would like to test the completely loaded data set by running some fetches and printing debug text to the console, how will I know when the background process is finished and it's safe to start querying?
Thanks!
p.s. I am developing in swift, so any swift-specific tips would be tremendous.
Instead of trying to make your app import the read-only data on first launch (forcing the user to wait while the data is imported), you can import the data yourself, then add the read-only .sqlite file and data model to your app target, to be copied to the app bundle.
For the import, specify that the persistent store should use the rollback journaling option, since write-ahead logging is not recommended for read-only stores:
let importStoreOptions: [NSObject: AnyObject] = [
NSSQLitePragmasOption: ["journal_mode": "DELETE"],]
In the actual app, also specify that the bundled persistent store should use the read-only option:
let readOnlyStoreOptions: [NSObject: AnyObject] = [
NSReadOnlyPersistentStoreOption: true,
NSSQLitePragmasOption: ["journal_mode": "DELETE"],]
Since the bundled persistent store is read-only, it can be accessed directly from the app bundle, and would not even need to be copied from the bundle to a user directory.
Leaving aside whether loading a JSON at the first startup is the best option and that this question is four years old, the solution to your two questions is probably using notifications. They work from all threads and every listening class instance will be notified. Plus, you only need to add two lines:
The listener (your view controller or test class for question 2) needs to listen for notifications of a specific notification name:
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.handleMySeedNotification(_:)), name: "com.yourwebsite.MyCustomSeedNotificationName", object: nil)
where #objc func handleMySeedNotification(_ notification: Notification) is the function where you are going to implement whatever should happen when a notification is received.
The caller (your database logic) the sends the notification on successful data import. This looks like this:
NotificationCenter.default.post(name: "com.yourwebsite.MyCustomSeedNotificationName", object: nil)
This is enough. I personally like to use an extension to Notification.Name in order to access the names faster and to prevent typos. This is optional, but works like this:
extension Notification.Name {
static let MyCustomName1 = Notification.Name("com.yourwebsite.MyCustomSeedNotificationName1")
static let MyCustomName2 = Notification.Name("CustomNotificationName2")
}
Using them now becomes as easy as this: NotificationCenter.default.post(name: .MyCustomSeedNotificationName1, object: nil) and even has code-completion after typing the dot!

Notification of changes in Core Data SQLite store between iPhone and Apple Watch app

I have an iPhone (iOS 8) and Apple Watch (watchOS 1) apps that share their data using Core Data (SQLite store, placed in shared app group). Both apps are using the same data access code that is placed in shared framework. NSPersistentStoreCoordinator is being set up in the following way:
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
let sharedContainerURL = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier(self.sharedAppGroup)!
let storeURL = sharedContainerURL.URLByAppendingPathComponent(self.databaseName)
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
var error: NSError? = nil
if coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil, error: &error) == nil {
fatalError("Unable to add persistent store: \(error)")
}
return coordinator
}()
From my understanding, in runtime each app has its own NSPersistenStoreCoordinator instance (as iPhone apps and WatchKit extensions do have completely separate address space), but these two connect to exactly the same SQLite database file.
How can an iPhone app be notified when Watch app changes some data in the common SQLite store and the other way around: how can a Watch app be notified when the iPhone app changes some data in the common persistent store?
The solution that I've found quite satisfactory was to use MMWormhole library.
It works by using CFNotificationCenter Darwin Notifications and writing/reading data files in the shared app group, which results in instant communication between an iOS app and an app extension (Watch app, today's widget, etc).
Basic code goes like this:
Wormhole initialization
wormhole = MMWormhole(applicationGroupIdentifier: appGroup, optionalDirectory: nil)
Passing data object to a wormhole
let payload = ["Key": "Value"]
wormhole.passMessageObject(payload, identifier: theSameMessageIdentifier)
Listening for incoming object from a wormhole
wormhole.listenForMessageWithIdentifier(theSameMessageIdentifier) { message -> Void in
if let payloadDictionary = message as? Dictionary<String, String> {
// Do some work
}
}
It's as simple as that.
Not easily. There is no way to send a direct communication between the two applications.
My current recommendation in this regard is to use files on disk that include the objectIDs of anything that has changed from one app to the other.
When you detect a save to disk you write a file that, for example, is JSON and includes up to three arrays: update, insert, delete. The file name should be some form of timestamp.
Separately you should be watching the directory for any files created by the other app and consume them. Load the ObjectIDs and then create a notification out of the ala iCloud or in iOS 9 a remote notification. Then delete the file after process.
On launch, remove all files from the other store since you will automatically be aware of anything that happened pre-launch.
Not simple but fairly straight forward.

Resources