UIManagedDocument used in UITabViewController crashes when trying open an existing database - uimanageddocument

I am using Justin Driscoll's article on Core Data with UIManagedDocument in singleton pattern to set it up for UITabViewController. I am running the app on Simulator. Its working fine for the first time. The database is created successfully and I can see data in the tableview controller for each tab. But when I restart my application, app crashes with error
Assertion failure in -[UIManagedDocument openWithCompletionHandler:],
** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason: 'attempt to open or a revert document that already has an open or revert
operation in flight
My code that causes crash. My did some debugging using NSLog statements.
if (![[NSFileManager defaultManager] fileExistsAtPath:[self.document.fileURL path]]) {
NSLog(#"document doesnot exist and hence START CREATING");
[self dataIntoDocument:self.document];
NSLog(#"document Finished Creating");
[self.document saveToURL:self.document.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:OnDocumentDidLoad];
NSLog(#"Saved to URL on disk");
} else if (self.document.documentState == UIDocumentStateClosed) {
NSLog(#"document is closed and its needs to be opened");
[self.document openWithCompletionHandler:OnDocumentDidLoad];
} else if (self.document.documentState == UIDocumentStateNormal) {
NSLog(#"test document is in normal state");
OnDocumentDidLoad(YES);
}
RESULT FROM FIRST RUN When the database doesn't exists
2013-08-12 14:51:35.458 <My APP>[368:11603] document doesnot exist and hence its created Start
2013-08-12 14:51:38.716 <My APP>[368:11603] document doesnot exist and hence its Finished Creating
2013-08-12 14:51:38.718 <My APP>[368:11603] NSManagedContext did save.
2013-08-12 14:51:38.718 <My APP>[368:11603] Saved to URL on disk
2013-08-12 14:51:38.721 <My APP>[368:11603] document is closed and its needs to be opened
2013-08-12 14:51:38.772 <My APP>[368:11603] NSManagedObjects did change
RESULT FROM SECOND RUN: When the database exists at the URL
2013-08-12 14:53:43.758 <MY APP> [380:11603] document is closed and its needs to be opened
2013-08-12 14:53:43.761 <MY APP>[380:11603] document is closed and its needs to be opened
2013-08-12 14:53:43.762
I understand the reason why its failing, as its not supposed to open in close succession. I have same code in all view controllers which control the tabs to get instance of the UIManagedDocument. Please let me know what am I missing. Thanks
-(id) initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (!self.databaseDocument) {
[[LTDatabaseDocumentHandler sharedDatabaseDocumentHandler] performWithDocument:^ (UIManagedDocument *document) {
self.databaseDocument = document;
[self populateTableViewArrayFromDocument:self.databaseDocument];
}];
}
return self;
}

I figured. I need to use -(void)viewWillAppear:animated method to instantiate the shared instance of UIManagedDocument instead of -(id)initWithCoder. As the viewWillAppear method gets executed only when the user tries to view the tab.

I don't understand how this helps you, but at least you figured it out. It just seems like viewWillAppear: may not be the best place to be initializing this kind of stuff.

Related

How to retrieve the XPC service of a file provider extension on macOS?

I have extended my example project from my previous question with an attempt to establish an XPC connection.
In a different project we have successfully implemented the file provider for iOS. The exposed service must be resolved by URLs it is responsible for. On iOS it is the only possibility and on macOS it appears like that, too. Because on macOS the system takes care of managing files there are no URLs except the one which can be resolved through NSFileProviderItemIdentifier.rootContainer.
In the AppDelegate.didFinishLaunching() method I try to retrieve the service like this (see linked code for full reference, I do not want to unnecessarily bloat this question page for now):
let fileManager = FileManager.default
let fileProviderManager = NSFileProviderManager(for: domain)!
fileProviderManager.getUserVisibleURL(for: NSFileProviderItemIdentifier.rootContainer) { url, error in
// [...]
fileManager.getFileProviderServicesForItem(at: url) { list, error in
// list always contains 0 items!
}
}
The delivered list always is empty. However the extension is creating a service source on initialization which creates an NSXPCListener which has an NSXPCListenerDelegate that exports the NSFileProviderReplicatedExtension object on new connections. What am I missing?
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
os_log("XPC listener delegate should accept new connection...")
newConnection.exportedObject = fileProviderExtension
newConnection.exportedInterface = NSXPCInterface(with: SomeProviderServiceInterface.self)
newConnection.remoteObjectInterface = NSXPCInterface(with: SomeProductServiceInterface.self)
newConnection.resume()
return true
}
Suspicious: serviceName of the FileProviderServiceSource never is queried. We are out of ideas why this is not working.
There is a protocol which your extension's principal class can implement, NSFileProviderServicing.
https://developer.apple.com/documentation/fileprovider/nsfileproviderservicing

PFUser currentUser saveInBackgroundWithBlock completes succeeded without even trying

I am trying to alter the logged in user. I make my changes as usual, and I call:
[[PFUser currentUser] saveInBackgroundWithBlock:^(BOOL succeeded, NSError *PF_NULLABLE_S error){
if(succeeded){
NSLog(#"Saved user successfully.");
}else{
NSLog(#"Unable to save user: %#", error);
}
}];
It saves successfully, but my changes are gone. Just before saving, my user objects has this key:
.meta.acceptsAllMessages = 1. The moment save completion block returns, that key is gone. `(meta is my generic JSON object at user, and other values in meta key are retained with no problem). My changes aren't also reflect to the server side too.
The first suspect was the beforeSave trigger, however there's absolutely nothing related to meta keys in my trigger, so that's not the case. Why would this happen?
UPDATE: There seems to be a problem deeper down. I was saving something else entirely, and ran into the same issue. I've enabled airplane mode, and I wanted to save my current user, and it called the completion handler immediately, with succeeded set to YES and error set to nil without an internet connection. I've double checked that I'm using saveInBackgroundWithBlock: and not saveEventually. Why does this happen?
Okay, I've found the solution.
I was adding an object to the array inside my user object, without assigning the property itself. In the latest instance, I was doing something like:
[[PFUser currentUser][#"myArray"] addObject:#"something"];
[[PFUser currentUser] saveInBackground...];
Because I was not assigning any object itself, [my assumption is that] Parse thought that my user object was not dirty, and it completed immediately without even trying to save. I've solved the problem like this:
NSMutableArray *array = [PFUser currentUser][#"myArray"];
[array addObject:#"something"];
[PFUser currentUser][#"myArray"] = array;
The last line is the key. I'm assigning to the "myArray" field of Parse object, which causes Parse to mark my user dirty. Then, when I save, because it is dirty, it actually saves my user to the server, and it works.

Can't register subscriptions; CKError 0x19030580, "Service Unavailable"

Every time when I try to register the subscription, I get the error: CKError 0x19030580: "Service Unavailable" (6/2022); server message = "refused to install an older schema (68f93710-7456-11e4-b13e-008cfac0c800) since we already have 693359e0-7456-11e4-8e42-008cfac03128"; uuid = 42F42F6B-98FB-4774-B735-271C1AEF07F1; container ID = "iCloud.com.*.*". And when I try to get all subscriptions that are on the server, I receive nothing.
Why am I receiving CKError 0x19030580? Why can't I retrieve subscriptions? How should I fix them?
Code:
NSPredicate *truePredicate = [NSPredicate predicateWithValue:YES];
CKSubscription *itemSubscription = [[CKSubscription alloc] initWithRecordType:ItemAssetRecordType
predicate:truePredicate
options:CKSubscriptionOptionsFiresOnRecordCreation | CKSubscriptionOptionsFiresOnRecordUpdate];
CKNotificationInfo *notification = [[CKNotificationInfo alloc] init];
notification.shouldSendContentAvailable = YES;
notification.alertLocalizationKey = #"New Item available.";
notification.shouldBadge = NO;
itemSubscription.notificationInfo = notification;
[self.privateDatabase saveSubscription:itemSubscription completionHandler:^(CKSubscription *subscription, NSError *error) {
if (error) {
} else {
self.subscribedItems = subscription.subscriptionID;
}
}];
I have similar issue. One of my record keep refused to upload. Similar "refused to install an older schema" error came up. First, I thought it's in my code, 2-3 days I try to solve it, but nothing.
Then I try to reset iCloud development container (via iCloud dashboard). Make again the records. Then try to run it again. And it works.
I think it's bug on Apple side. Btw, my affected records not subscription though. But you may want to try it.
Tips, you may want to check if your app already subscribed first before subscribing. I save subscriptionID while subscribe method called in NSUserDefaults. Then I remove it if unsubscribe method called.
I had the same "refused to install an older schema" error but with simple writes, not subscriptions. In my case I was writing a record with a field that did not exist. Now I thought that when under the development environment the schema automatically added new field types - but anyway, when I added it manually using the dashboard it worked.
CloudKit sends CKError.serviceUnavailable when the user has disabled iCloud Drive on their device.

Updating/replacing CoreData sqlite file in app

My app currently have this sqlite file (lets name it v1)
What i want to do is that when i activate a IBAction , it will automatically delete the current file (v1) and retrieve values from webservice and store inside a new file (v2) and the application will then use the new sqlite file (v2)
any idea on how to implement?
my current code is here:
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (persistentStoreCoordinator != nil) {
return persistentStoreCoordinator;
}
NSURL *storeUrl = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory] stringByAppendingPathComponent: #"test.sqlite"]];
NSLog(#"%#",storeUrl);
NSError *error = nil;
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:nil error:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return persistentStoreCoordinator;
}
You can:
download the new database from the server
reset the whole Core Data context (store coordinator, managed object contexts, managed objects)
replace the old database with the new one
ask the UI to refresh to display the newest content: the Core Data context should reload lazily
But this approach is quite dangerous, as you'd have to make sure every Core Data object in your app can be reset. Also, replacing the database should happen just after resetting the context, to avoid a race condition. If you have threads / blocks accessing Core Data this is even more difficult.
A better approach may be to update the database records by downloading a JSON file that contains the new database's contents, delete the old records, and insert the new ones. This also makes sure that you can update your Core Data schema in a future version of the app without breaking your content updating process. And by using the NSFetchResultsController class, your table views or other UI elements may even update automatically with a nice animation as your database gets updated.
If you care about bandwidth, I use protocol buffers which are way more compact than any JSON or even sqlite file would be, and quite easy to use.

How do I share a Core Data store between processes using NSDistributedNotifications?

Background
I've already posted a question about the basics of sharing a Core Data store between processes.
I'm trying to implement the recommendations given and I'm running into problems.
My Goal
I have two processes - the Helper App and the UI. They both share a single data store. I want the UI to update it's NSManagedObjectContext when the Helper App has saved new data to the store.
Current Program Flow
The Helper App Process writes data to the Store.
In the Helper App, I listen for NSManagedObjectContextDidSaveNotification notifications.
When the context is saved, I encode the inserted, deleted and updated objects using their URI representations and NSArchiver.
I send an NSNotification to the NSDistributedNotificationCenter with this encoded dictionary as the userInfo.
The UI Process is listening for the save notification. When it receives the notification, it unarchives the userInfo using NSUnarchiver.
It looks up all the updated/inserted/deleted objects from the URIs given and replaces them with NSManagedObjects.
It constructs an NSNotification with the updated/inserted/deleted objects.
I call mergeChangesFromContextDidSaveNotification: on the Managed Object Context of the UI Process, passing in the NSNotification I constructed in the previous step.
The Problem
Inserted objects are faulted into the UI Managed Object Context fine and they appear in the UI. The problem comes with updated objects. They just don't update.
What I've tried
The most obvious thing to try would
be to pass the save Notification
from the Helper App process to the
UI process. Easy, right? Well, no.
Distributed Notifications won't
allow me to do that as the userInfo
dictionary is not in the right
format. That's why I'm doing all the
NSArchiving stuff.
I've tried calling
refreshObject:mergeChanges:YES on
the NSManagedObjects to be updated,
but this doesn't seem to have any
effect.
I've tried performing the
mergeChangesFromContextDidSaveNotification:
selector on the main thread and the
current thread. Neither seems to
affect the result.
I've tried using
mergeChangesFromContextDidSaveNotification:
before between threads, which of
course is much simpler and it worked
perfectly. But I need this same
functionality between processes.
Alternatives?
Am I missing something here? I'm consistently getting the feeling I'm making this much more complex than it needs to be, but after reading the documentation several times and spending a few solid days on this, I can't see any other way of refreshing the MOC of the UI.
Is there a more elegant way of doing this? Or am I just making a silly mistake somewhere in my code?
The Code
I've tried to make it as readable as possible, but it's still a mess. Sorry.
Helper App Code
-(void)workerThreadObjectContextDidSave:(NSNotification *)saveNotification {
NSMutableDictionary *savedObjectsEncodedURIs = [NSMutableDictionary dictionary];
NSArray *savedObjectKeys = [[saveNotification userInfo] allKeys];
for(NSString *thisSavedObjectKey in savedObjectKeys) {
// This is the set of updated/inserted/deleted NSManagedObjects.
NSSet *thisSavedObjectSet = [[saveNotification userInfo] objectForKey:thisSavedObjectKey];
NSMutableSet *thisSavedObjectSetEncoded = [NSMutableSet set];
for(id thisSavedObject in [thisSavedObjectSet allObjects]) {
// Construct a set of URIs that will be encoded as NSData
NSURL *thisSavedObjectURI = [[(NSManagedObject *)thisSavedObject objectID] URIRepresentation];
[thisSavedObjectSetEncoded addObject:thisSavedObjectURI];
}
// Archive the set of URIs.
[savedObjectsEncodedURIs setObject:[NSArchiver archivedDataWithRootObject:thisSavedObjectSetEncoded] forKey:thisSavedObjectKey];
}
if ([[savedObjectsEncodedURIs allValues] count] > 0) {
// Tell UI process there are new objects that need merging into it's MOC
[[NSDistributedNotificationCenter defaultCenter] postNotificationName:#"com.synapticmishap.lapsus.save" object:#"HelperApp" userInfo:(NSDictionary *)savedObjectsEncodedURIs];
}
}
UI Code
-(void)mergeSavesIntoMOC:(NSNotification *)notification {
NSDictionary *objectsToRefresh = [notification userInfo];
NSMutableDictionary *notificationUserInfo = [NSMutableDictionary dictionary];
NSArray *savedObjectKeys = [[notification userInfo] allKeys];
for(NSString *thisSavedObjectKey in savedObjectKeys) {
// Iterate through all the URIs in the decoded set. For each URI, get the NSManagedObject and add it to a set.
NSSet *thisSavedObjectSetDecoded = [NSUnarchiver unarchiveObjectWithData:[[notification userInfo] objectForKey:thisSavedObjectKey]];
NSMutableSet *savedManagedObjectSet = [NSMutableSet set];
for(NSURL *thisSavedObjectURI in thisSavedObjectSetDecoded) {
NSManagedObject *thisSavedManagedObject = [managedObjectContext objectWithID:[persistentStoreCoordinator managedObjectIDForURIRepresentation:thisSavedObjectURI]];
[savedManagedObjectSet addObject:thisSavedManagedObject];
// If the object is to be updated, refresh the object and merge in changes.
// This doesn't work!
if ([thisSavedObjectKey isEqualToString:NSUpdatedObjectsKey]) {
[managedObjectContext refreshObject:thisSavedManagedObject mergeChanges:YES];
[managedObjectContext save:nil];
}
}
[notificationUserInfo setObject:savedManagedObjectSet forKey:thisSavedObjectKey];
}
// Build a notification suitable for merging changes into MOC.
NSNotification *saveNotification = [NSNotification notificationWithName:#"" object:nil userInfo:(NSDictionary *)notificationUserInfo];
[managedObjectContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:saveNotification
waitUntilDone:YES];
}
I used the method in
http://www.mlsite.net/blog/?p=518
then every object is correctly faulted but the faults are fetch in cache so still no update
I had to do
[moc stalenessInterval = 0];
And it finally worked, with relationship.
You're looking for - (void)refreshObject:(NSManagedObject *)object mergeChanges:(BOOL)flag I believe.
This will refresh the object with the info in the persistent store, merging changes if you want.
I'd go with Mike's suggestion and just watch the store file for changes.
Though it may not be the most efficient, I've had success using - [NSManagedObjectContext reset] from a second process when there's a change to a store. In my case case, the code is fairly linear — all I do is run a fetch request for some data after resetting. I don't know how this will work with bindings and a complicated UI, but you may be able to post a notification to manually update things if it's not handled automatically.
I had this exact same issue with an iPhone app that I've been working on. In my case, the solution involved setting the Context's stalenessInterval to something suitably infinitesimal (e.g., 0.5 seconds).
This works, except for sandboxes apps. You can't send a notification with a user info dict. Instead consider some other IPC like XPC or DO.
On a side note, using NSDustributedNotificationCenter is not always 100% if the system is busy.
Setting stalenessInterval of managed object context works. My case involves multiple threads instead of process though.
Starting with iOS 9, you should now use mergeChangesFromRemoteContextSave:intoContexts:. See this for an explanation: https://www.innoq.com/en/blog/ios-writing-core-data-in-today-extension/

Resources