iOS: Storing data in memory rather than on disk - ios

I'm looking for a tutorial on how to store sensitive data in memory rather than on disk for iOS (10+). I've googled but nothing really relevant has come up.
I'm familiar with most data storage options for iOS, SQLite, Plist, Core Data, User Defaults and Keychain. I know Core Data has an in-memory persistent store option but am not sure how to designate that as the one I want to use. Looking at the Apple docs and other tutorials I've only seen the instantiation of a persistent store but not declaring whether it was to be sqlite or core data or in-memory.
For example, Apple's docs on the Core Data stack:
import UIKit
import CoreData
class DataController: NSObject {
var managedObjectContext: NSManagedObjectContext
init(completionClosure: #escaping () -> ()) {
persistentContainer = NSPersistentContainer(name: "DataModel")
persistentContainer.loadPersistentStores() { (description, error) in
if let error = error {
fatalError("Failed to load Core Data stack: \(error)")
}
completionClosure()
}
}
}
This question seems to point in the right direction (just the code initially posted)
Save In-Memory store to a file on iOS
- (NSPersistentStore *)addPersistentStoreWithType:(NSString *)storeType configuration:(NSString *)configuration URL:(NSURL *)storeURL options:(NSDictionary *)options error:(NSError **)error;
Is it just that, passing a type? And to follow-up, once the app closes, the in-memory data is released?
Thanks

Since you're using NSPersistentContainer, you tell Core Data what kind of store to use with an instance of NSPersistentStoreDescription. It has a property called type that accepts values like NSInMemoryStoreType. Set up a description and then assign that to the container's persistentStoreDescriptions property, and you'll get an in-memory store. The method you mention would work but would require changing your Core Data setup to drop NSPersistentContainer.
It exists, as the name implies, only in memory, so anything stored there disappears when the app exits.

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.

Xcode 8 Core Data App Crash After Change of Project Name

I changed the name of my Xcode 8 project how described in this post How do I completely rename an Xcode project (i.e. inclusive of folders)?
Now when running the new version of the app on a device where the old version is installed the app crashes with the following error message:
[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class
(OLD_PROJECTNAME.SomeObject) for key (NS.objects); the class may be
defined in source code or a library that is not linked
So it seems that the old name doesn't fit to the core data stack of the renamed version.
How can I change this name to make the app executable?
EDIT:
This is the core data initialization code inside my AppDelegate.Swift:
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "Data")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
It's a production app and the user should not lose any data after the update.
You mentioned in a comment that you have some transformable attributes. This is consistent with your error message, since [NSKeyedUnarchiver decodeObjectForKey:] is part of NSCoding, and since Core Data uses NSCoding with transformable attributes.
This isn't a Core Data problem. You'd get the same error if you used NSCoding on the values of those attributes and wrote the result to a file. In Swift, the full name of a class is something like AppName.ClassName. If AppName doesn't match, NSCoding can't decode the object. You saved objects with a name like OLD_PROJECTNAME.SomeObject, but now your class name is something like NEW_PROJECTNAME.SomeObject. NSCoding can't handle that on its own. You need to help it.
To fix this, you need to tell the archiving system what class to use. You can do that with the NSKeyedUnarchiver class method setClass(_:forClassName:). You might have to experiment a little to get the syntax right but you'll have something like
NSKeyedUnarchiver.setClass(SomeClass.self, forClassName:"OLD_PROJECT_NAME.SomeObject")
You must do this before you attempt to create any objects that use this class so that it will affect how those objects are created. Since you're using Core Data that means before doing anything at all that touches Core Data in any way.

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.

WatchKit Core Data Sync Up

I have an app structured as follows
iOS App Writes data to Core Data which has a persistent store stored in a shared app group.
The Watch Kit extension is able to read data from Core Data that was written by the iOS app.
The issue I am having is if my iOS app writes data while my watch kit app is open I am not getting updates because the object context is not syncing with the data on the disk.
Is there a way that since my watch kit extension is only reading data to be able to refresh the context and force it to load again from the data on the disk?
My working solution was using MMWormhole to send notification (NSManagedObjectContextDidSaveNotification) from iPhone app to my watch app. In the watch app's controller i used mergeChangesFromContextDidSaveNotification: method of NSManagedObjectContext.
// in iPhone app's notification handler
MMWormhole *wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:#"your.group.container.identifier" optionalDirectory:nil];
[wormhole passMessageObject:notification identifier:#"your notification identifier"];
// in WKInterfaceController's awakeWithContext: method
MMWormhole *wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:#"your.group.container.identifier" optionalDirectory:nil];
[wormhole listenForMessageWithIdentifier:#"your notification identifier" listener:^(id messageObject) {
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:messageObject];
}];
Then NSFetchedResultsController did all other work with UI updates.
You must implement initWithCoder: and encodeWithCoder: methods from NSCoding protocol for your NSManagedObject subclasses because MMWormhole uses NSKeyedArchiver as a serialization medium.
- (id)initWithCoder:(NSCoder *)decoder {
NSManagedObjectContext *context = ... // use your NSManagedObjectContext
NSPersistentStoreCoordinator *coordinator = ...; //use your NSPersistentStoreCoordinator
NSURL *url = (NSURL *)[decoder decodeObjectForKey:#"URIRepresentation"];
NSManagedObjectID *managedObjectID = [coordinator managedObjectIDForURIRepresentation:url];
self = [context existingObjectWithID:managedObjectID error:nil];
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:[[self objectID] URIRepresentation] forKey:#"URIRepresentation"];
}
I ran into the same issue. I used - (void)refreshObject:(NSManagedObject *)object mergeChanges:(BOOL)flag in the NSManagedObjectContext to get the latest data for the managed object.
Ran into similar issues. Despite creating a shared Fetched Results Controller in the App Group observing Managed Object Context changes and refreshing Managed Object Context were not feasible.
Managed Object Contexts cache a certain level of the object graph for retrieval without reading from the actual SQLite store on the disk. The only potential way to actually get live sync between the two would be sending messages across iOS app to Extension when the MOC changes and destroying/rebuilding the Core Data stack from the disk every single time, not a great solution at all.
I believe the use case for live sync between iOS and Extension at initial launch isn't a necessity. Hopefully we get a more deliberate solution to this problem in a future release.

Resources