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]];
Related
I've been working on adding icloud to my project (which is quite a pain in the buns) and I'm able to save and remove files, but I can't get a list of the files stored in iCloud. I've tried solutions from about 10 different websites (including the Apple documentation). Whenever I call [self.query startQuery]; everything seems to be working: The correct methods get called, the methods execute exactly as they should. Then when I ask for an nsarray of the files in my app's iCloud Documents directory I get two parenthesis with nothing between them (when I view in NSLog): File List: ( ). I know for a fact that there are many different documents of all shapes, extensions, sizes, and names in my app's iCloud Documents directory because I've been using the iCloud Developer site to check if things are working. My first method to setup the query is as follows:
- (void)syncWithCloud {
self.query = [[NSMetadataQuery alloc] init];
NSURL *mobileDocumentsDirectoryURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
[query setSearchScopes:[NSArray arrayWithObjects:NSMetadataQueryUbiquitousDocumentsScope, nil]];
//[query setPredicate:[NSPredicate predicateWithFormat:#"%K LIKE '*'", NSMetadataItemFSNameKey]];
[query setPredicate:[NSPredicate predicateWithFormat:[NSString stringWithFormat:#"%%K like \"%#*\"", [mobileDocumentsDirectoryURL path]], NSMetadataItemPathKey]];
//Pull a list of all the Documents in The Cloud
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(processFiles:)
name:NSMetadataQueryDidFinishGatheringNotification object:self.query];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(processFiles:)
name:NSMetadataQueryDidUpdateNotification object:self.query];
[self.query startQuery];
}
The process files method provided by Apple is next:
- (void)processFiles:(NSNotification*)aNotification {
NSMutableArray *discoveredFiles = [NSMutableArray array];
//Always disable updates while processing results.
[query disableUpdates];
//The query reports all files found, every time.
NSArray *queryResults = [query results];
for (NSMetadataItem *result in queryResults) {
NSURL *fileURL = [result valueForAttribute:NSMetadataItemURLKey];
NSNumber *aBool = nil;
// Don't include hidden files.
[fileURL getResourceValue:&aBool forKey:NSURLIsHiddenKey error:nil];
if (aBool && ![aBool boolValue])
[discoveredFiles addObject:fileURL];
}
//Update the list of documents.
[FileList removeAllObjects];
[FileList addObjectsFromArray:discoveredFiles];
//[self.tableView reloadData];
//Reenable query updates.
[query enableUpdates];
NSLog(#"File List: %#", FileList);
}
Why doesn't this give me a list of files or at least some kind of data? Am I defining the NSMetadata query wrong, maybe my predicate isn't formatted right? I know I'm doing something wrong because there's no way iCloud could be this complicated (or could it?).
Thanks for the help in advance!
Edit #1: I am continuing to try different approaches to this problem. Using one of the answers below I have changed the predicate filter as follows:
[query setPredicate:[NSPredicate predicateWithFormat:#"NSMetadataItemFSNameKey LIKE '*'"]];
I have also added the following lines before the [query enableUpdates] call:
for (NSMetadataItem *item in query.results) {
[FileList addObject:[item valueForAttribute:NSMetadataItemFSNameKey]];
}
In the processFiles method, I've tried placing all of the code on the background thread, but this makes no difference - as a matter of fact, when the code is not executed on the background thread FileList gives me this (null) instead of this ( ).
Could my problem have to do with thread management or memory allocation? Please note that I am using ARC.
Edit #2: The FileList variable is an NSMutableArray defined in my #interface and initialized in the -(id)init method before calling the processFiles method.
Edit #3: When testing my code with breakpoints I found that the following for-loop never gets run through - not even once. This leads me to believe that:
A. The proper directory isn't being connected with
B. iCloud can't see the files in the directory
C. My NSMetadataQuery isn't being setup properly
D. Something completely different
Here's the code that starts the for-loop which never gets run:
NSArray *queryResults = [query results];
for (NSMetadataItem *result in queryResults) {
Since you already set the search scope correct, there's no need to use special filters in the predicate.
Just use:
query.predicate = [NSPredicate predicateWithFormat:#"NSMetadataItemFSNameKey == '*'"];
And to get the array use:
NSMutableArray *array = [NSMutableArray array];
for (NSMetadataItem *item in query.results) {
[array addObject:[item valueForAttribute:NSMetadataItemFSNameKey]];
}
I've solved my problem. Getting the list of files in iCloud was just a matter of correctly defining, allocating, and initializing properties. SAE's answer and this SO posting helped me solve my problem and create this GitHub Repo called iCloud Document Sync. The iCloud Document Sync class simplifies the whole iCloud Document storage process down to a few lines of code. The commit linked here fixes the issues from my question.
I have an application that first loads some data into an UIManagedDocument, then executes saveToURL:forSaveOperation:completionHandler:. Inside the completionHandler block, it does an update of various elements of this database, and when it's done, it does another saving.
Besides that, the app has 3 buttons that reload the data, re-update the data, and delete one entity of the database, respectively. In every button method, the last instruction is a saving as well.
When I run all this in the simulator, all goes smoothly. But in the device doesn't. It constantly crashes. I have observed that, normally, it crashes when pressing the "delete" button, or when reloading or re-updating the database. And it's always in the saveToURL operation.
In my opinion, the problem comes when there are multiple threads saving the database. As the device executes the code slower, maybe multiple savings come at same time and the app can't handle them correctly. Also, sometimes the delete button doesn't delete the entity, and says that doesn't exist (when it does).
I'm totally puzzled with this, and all this saving operations must be done...In fact, if I remove them, the app behaves even more incoherently.
Any suggestions of what could I do to resolve this problem? Thank you very much!
[Edit] Here I post the problematic code. For first loading the data, I use a helper class, with this two methods in particular:
+ (void)loadDataIntoDatabase:(UIManagedDocument *)database
{
[database.managedObjectContext performBlock:^{
// Read from de plist file and fill the database
[database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) {
[DataHelper completeDataOfDatabase:database];
}];
}
+ (void)completeDataOfDatabase:(UIManagedDocument *)database
{
[database.managedObjectContext performBlock:^{
// Read from another plist file and update some parameters of the already existent data (uses NSFetchRequest and works well)
// [database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:nil];
[database updateChangeCount:UIDocumentChangeDone];
}];
}
And in the view, I have 3 action methods, like these:
- (IBAction)deleteButton {
[self.database.managedObjectContext performBlock:^{
NSManagedObject *results = ;// The item to delete
[self.database.managedObjectContext deleteObject:results];
// [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
}];
}
- (IBAction)reloadExtraDataButton {
[DataHelper loadDataIntoDatabase:self.database];
// [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
}
- (IBAction)refreshDataButton {
[DataHelper completeDataOfDatabase:self.database];
//[self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
}
[Edit 2] More code: First of all, the initial view executes viewDidLoad this way:
- (void)viewDidLoad{
[super viewDidLoad];
self.database = [DataHelper openDatabaseAndUseBlock:^{
[self setupFetchedResultsController];
}];
}
This is what the setupFetchedResultsController method looks like:
- (void)setupFetchedResultsController
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Some entity name"];
request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES selector:#selector(localizedCaseInsensitiveCompare:)]];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.database.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
}
Each view of the app (it has tabs) has a different setupFetchedResultsController in order to show the different entities the database contains.
Now, in the helper class, this is the first class method that gets executed, via the viewDidLoad of each view:
+ (UIManagedDocument *)openDatabaseAndUseBlock:(completion_block_t)completionBlock
{
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"Database"];
UIManagedDocument *database = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[database.fileURL path]]) {
[database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
[self loadDataIntoDatabase:database];
completionBlock();
}];
} else if (database.documentState == UIDocumentStateClosed) {
// Existe, pero cerrado -> Abrir
[database openWithCompletionHandler:^(BOOL success) {
[self loadDataIntoDatabase:database];
completionBlock();
}];
} else if (database.documentState == UIDocumentStateNormal) {
[self loadDataIntoDatabase:database];
completionBlock();
}
return database;
}
You didn't really provide much code. The only real clue you gave was that you are using multiple threads.
UIManagedDocument has two ManagedObjectContexts (one specified for the main queue, and the other for a private queue), but they still must each only be accessed from within their own thread.
Thus, you must only use managedDocument.managedObjectContext from within the main thread. If you want to use it from another thread, you have to use either performBlock or performBlockAndWait. Similarly, you can never know you are running on the private thread for the parent context, so if you want to do something specifically to the parent, you must use performBlock*.
Finally, you really should not be calling saveToURL, except when you initially create the database. UIManagedDocument will auto-save (in its own time).
If you want to encourage it to save earlier, you can send it updateChangeCount: UIDocumentChangeDone to tell it that it has changes that need to be saved.
EDIT
You should only call saveToURL when you create the file for the very first time. With UIManagedDocument, there is no need to call it again (and it can actually cause some unintended issues).
Basically, when you create the document DO NOT set your iVar until the completion handler executes. Otherwise, you could be using a document in a partial state. In this case, use a helper, like this, in the completion handler.
- (void)_document:(UIManagedDocument*)doc canBeUsed:(BOOL)canBeUsed
{
dispatch_async(dispatch_get_main_queue(), ^{
if (canBeUsed) {
_document = doc;
// Now, the document is ready.
// Fire off a notification, or notify a delegate, and do whatever you
// want... you really should not use the document until it's ready, but
// as long as you leave it nil until it is ready any access will
// just correctly do nothing.
} else {
_document = nil;
// Do whatever you want if the document can not be used.
// Unfortunately, there is no way to get the actual error unless
// you subclass UIManagedDocument and override handleError
}
}];
}
And to initialize your document, something like...
- (id)initializeDocumentWithFileURL:(NSURL *)url
{
if (!url) {
url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"Default_Project_Database"];
}
UIManagedDocument *doc = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[doc.fileURL path]]) {
// The file does not exist, so we need to create it at the proper URL
[doc saveToURL:doc.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
[self _document:doc canBeUsed:success];
}];
} else if (doc.documentState == UIDocumentStateClosed) {
[doc openWithCompletionHandler:^(BOOL success) {
[self _document:doc canBeUsed:success];
}];
} else {
// You only need this if you allow a UIManagedDocument to be passed
// in to this object -- in which case the code above that initializes
// the <doc> variable will be conditional on what was passed...
BOOL success = doc.documentState == UIDocumentStateNormal;
[self _document:doc canBeUsed:success];
}
}
The "pattern" above is necessary to make sure you do not use the document until it is fully ready for use. Now, that piece of code should be the only time you call saveToURL.
Note that by definition, the document.managedObjectContext is of type NSMainQueueConcurrencyType. Thus, if you know your code is running on the main thread (like all your UI callbacks), you do not have to use performBlock.
However, if you are actually doing loads in the background, consider..
- (void)backgroundLoadDataIntoDocument:(UIManagedDocument*)document
{
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
moc.parentContext = document.managedObjectContext;
[moc performBlock:^{
// Do your loading in here, and shove everything into the local MOC.
// If you are loading a lot of stuff from the 'net (or elsewhere),
// consider doing it in strides, so you deliver objects to the document
// a little at a time instead of all at the end.
// When ready to save, call save on this MOC. It will shove the data up
// into the MOC of the document.
NSrror *error = nil;
if ([moc save:&error]) {
// Probably don't have to synchronize calling updateChangeCount, but I do it anyway...
[document.managedObjectContext performBlockAndWait:^{
[document updateChangeCount:UIDocumentChangeDone];
}];
} else {
// Handle error
}
}];
}
Instead of parenting your background MOC to the mainMOC, you can parent it to the parentContext. Loading and then saving into it will put the changes "above" the main MOC. The main MOC will see those changes the next time it does a fetch operation (note the properties of NSFetchRequest).
NOTE: Some people have reported (and it also appears as a note in Erica Sadun's book), that after the very first saveToURL, you need to close, then open to get everything working right.
EDIT
This is getting really long. If you had more points, I'd suggest a chat. Actually, we can't do it through SO, but we could do it via another medium. I'll try to be brief, but please go back and reread what I posted, and pay careful attention because your code is still violating several tenants.
First, in viewDidLoad(), you are directly assigning your document to the result of calling openDatabaseAndUseBlock. The document is not in a usable state at that time. You do not want the document accessible until the completion handlers fire, which will not happen before openDatabaseAndUseBlock() returns.
Second, only call saveToURL the very first time you create your database (inside openDatabaseAndUseBlock()). Do not use it anywhere else.
Third. Register with the notification center to receive all events (just log them). This will greatly assist your debugging, because you can see what's happening.
Fourth, subclass UIManagedDocument, and override the handleError, and see if it is being called... it's the only way you will see the exact NSError if/when it happens.
3/4 are mainly to help you debug, not necessary for your production code.
I have an appointment, so have to stop now. However, address those issues, and here's on
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
[self NextHeading]; // this plays an mp3 file
[self NextHeadingMeaning]; // this plays an Mp3 file
}
Only [self NextHeadingMeaning] method is called and NextHeading method is missed each time
-(IBAction) NextHeading{
[audio stop];
NSString *Filename = [[NSString alloc]initWithFormat:#"CH%#S%#",Heading,Meaning];
Filepath = [[NSBundle mainBundle]pathForResource:Filename ofType:#"mp3"];
audio = [[AVAudioPlayer alloc]initWithContentsOfURL:[NSURL fileURLWithPath:Filepath] error:NULL];
audio.delegate = self;
[audio play];
[Filename autorelease];
}
-(IBAction) NextHeadingMeaning {
[audio stop];
NSString *Filename = [[NSString alloc] initWithFormat:#"CH%#S%#",bold**Chapter**bold, Meaning];
Filepath = [[NSBundle mainBundle]pathForResource:Filename ofType:#"mp3"];
audio = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:Filepath] error:NULL];
audio.delegate = self;
[audio play];
[Filename autorelease];
}
Why is this happening and how can I resolve it ?
Please advice, thanks in advance.
You just used a single iVar (audio) as an player, and when you send NextHeading & NextHeadingMeaning message, the audio init with your sound_1.mp3 file firstly (it'll take some seconds if the mp3 file is big), then at the next moment (your first mp3 file might not inited, or has inited, but stopped followed by next message), you redo the init action with another mp3 file (sound_2.mp3), and finally, when the second mp3 file init done, audio plays sound_2.mp3. That's why you think the NextHeading is skipped.
So, to solve this problem, you can use a NSMutableArray iVar (e.g. audioPlayers), and create a local audio for both NextHeading & NextHeadingMeaning, and push it to audioPlayers.
And I think it is better to preload sound files if you can. :)
EDIT:
There's a playAtTime: method instead of play, you can delay the second audio player's playing time by this method, just like this:
[audioPlayer playAtTime:(audioPlayer.deviceCurrentTime + delay)];
delay is in seconds (NSTimeInterval).
There is no way that the first call is skipped, put a breakpoint in it or output something with NSLog() and you'll see. Most probable cause is that the first method doesn't do what you expect and this could be for various reasons - for example condition or specific timeout.
Edit:
After looking your code, it seems that you're missing some basic stuff like variable naming, variable scope and so. To simply make your code run, just replace the NSString *Filename.. string from the second method and probably it'll work. A better choice would be to visit Start Developing iOS Apps Today and follow the roadmap.
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 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.