I used as described in the Apple Docs NSMetadataQuery to search my iCloud file. I have only one file and I know its name. My problem is that sometimes this file doesn't exist (I guess because it has not yet been downloaded) and NSMetadataQuery is unable to find it.
Ever tried to force download with NSFileManager startDownloadingUbiquitousItemAtURL:error: and it returns me an error. (Read EDIT)
My solution is that I created the file the first time, then I guess it exists and I open it with UIDocument. But It couldn't exist or it could be the first time the user opens the app. I can't be sure of these things. My first question is: if UIDocument opens the file, it means that it found the file somewhere. How could it use the file if it DOESN'T EXIST?
And then, second question: If I app which has to manage multiple files or files with unknown name. How can I find them if NSMetadataQuery doesn't work.
EDIT:
if startDownloadingUbiquitousItemAtURL should be used to start downloading a file, how can I know when the file finished downloading (perhaps with a notification)? But, a more important thing: How can I download the file if is always says (removed original names)?
Error Domain=NSPOSIXErrorDomain Code=2 "The operation couldn’t be completed.
No such file or directory" UserInfo=0x166cb0 {
NSDescription=Unable to get real path for Path
'/private/var/mobile/Library/Mobile Documents/teamid~com~team~app/Documents/file.extension'
}
I suggest the following load routine for iCloud files. It involves 4 steps:
First, testing if iCloud is accessible
then look for your files (either look for a specific file like you indicated or for all files with a certain extension like *.txt, or if you don't really know what file extension you are looking for, something like NSPredicate *pred = [NSPredicate predicateWithFormat:#"NOT %K.pathExtension = '.'", NSMetadataItemFSNameKey]; will return all files which have an extension, like jpg, txt, dat etc.)
then checking if the query is done, and
finally attempt to load the file. If the file doesn't exist, create it. If it does exist, load it.
Here is the code that exemplifies these four steps:
- (void)loadData:(NSMetadataQuery *)query {
// (4) iCloud: the heart of the load mechanism: if texts was found, open it and put it into _document; if not create it an then put it into _document
if ([query resultCount] == 1) {
// found the file in iCloud
NSMetadataItem *item = [query resultAtIndex:0];
NSURL *url = [item valueForAttribute:NSMetadataItemURLKey];
MyTextDocument *doc = [[MyTextDocument alloc] initWithFileURL:url];
//_document = doc;
doc.delegate = self.viewController;
self.viewController.document = doc;
[doc openWithCompletionHandler:^(BOOL success) {
if (success) {
NSLog(#"AppDelegate: existing document opened from iCloud");
} else {
NSLog(#"AppDelegate: existing document failed to open from iCloud");
}
}];
} else {
// Nothing in iCloud: create a container for file and give it URL
NSLog(#"AppDelegate: ocument not found in iCloud.");
NSURL *ubiq = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
NSURL *ubiquitousPackage = [[ubiq URLByAppendingPathComponent:#"Documents"] URLByAppendingPathComponent:#"text.txt"];
MyTextDocument *doc = [[MyTextDocument alloc] initWithFileURL:ubiquitousPackage];
//_document = doc;
doc.delegate = self.viewController;
self.viewController.document = doc;
[doc saveToURL:[doc fileURL] forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
NSLog(#"AppDelegate: new document save to iCloud");
[doc openWithCompletionHandler:^(BOOL success) {
NSLog(#"AppDelegate: new document opened from iCloud");
}];
}];
}
}
- (void)queryDidFinishGathering:(NSNotification *)notification {
// (3) if Query is finished, this will send the result (i.e. either it found our text.dat or it didn't) to the next function
NSMetadataQuery *query = [notification object];
[query disableUpdates];
[query stopQuery];
[self loadData:query];
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidFinishGatheringNotification object:query];
_query = nil; // we're done with it
}
-(void)loadDocument {
// (2) iCloud query: Looks if there exists a file called text.txt in the cloud
NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
_query = query;
//SCOPE
[query setSearchScopes:[NSArray arrayWithObject:NSMetadataQueryUbiquitousDocumentsScope]];
//PREDICATE
NSPredicate *pred = [NSPredicate predicateWithFormat: #"%K == %#", NSMetadataItemFSNameKey, #"text.txt"];
[query setPredicate:pred];
//FINISHED?
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(queryDidFinishGathering:) name:NSMetadataQueryDidFinishGatheringNotification object:query];
[query startQuery];
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSLog(#"AppDelegate: app did finish launching");
self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
// Override point for customization after application launch.
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
self.viewController = [[[ViewController alloc] initWithNibName:#"ViewController_iPhone" bundle:nil] autorelease];
} else {
self.viewController = [[[ViewController alloc] initWithNibName:#"ViewController_iPad" bundle:nil] autorelease];
}
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
// (1) iCloud: init
NSURL *ubiq = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
if (ubiq) {
NSLog(#"AppDelegate: iCloud access!");
[self loadDocument];
} else {
NSLog(#"AppDelegate: No iCloud access (either you are using simulator or, if you are on your phone, you should check settings");
}
return YES;
}
edo42, did you solve the issue? I had purchased a new 4S to go back to AT&T and I had backed up my first 4S and then restored from backup on new 4S and then this was happening for me.
When I do a [iCloudFileURL getResource:&fileState forKey:NSURLIsUbiquitousItemKey error:&error] I get the same error.
Error Domain=NSPOSIXErrorDomain Code=2 "The operation couldn’t be completed. No such file or directory" UserInfo=0x448500 {NSDescription=Unable to get real path for Path '/private/var/mobile/Library/Mobile Documents/~UbiquitousDir~/Documents/MyDocument.ext'
(Works fine on my iPad 2)
Then I would do the NSMetadataQuery but then it comes back with no results. (Works on my iPad 2 just fine to see the document on iCloud).
I checked to see that the ubiquitous file exists but it is not there as the error suggest.
I tried using [fileMgr evictUbiquitousItemAtURL:ubiquitousDocumentsDir &error] and it does succeed. But when I try the NSMetadataQuery it still comes back with no results.
Hope this helps to figure what is wrong in both cases.
UPDATE: Ok there is a real problem with iCloud for this device that I restored from a backup:
I have deleted my app from the iPhone device.
I deleted the backup files for my app from Manage Storage.
I have even delete the file for my app from iCloud (Documents & Data) under Managed storage.
The iCloud file is no longer there and I look on iPad 2 and it's gone.
I started my app on my iPad 2 and he successfully saves a new iCloud document.
The new iCloud file shows on my iPhone.
I build and run my app on my device but he cannot download the file with NSMetadaQuery query.
I had a similar problem, metadataQueryDidUpdate and metadataQueryDidFinishGathering would fire, but the NSMetadataQuery's results would be empty. There was a provisioning profile problem that Xcode didn't tell me about until I attempted to test it on my device, where it failed, saying that my bundle identifier was invalid (it wasn't).
What fixed it for me was going into Preferences and clicking the Download All button for the provisioning profiles, and then doing a Clean in Xcode. Rebuild, run, and all my files showed up. Check in the developer portal to make sure none of your profiles are marked as Invalid. None of mine were, so it's not required to trigger this, but it can happen.
Not exactly scientific, but this did work for me.
Related
I know that the file exists, because I can download it, but I need to check to see whether it exists. I have tried using
[NSFileManager contentsOfDirectoryAtPath:error:]
but it gives me null. I don't understand why that is because I can still download the files that I'm looking for. Maybe it's an incorrect URL, but the URL that I'm using is the one that I printed at creation of my UIDocument that I'm looking for. Maybe I'm using the wrong method?
EDIT:
I have also tried using NSMetadataQuery, and I can get it to give back notifications, but it doesn't ever have results even though I can explicitly download the files I'm looking for.
To find files in iCloud, you use NSMetadataQuery. That will find both files that have already been downloaded as well as files that are in the user's account but which haven't been downloaded to the local device yet. Using NSFileManager will, at best, only find files that have already been downloaded.
You set it up with something like this:
NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
[self setMetadataQuery:query];
[query setSearchScopes:#[NSMetadataQueryUbiquitousDataScope]];
[query setPredicate:[NSPredicate predicateWithFormat:#"%K LIKE '*'", NSMetadataItemFSNameKey]];
You'll want to observe NSMetadataQueryDidStartGatheringNotification, NSMetadataQueryDidUpdateNotification, and probably NSMetadataQueryDidFinishGatheringNotification. Then start the query:
[query startQuery];
With that done, you'll get notifications as the query discovers iCloud files. The notifications will include instances of NSMetadataItem, which you can use to get information like file size, download status, etc.
Use a metadata query - here is some sample code
/*! Creates and starts a metadata query for iCloud files
*/
- (void)createFileQuery {
[_query stopQuery];
if (_query) {
[_query startQuery];
}
else {
_query = [[NSMetadataQuery alloc] init];
[_query setSearchScopes:[NSArray arrayWithObjects:NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope, nil]];
// NSString * str = [NSString stringWithFormat:#"*.%#",_fileExtension];
NSString *str = #"*";
[_query setPredicate:[NSPredicate predicateWithFormat:#"%K LIKE %#", NSMetadataItemFSNameKey, str]];
NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:#selector(fileListReceived) name:NSMetadataQueryDidFinishGatheringNotification object:_query];
[notificationCenter addObserver:self selector:#selector(fileListReceived) name:NSMetadataQueryDidUpdateNotification object:_query];
[_query startQuery];
}
}
/*! Gets called by the metadata query any time files change. We need to be able to flag files that
we have created so as to not think it has been deleted from iCloud.
*/
- (void)fileListReceived {
LOG(#"fileListReceived called.");
NSArray* results = [[_query results] sortedArrayUsingComparator:^(NSMetadataItem* firstObject, NSMetadataItem* secondObject) {
NSString* firstFileName = [firstObject valueForAttribute:NSMetadataItemFSNameKey];
NSString* secondFileName = [secondObject valueForAttribute:NSMetadataItemFSNameKey];
NSComparisonResult result = [firstFileName.pathExtension compare:secondFileName.pathExtension];
return result == NSOrderedSame ? [firstFileName compare:secondFileName] : result;
}];
//FLOG(#" results of query are %#", results);
for (NSMetadataItem* result in results) {
NSURL* fileURL = [result valueForAttribute:NSMetadataItemURLKey];
NSString* fileName = [result valueForAttribute:NSMetadataItemDisplayNameKey];
NSNumber* percentDownloaded = [result valueForAttribute:NSMetadataUbiquitousItemPercentDownloadedKey];
NSNumber *isDownloaded = nil;
NSNumber *isDownloading = nil;
NSError *er;
[fileURL getResourceValue: &isDownloaded forKey:NSURLUbiquitousItemIsDownloadedKey error:&er];
[fileURL getResourceValue: &isDownloading forKey:NSURLUbiquitousItemIsDownloadingKey error:&er];
FLOG(#" Found file %#", fileName);
}
}
From the docs:
In iOS, actively download files when required. Items in iCloud but not
yet local are not automatically downloaded by iOS; only their metadata
is automatically downloaded. The initial download of new iCloud-based
documents requires your attention and careful design in your app.
After you explicitly download such an item, the system takes care of
downloading changes arriving from iCloud.
Consider keeping track of
file download status as part of your iOS app’s model layer. Having
this information lets you provide a better user experience: you can
design your app to not surprise users with long delays when they want
to open a document that is not yet local. For each file (or file
package) URL provided by your app’s metadata query, get the value of
the NSURLUbiquitousItemIsDownloadedKeykey by calling the NSURL method
getResourceValue:forKey:error:. Reading a file that has not been
downloaded can take a long time, because the coordinated read
operation blocks until the file finishes downloading (or fails to
download).
For a file (or file package) that is not yet local, you can initiate
download either when, or before, the user requests it. If your app’s
user files are not large or great in number, one strategy to consider
is to actively download all the files indicated by your metadata
query. For each file (or file package) URL provided by the query, make
the corresponding item local by calling the NSFileManager method
startDownloadingUbiquitousItemAtURL:error:. If you pass this method a
URL for an item that is already local, the method performs no work and
returns YES.
Update: iOS7 should use NSURLUbiquitousItemDownloadingStatusKey instead of NSURLUbiquitousItemIsDownloadedKey.
Is there any delegate method that will be called when the user upgrades to or reinstalls a newer version of the iOS app?
I use Core Data to cache some information from server. When the schema of any entity is changed, I need to manually delete the SQLite database from the simulator, otherwise the app will crash on startup, with an error "The model used to open the store is incompatible with the one used to create the store." If there is any delegate method for app upgrade, the deletion could be automated.
You need to use CoreData versioning:
http://developer.apple.com/library/mac/#documentation/cocoa/conceptual/CoreDataVersioning/Articles/Introduction.html
Daniel Smith's answer is the proper one, but I just want to add how my app determines its been updated. I look keep a 'current version' string in the defaults. When the app starts up, I compare it to the current version:
defaults has no string - this is the first run of the app
defaults version is different - the user updated the app
defaults is the same - user just restarted the app
Sometimes its nice to know the above. Make sure to save the defaults immediately after you set the tag and do whatever versioning you want, so a crash doesn't have you do it again.
EDIT: how not to crash if he model changes. I use this now, keep the old repository, and tweaking the model, on every tweak it just removes the old one (if it cannot open it) and creates a new one. This is modeled on Apple's code but not sure about what changes I made. In any case you don't get a crash if the model changes.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
//LTLog(#"_persistentStoreCoordinator = %#", _persistentStoreCoordinator);
if (_persistentStoreCoordinator)
{
return _persistentStoreCoordinator;
}
NSFileManager *manager = [NSFileManager defaultManager];
NSString *path = [[appDelegate applicationAppSupportDirectory] stringByAppendingPathComponent:[_dbName stringByAppendingPathExtension:#"SQLite"]];
storeURL = [NSURL fileURLWithPath:path];
BOOL fileExists = [manager fileExistsAtPath:path];
if(!fileExists) {
_didCreateNewRepository = YES;
}
if(_createNewRepository) {
[manager removeItemAtURL:storeURL error:nil];
if(fileExists) _didDestroyOldRepository = YES;
_didCreateNewRepository = YES;
}
while(YES) {
__autoreleasing NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if ([_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
break;
} else {
_persistentStoreCoordinator = nil;
[manager removeItemAtURL:storeURL error:&error];
if(fileExists) {
_didDestroyOldRepository = YES; // caller didn't want a new one but got a new one anyway (old one corrupt???)
_didCreateNewRepository = YES;
}
#ifndef NDEBUG
LTLog(#"CORE DATA failed to open store %#: error=%#", _dbName, error);
#endif
/*
Replace this implementation with code to handle the error appropriately.
abort() 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. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
Typical reasons for an error here include:
* The persistent store is not accessible
* The schema for the persistent store is incompatible with current managed object model
Check the error message to determine what the actual problem was.
*/
//LTLog(#"Unresolved error %#, %#", error, [error userInfo]);
//abort();
}
}
return _persistentStoreCoordinator;
}
Follow the blog its good:
http://blog.10to1.be/cocoa/2011/11/28/core-data-versioning/
I am creating a new UIManagedDocument with iCloud support as follows:
Alloc and init with local sandbox URL
Set persistent store options to support iCloud: ubiquitousContentNameKey and ubiquitousContentURL. The name I'm generating uniquely and the URL is pointing to my ubiquityContainer / CoreData.
Save locally to sandbox with UIManagedDocument's saveToURL method.
In completion handler, move to iCloud with FileManager's setUbiquitous method.
So far, this dance works. (Well, sort of). After I call setUbiquitous, I get an error that says it WASN'T successful, however the document moves to the cloud. When it's done, I have a new document in the cloud. This appears to be a bug, as I've been able to replicate it with others' code.
I'm actually generating this document in a "Documents View Controller," which lists all of the documents in the cloud. So when this new document's final completion handler is finished, it shows up in the table view thanks to an NSMetadataQuery. So far, pretty standard usage I think.
To edit a document, the user taps and goes to a "Single View Document View Controller."
In this view controller, I need to "reopen" the selected document so the user can edit it.
So I go through series of steps again:
Alloc / init a UIManagedDocument with a fileURL -- this time, the URL is from the cloud.
Set my persistent store options, same as step 2 above, with same settings.
Now, I ATTEMPT step 3, which is to open the document from disk, but it fails. The document is in a state of "Closed | SavingError" and the attempt to open fails.
Does anyone know why my document would create OK, move to the cloud OK, but then fail to open on an immediate subsequent attempt? (Really, an attempt within that launch of the app - see below). Specifically, what would make a UIManagedDocument instance be created but in a closed, non-openable state?
Interestingly enough, if I quit the app and launch again, I can tap and reload the document and edit it.
And very occasionally I can create, then open, and edit very briefly, say insert one managedobject, and then it goes into this close | saving error state.
ERROR INFO:
I've subclassed UIManagedDocument and overrode the -handleError: method to try and get more information, and here's what I get (along with some other debugging logs I put in):
2012-10-05 14:57:06.000 Foundations[23687:907] Single Document View Controller View Did Load. Document: fileURL: file://localhost/private/var/mobile/Library/Mobile%20Documents/7PB5426XF4~com~howlin~MyApp/Documents/New%20Document%2034/ documentState: [Closed]
2012-10-05 14:57:06.052 MyApp[23687:907] Document state changed. Current state: 5 fileURL: file://localhost/private/var/mobile/Library/Mobile%20Documents/7PB5426XF4~com~howlin~MyApp/Documents/New%20Document%2034/ documentState: [Closed | SavingError]
2012-10-05 14:57:06.057 Foundations[23687:5303] UIManagedDocument error: The store name: New Document 34 is already in use.
Store URL: file://localhost/private/var/mobile/Library/Mobile%20Documents/7PB5426XF4~com~howlin~MyApp/Documents/New%20Document%2034/StoreContent.nosync/persistentStore
In Use Store URL: file://localhost/var/mobile/Applications/D423F5FF-4B8E-4C3E-B908-11824D70FD34/Documents/New%20Document%2034/StoreContent.nosync/persistentStore
2012-10-05 14:57:06.059 MyApp[23687:5303] {
NSLocalizedDescription = "The store name: New Document 34 is already in use.\n\tStore URL: file://localhost/private/var/mobile/Library/Mobile%20Documents/7PB5426XF4~com~howlin~MyApp/Documents/New%20Document%2034/StoreContent.nosync/persistentStore\n\tIn Use Store URL: file://localhost/var/mobile/Applications/D423F5FF-4B8E-4C3E-B908-11824D70FD34/Documents/New%20Document%2034/StoreContent.nosync/persistentStore\n";
NSPersistentStoreUbiquitousContentNameKey = "New Document 34";
}
The error seems to think I'm it create a store that already exists on the subsequent opening. Am I now supposed to set those iCloud option on the persistent store on a second opening? I've tried that approach and it didn't work either.
I've studied the Stanford lectures on UIManagedDocument and don't see what I'm doing wrong.
Here's my method to create the doc and move to cloud:
- (void) testCreatingICloudDocWithName:(NSString*)name
{
NSURL* cloudURL = [self.docManager.iCloudURL URLByAppendingPathComponent:name isDirectory:YES];
NSURL* fileURL = [self.docManager.localURL URLByAppendingPathComponent:name];
self.aWriting = [[FNFoundationDocument alloc] initWithFileURL:fileURL];
[self setPersistentStoreOptionsInDocument:self.aWriting];
[self.aWriting saveToURL:fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
if (success == YES) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//create file coordinator
//move document to icloud
NSFileCoordinator* fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
NSError* coorError = nil;
[fileCoordinator coordinateWritingItemAtURL:cloudURL options:NSFileCoordinatorWritingForReplacing error:&coorError byAccessor:^(NSURL *newURL) {
if (coorError) {
NSLog(#"Coordinating writer error: %#", coorError);
}
NSFileManager* fm = [NSFileManager defaultManager];
NSError* error = nil;
NSLog(#"Before set ubiq");
[fm setUbiquitous:YES itemAtURL:fileURL destinationURL:newURL error:&error];
if (!error) {
NSLog(#"Set ubiquitous successfully.");
}
else NSLog(#"Error saving to cloud. Error: %#", error);
NSLog(#"State of Doc after error saving to cloud: %#", self.aWriting);
}];
});
}
}];
}
Here's where I set options for iCloud on the persistentStore:
- (void)setPersistentStoreOptionsInDocument:(FNDocument *)theDocument
{
NSMutableDictionary *options = [NSMutableDictionary dictionary];
[options setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];
[options setObject:[NSNumber numberWithBool:YES] forKey:NSInferMappingModelAutomaticallyOption];
[options setObject:[theDocument.fileURL lastPathComponent] forKey:NSPersistentStoreUbiquitousContentNameKey];
NSURL* coreDataLogDirectory = [self.docManager.coreDataLogsURL URLByAppendingPathComponent:[theDocument.fileURL lastPathComponent]];
NSLog(#"Core data log dir: %#", coreDataLogDirectory);
[options setObject:coreDataLogDirectory forKey:NSPersistentStoreUbiquitousContentURLKey];
theDocument.persistentStoreOptions = options;
}
And here's where I try to reopen it:
- (void) prepareDocForUse
{
NSURL* fileURL = self.singleDocument.fileURL;
if (![[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]]) {
NSLog(#"File doesn't exist");
}
else if (self.singleDocument.documentState == UIDocumentStateClosed) {
// exists on disk, but we need to open it
[self.singleDocument openWithCompletionHandler:^(BOOL success) {
if (!success) {
NSError* error;
[self.singleDocument handleError:error userInteractionPermitted:NO];
}
[self setupFetchedResultsController];
}];
} else if (self.singleDocument.documentState == UIDocumentStateNormal) {
// already open and ready to use
[self setupFetchedResultsController];
}
}
Have you recently been testing various version of iOS? Try changing the title of your saved document to something other than "New Document 34", I was experiencing this same issue and I believe it had to do with conflicting documents saved from different sdk compilations of the app using the same document url.
I target iOS7, I use a single UIManagedDocument as my app DB with the goal to better integrate CoreData and iCloud as suggested by Apple in its documentation. I had the same problem, i solved with the following code.
Since I wrote it, I moved the PSC options settings inside the lazy instantiation of the UIManagedDocument.
My original code created, closed and then reopened the document using the callback with success standard functions. I found it on a book by Erika Sadun. Everything seemed ok but I couldn't reopen the just created and then closed document because it was in "savings error" state. I lost a week on it, I couldn't understand what I was doing wrong because until the reopen everything was perfect.
The following code works perfectly on my iPhone5 and iPad3.
Nicola
-(void) fetchDataWithBlock: (void (^) (void)) fetchingDataBlock
{
//If the CoreData local file exists then open it and perform the query
if([[NSFileManager defaultManager] fileExistsAtPath:[self.managedDocument.fileURL path]]){
NSLog(#"The CoreData local file in the application sandbox already exists.");
if (self.managedDocument.documentState == UIDocumentStateNormal){
NSLog(#"The CoreData local file it's in Normal state. Fetching data.");
fetchingDataBlock();
}else if (self.managedDocument.documentState == UIDocumentStateClosed){
NSLog(#"The CoreData local file it's in Closed state. I am opening it.");
[self.managedDocument openWithCompletionHandler:^(BOOL success) {
if(success){
NSLog(#"SUCCESS: The CoreData local file has been opened succesfully. Fetching data.");
fetchingDataBlock();
}else{
NSLog(#"ERROR: Can't open the CoreData local file. Can't fetch the data.");
NSLog(#"%#", self.managedDocument);
return;
}
}];
}else{
NSLog(#"ERROR: The CoreData local file has an unexpected documentState: %#", self.managedDocument);
}
}else{
NSLog(#"The CoreData local file in the application sandbox did not exist.");
NSLog(#"Setting the UIManagedDocument PSC options.");
[self setPersistentStoreOptionsInDocument:self.managedDocument];
//Create the Core Data local File
[self.managedDocument saveToURL:self.managedDocument.fileURL
forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success) {
if(success){
NSLog(#"SUCCESS: The CoreData local file has been created. Fetching data.");
fetchingDataBlock();
}else{
NSLog(#"ERROR: Can't create the CoreData local file in the application sandbox. Can't fetch the data.");
NSLog(#"%#", self.managedDocument);
return;
}
}];
}
}
I have a straightforward NSDocument-based Mac OS X app in which I am trying to implement iCloud Document storage. I'm building with the 10.7 SDK.
I have provisioned my app for iCloud document storage and have included the necessary entitlements (AFAICT). The app builds, runs, and creates the local ubiquity container Documents directory correctly (this took a while, but that all seems to be working). I am using the NSFileCoordinator API as Apple recommended. I'm fairly certain I am using the correct UbiquityIdentifier as recommended by Apple (it's redacted below tho).
I have followed Apple's iCloud Document storage demo instructions in this WWDC 2011 video closely:
Session 107 AutoSave and Versions in Lion
My code looks almost identical to the code from that demo.
However, when I call my action to move the current document to the cloud, I experience liveness problems when calling the -[NSFileManager setUbiquitous:itemAtURL:destinationURL:error:] method. It never returns.
Here is the relevant code from my NSDocument subclass. It is almost identical to Apple's WWDC demo code. Since this is an action, this is called on the main thread (as Apple's demo code showed). The deadlock occurs toward the end when the -setUbiquitous:itemAtURL:destinationURL:error: method is called. I have tried moving to a background thread, but it still never returns.
It appears that a semaphore is blocking while waiting for a signal that never arrives.
When running this code in the debugger, my source and destination URLs look correct, so I'm fairly certain they are correctly calculated and I have confirmed the directories exist on disk.
Am I doing anything obviously wrong which would lead to -setUbiquitous never returning?
- (IBAction)moveToOrFromCloud:(id)sender {
NSURL *fileURL = [self fileURL];
if (!fileURL) return;
NSString *bundleID = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleIdentifier"];
NSString *appID = [NSString stringWithFormat:#"XXXXXXX.%#.macosx", bundleID];
BOOL makeUbiquitous = 1 == [sender tag];
NSURL *destURL = nil;
NSFileManager *mgr = [NSFileManager defaultManager];
if (makeUbiquitous) {
// get path to local ubiquity container Documents dir
NSURL *dirURL = [[mgr URLForUbiquityContainerIdentifier:appID] URLByAppendingPathComponent:#"Documents"];
if (!dirURL) {
NSLog(#"cannot find URLForUbiquityContainerIdentifier %#", appID);
return;
}
// create it if necessary
[mgr createDirectoryAtURL:dirURL withIntermediateDirectories:NO attributes:nil error:nil];
// ensure it exists
BOOL exists, isDir;
exists = [mgr fileExistsAtPath:[dirURL relativePath] isDirectory:&isDir];
if (!(exists && isDir)) {
NSLog(#"can't create local icloud dir");
return;
}
// append this doc's filename
destURL = [dirURL URLByAppendingPathComponent:[fileURL lastPathComponent]];
} else {
// get path to local Documents folder
NSArray *dirs = [mgr URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
if (![dirs count]) return;
// append this doc's filename
destURL = [[dirs objectAtIndex:0] URLByAppendingPathComponent:[fileURL lastPathComponent]];
}
NSFileCoordinator *fc = [[[NSFileCoordinator alloc] initWithFilePresenter:self] autorelease];
[fc coordinateWritingItemAtURL:fileURL options:NSFileCoordinatorWritingForMoving writingItemAtURL:destURL options:NSFileCoordinatorWritingForReplacing error:nil byAccessor:^(NSURL *fileURL, NSURL *destURL) {
NSError *err = nil;
if ([mgr setUbiquitous:makeUbiquitous itemAtURL:fileURL destinationURL:destURL error:&err]) {
[self setFileURL:destURL];
[self setFileModificationDate:nil];
[fc itemAtURL:fileURL didMoveToURL:destURL];
} else {
NSWindow *win = ... // get my window
[self presentError:err modalForWindow:win delegate:nil didPresentSelector:nil contextInfo:NULL];
}
}];
}
I don't know if these are the source of your problems, but here are some things I'm seeing:
-[NSFileManager URLForUbiquityContainerIdentifier:] may take a while, so you shouldn't invoke it on the main thread. see the "Locating the Ubiquity Container" section of this blog post
Doing this on the global queue means you should probably use an allocated NSFileManager and not the +defaultManager.
The block passed to the byAccessor portion of the coordinated write is not guaranteed to be called on any particular thread, so you shouldn't be manipulating NSWindows or presenting modal dialogs or anything from within that block (unless you've dispatched it back to the main queue).
I think pretty much all of the iCloud methods on NSFileManager will block until things complete. It's possible that what you're seeing is the method blocking and never returning because things aren't configured properly. I'd double and triple check your settings, maybe try to simplify the reproduction case. If it still isn't working, try filing a bug or contacting DTS.
Just shared this on Twitter with you, but I believe when using NSDocument you don't need to do any of the NSFileCoordinator stuff - just make the document ubiquitous and save.
Hmm,
did you try not using a ubiquity container identifier in code (sorry - ripped out of a project so I've pseudo-coded some of this):
NSFileManager *fm = [NSFileManager defaultManager];
NSURL *iCloudDocumentsURL = [[fm URLForUbiquityContainerIdentifier:nil] URLByAppendingPathComponent:#"Documents"];
NSURL *iCloudFileURL = [iCloudDocumentsURL URLByAppendingPathComponent:[doc.fileURL lastPathComponent]];
ok = [fm setUbiquitous:YES itemAtURL:doc.fileURL destinationURL:iCloudRecipeURL error:&err];
NSLog(#"doc moved to iCloud, result: %d (%#)",ok,doc.fileURL.fileURL);
And then in your entitlements file:
<key>com.apple.developer.ubiquity-container-identifiers</key>
<array>
<string>[devID].com.yourcompany.appname</string>
</array>
Other than that, your code looks almost identical to mine (which works - except I'm not using NSDocument but rolling it all myself).
If this is the first place in your code that you are accessing iCloud look in Console.app for a message like this:
taskgated: killed yourAppID [pid 13532] because its use of the com.apple.developer.ubiquity-container-identifiers entitlement is not allowed
Anytime you see this message delete your apps container ~/Library/Containers/<yourAppID>
There may also be other useful messages in Console.app that will help you solve this issue.
I have found that deleting the app container is the new Clean Project when working with iCloud.
Ok, So I was finally able to solve the problem using Dunk's advice. I'm pretty sure the issue I was having is as follows:
Sometime after the WWDC video I was using as a guide was made, Apple completed the ubiquity APIs and removed the need to use an NSFileCoordinator object while saving from within an NSDocument subclass.
So the key was to remove both the creation of the NSFileCoordinator and the call to -[NSFileCoordinator coordinateWritingItemAtURL:options:writingItemAtURL:options:error:byAccessor:]
I also moved this work onto a background thread, although I'm fairly certain that was not absolutely required to fix the issue (although it was certainly a good idea).
I shall now submit my completed code to Google's web crawlers in hopes of assisting future intrepid Xcoders.
Here's my complete solution which works:
- (IBAction)moveToOrFromCloud:(id)sender {
NSURL *fileURL = [self fileURL];
if (!fileURL) {
NSBeep();
return;
}
BOOL makeUbiquitous = 1 == [sender tag];
if (makeUbiquitous) {
[self displayMoveToCloudDialog];
} else {
[self displayMoveFromCloudDialog];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self doMoveToOrFromCloud:makeUbiquitous];
});
}
- (void)doMoveToOrFromCloud:(BOOL)makeUbiquitous {
NSURL *fileURL = [self fileURL];
if (!fileURL) return;
NSURL *destURL = nil;
NSFileManager *mgr = [[[NSFileManager alloc] init] autorelease];
if (makeUbiquitous) {
NSURL *dirURL = [[MyDocumentController instance] ubiquitousDocumentsDirURL];
if (!dirURL) return;
destURL = [dirURL URLByAppendingPathComponent:[fileURL lastPathComponent]];
} else {
// move to local Documentss folder
NSArray *dirs = [mgr URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
if (![dirs count]) return;
destURL = [[dirs firstObject] URLByAppendingPathComponent:[fileURL lastPathComponent]];
}
NSError *err = nil;
void (^completion)(void) = nil;
if ([mgr setUbiquitous:makeUbiquitous itemAtURL:fileURL destinationURL:destURL error:&err]) {
[self setFileURL:destURL];
[self setFileModificationDate:nil];
completion = ^{
[self hideMoveToFromCloudDialog];
};
} else {
completion = ^{
[self hideMoveToFromCloudDialog];
NSWindow *win = [[self canvasWindowController] window];
[self presentError:err modalForWindow:win delegate:nil didPresentSelector:nil contextInfo:NULL];
};
}
dispatch_async(dispatch_get_main_queue(), completion);
}
I'm trying to record and play movies with my app using qtkit. I record the video in one view and display it into another view. Here's how I do it
- (void)startRecording
{
NSString *applicationSupportDirectory = [[NSFileManager defaultManager] applicationSupportDirectory];
NSString *path = [applicationSupportDirectory stringByAppendingPathComponent:kVideoOutputName];
NSURL *url = [NSURL fileURLWithPath:path];
// Delete the previous file
[[NSFileManager defaultManager] removeItemAtURL:url error:nil];
mCaptureMovieFileOutput.delegate = self;
[mCaptureMovieFileOutput recordToOutputFileURL:url];
}
- (void)stopRecording
{
[mCaptureMovieFileOutput recordToOutputFileURL:nil];
}
- (void)captureOutput:(QTCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL forConnections:(NSArray *)connections dueToError:(NSError *)error
{
// [[NSWorkspace sharedWorkspace] openURL:outputFileURL];
// removes the current view
[self cleanView];
MyViewController *controller = [[SharingViewController alloc] init];
controllerpath.path = outputFileURL;
[self.view addSubview:[controller view]];
[self stopCamera];
}
Now in my view controller I assign the movie to my movie player.
- (void)awakeFromNib
{
NSError *error;
moviePlayer.movie = [QTMovie movieWithURL:path error:&error];
NSLog(#"%#", [error localizedDescription]);
}
Now, this code works the first time around, but I need to register and show multiple times.
There's already one problem here. If I want to record videos multiple times, I have to delete the first one, otherwise after the first time it wont record anything (it complains that a file already exists).
The problem is that after the first time, it also doesn't show the video at all. When I execute [QTMovie movieWithURL:path error:&error]; it complains that the file or directory does not exists, when in reality it does (I also checked with [QTMovie canInitWithUrl:]).
I'm not sure what's going on here. The apple's sample code is able to record multiple times, but for some reasons I can't without first deleting the existing file (it works the first time, though).
I'd be happy to provide further details if needed.
EDIT: If I use a different name every time for the video, everything works. So this is really an issue about recording with the same name every time.
I ended up using a different file name for every file.
I ran into the same issue, and I found that setting the movie view to nil, prior to reusing the same file name resolved the issue.
I get the same weird problem creating a QTMovie. I just loaded the file to an NSData object like this and it worked:
[self setMovieData:[NSData dataWithContentsOfURL:[self movieURL]]];
[self setMovie:[QTMovie movieWithData:[self movieData] error:&error]];