How can I get the logged-in user's profile picture? - objective-c

macOS allows users to set a profile picture for their local accounts. This picture can be modified in the Accounts system preference pane and is notably displayed at login. Is there a way to access it using Cocoa or some other system framework?

The information is accessible from Open Directory, as is suggested by dscl . -read /Users/my_user_name_here having both a JPEGPhoto and a Picture attribute. The Open Directory framework is rather ancient: documentation is sparse and examples are almost inexistant, but while digging I found a Python test in an Apple repository that shows just enough how to use the API to get started. I finally came up with this:
NSData *getODImage(NSString *userName) {
ODNode *local = [ODNode nodeWithSession:ODSession.defaultSession name:#"/Local/Default" error:nil];
if (!local) {
return nil;
}
ODRecord *record = [local recordWithRecordType:kODRecordTypeUsers name:userName attributes:nil error:nil];
if (!record) {
return nil;
}
NSArray *photoAttributes = #[kODAttributeTypePicture, kODAttributeTypeJPEGPhoto];
NSDictionary *details = [record recordDetailsForAttributes:photoAttributes error:nil];
if (!details) {
return nil;
}
NSData *jpegData = [details[kODAttributeTypeJPEGPhoto] objectAtIndex:0];
if (jpegData) {
return jpegData;
}
for (NSString *picturePath in details[kODAttributeTypePicture]) {
NSData *pictureContents = [NSData dataWithContentsOfFile:picturePath];
if (pictureContents) {
return pictureContents;
}
}
return nil;
}
where userName is the result of NSUserName(). This seems to execute fast locally, but I still have it run from a concurrent queue to avoid blocking the main thread.
The picture path returned through kODAttributeTypePicture is in the global /Library directory, which is accessible for reading from the default sandbox. This is the case for all default user pictures. It might not always be true, however, so prepare for not having a picture.
Correctness-speaking, YMMV when the user is a network user; I think that you'd have to change the node from /Local/Default to something else.

Related

How do I verify a file's existence in iCloud?

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.

Delegate for ios app upgrade

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/

Can create, but not open, a UIManagedDocument with iCloud support

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;
}
}];
}
}

EXC_BAD_ACCESS using iCloud on multiple devices

I'm creating an app with iCloud. But I have some problem. It creates directory on iCloud using NSFileWrapper, then it creates NSData (container) file in NSFileWrapper directory. I'm using this code to convert NSFileWrapper to NSMutableArray:
NSFileWrapper *MyWrapper=[[[MyDocument data] fileWrappers] objectForKey:#"myFile.doh"];
    NSData *MyData=[NSData dataWithData:[MyWrapper regularFileContents]];
    NSMutableArray *MyList=[NSPropertyListSerialization propertyListFromData:MyData mutabilityOption:NSPropertyListMutableContainers format:nil errorDescription:nil];
And it works correctly only on the device, which has created this container. On other devices the result of this code is BAD_ACCESS (in the second line of the code, where I start doing something with data). While debugging, function "regularFileContents" returns correct object with correct data size, but when I try to read this data, BAD_ACEESS(code=10) happens.
I'm using ARC, so it's not an error of memory management.
May be the problem is in some project/code sign settings? Any ideas?
Thanks!
I ran into this as well and after much experimentation I've found that even though the outer wrapper has downloaded the inner contents have not actually downloaded yet and that causes the call to regularFileContents to fail.
I've been calling startDownloadingUbiquitousItemAtURL on MyWrapper and once that completes the error is gone. Here's a method that checks the downloaded status of a file (assuming you know the url to your MyWrapper) and starts the download if it isn't downloaded yet.
-(BOOL)downloadFileIfNotAvailable:(NSURL*)fileURL
{
NSNumber *isInCloud = nil;
if ([fileURL getResourceValue:&isInCloud forKey:NSURLIsUbiquitousItemKey error:nil])
{
if ([isInCloud boolValue]) {
NSNumber *isDownloaded = nil;
if ([fileURL getResourceValue:&isDownloaded forKey:NSURLUbiquitousItemIsDownloadedKey error:nil])
{
if ([isDownloaded boolValue])
{
return YES;
}
NSError *error = nil;
[[NSFileManager defaultManager] startDownloadingUbiquitousItemAtURL:fileURL error:&error];
if (error)
{
NSLog(#"Download Failed :: %#", error);
}
return NO;
}
}
}
return YES;
}

Calling -[NSFileManager setUbiquitous:itemAtURL:destinationURL:error:] never returns

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);
}