Threads are killing my app - objective-c

Looking for some advice on stablizing my app. First some requirements - files with PII (personally identifying information) must be encrypted when on disk. Tumbnails and logos are in the custom TableViewCells (if available) and must be decrypted before display.
There are several layers of threading going on. There is a central function getFileData that checks to see if files are on the device or if the files need to be obtained from the network. I desire to keep the UI responsive and (I think) therein lies my problem.
Here is some code:
This is the workhorse method for processing files in my application. It decides where the file is decrypts it and hands it back to a callback:
-(void)fetchFileData:(UserSession *) session
onComplete: (void(^)(NSData* data)) onComplete
{
NSURL *url = [File urlForMail:self.fileId andSession:session];
//NSLog(#"File id: %#", self.fileId);
NSString *encryptionKey = session.encryptionKey;
dispatch_queue_t cryptoQ = dispatch_queue_create(FILE_CRYPTOGRAPHY_QUEUE, NULL);
dispatch_async(cryptoQ, ^(void){
// Get the file and d/encrypt it
NSError *error = nil;
if ([File fileExistsAtUrl:url] == YES) {
NSLog(#"file is on disk.");
NSData *localEncryptedFile = [File getDataForFile:url];
NSData *decryptedFile = [RNDecryptor decryptData:localEncryptedFile
withPassword:encryptionKey
error:&error];
onComplete(decryptedFile);
dispatch_release(cryptoQ);
} else {
//NSLog(#"File is not on disk");
NSDictionary *remoteFile = [session.apiFetcher getFileContent:self.fileId
andToken:session.token];
if (remoteFile && [[remoteFile objectForKey:#"success"] isEqualToString:#"true"]) {
NSData *remoteFileData = [remoteFile objectForKey:#"data"];
NSString *mimeType = [remoteFile objectForKey:#"mimeType"];
self.mimeType = mimeType;
NSData *encryptedData = [RNEncryptor encryptData:remoteFileData
withSettings:kRNCryptorAES256Settings
password:encryptionKey
error:&error];
[encryptedData writeToURL:url atomically:YES];
onComplete(remoteFileData);
dispatch_release(cryptoQ);
}
}
});
Here is an example of a getFileData caller:
+(void)loadThumbnailForMail: (NSNumber*)thumbnailId
session: (UserSession*)session
callback: (void(^)(NSData* data))callback
{
File *file = [File findFile:thumbnailId inContext:session.mailDatabase.managedObjectContext];
dispatch_queue_t fetchQ = dispatch_queue_create(FILE_FETCHER_QUEUE_LABEL, NULL);
dispatch_async(fetchQ, ^(void) {
if (file) {
[file fetchFileData:session onComplete:^(NSData *data) {
if (data && file.mimeType) {
callback(data);
}
}];
}
});
dispatch_release(fetchQ);
}
Here is an example of the TableViewCell that is calling loadThumbnailForMail:
-(void)loadAndShowThumbnailImage:(Mail*) mail
{
UIImage *placeHolder = [UIImage imageNamed:#"thumbnail_placeholder.png"];
[self.thumbnailImageForMail setImage:placeHolder];
dispatch_queue_t loaderQ = dispatch_queue_create(THUMBNAIL_FETCHER, NULL);
dispatch_async(loaderQ, ^ {
[File loadThumbnailForMail: mail.thumbnailId
session: [UserSession instance]
callback: ^(NSData *data) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *thumbnailImage = [UIImage imageWithData:data];
[self.thumbnailImageForMail setImage:thumbnailImage];
});
}];
});
dispatch_release(loaderQ);
}
I think that my issue here is the callback in my loadThumbnailImage. If the user scrolls fast enough I suspect that there could be two threads trying to access the same TableViewCell
(MyCell *cell = (MyCell*)[tableView dequeueReusableCellWithIdentifier:CellTableIdentifier];)
It doesn't always happen right away but eventually, after some scrolling the tableView list of cells the app crashes with this: * Terminating app due to uncaught exception 'NSGenericException', reason: '* Collection <__NSCFSet: 0xde6a650> was mutated while being enumerated.'
I need to have the decrypted images in the cells and the first solution (above) does this for me when the images are available but causes the app to crash. I am wondering if some sort of in memory cache would help improve this if I put images in memory there when they were decrypted and checked that cache in loadAndShowThumbnailImage before I kick off all the threads to get and decrypt them.
Thoughts? I have been banging on this for a week now trying different things and would appreciate some fresh perspective.
Thanks.

Based on Justins link and subsequent research I ended up going in this direction: In a UITableView, best method to cancel GCD operations for cells that have gone off screen?

Related

How to get data out of a block?

I'm trying to make an equivalent to the .NET recognize() call, which is synchronous, for ios in objective-c. I found code to recognize speech but the string that was recognized is only inside a block.
I've tried making the block not a block (it seems to be part of the API that it be a block), making __block variables and returning their values, also out parameters in the caller/declarer of the block; finally I wrote a file while in the block and read the file outside. It still didn't work like I want because of being asynchronous although I at least got some data out. I also tried writing to a global variable from inside the block and reading it outside.
I'm using code from here: How to implement speech-to-text via Speech framework, which is (before I mangled it):
/*!
* #brief Starts listening and recognizing user input through the
* phone's microphone
*/
- (void)startListening {
// Initialize the AVAudioEngine
audioEngine = [[AVAudioEngine alloc] init];
// Make sure there's not a recognition task already running
if (recognitionTask) {
[recognitionTask cancel];
recognitionTask = nil;
}
// Starts an AVAudio Session
NSError *error;
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryRecord error:&error];
[audioSession setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
// Starts a recognition process, in the block it logs the input or stops the audio
// process if there's an error.
recognitionRequest = [[SFSpeechAudioBufferRecognitionRequest alloc] init];
AVAudioInputNode *inputNode = audioEngine.inputNode;
recognitionRequest.shouldReportPartialResults = YES;
recognitionTask = [speechRecognizer recognitionTaskWithRequest:recognitionRequest resultHandler:^(SFSpeechRecognitionResult * _Nullable result, NSError * _Nullable error) {
BOOL isFinal = NO;
if (result) {
// Whatever you say in the microphone after pressing the button should be being logged
// in the console.
NSLog(#"RESULT:%#",result.bestTranscription.formattedString);
isFinal = !result.isFinal;
}
if (error) {
[audioEngine stop];
[inputNode removeTapOnBus:0];
recognitionRequest = nil;
recognitionTask = nil;
}
}];
// Sets the recording format
AVAudioFormat *recordingFormat = [inputNode outputFormatForBus:0];
[inputNode installTapOnBus:0 bufferSize:1024 format:recordingFormat block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when) {
[recognitionRequest appendAudioPCMBuffer:buffer];
}];
// Starts the audio engine, i.e. it starts listening.
[audioEngine prepare];
[audioEngine startAndReturnError:&error];
NSLog(#"Say Something, I'm listening");
}
I want to call Listen(), (like startListening() above), have it block execution until done, and have it return the string that was said. But actually I would be thrilled just to get result.bestTranscription.formattedString somehow to the caller of startListening().
I'd recommend you to take another approach. In Objective-C having a function that blocks for a long period of time is an anti-pattern.
In this language there's no async/await, nor cooperative multitasking, so blocking for long-ish periods of time might lead to resource leaks and deadlocks. Moreover if done on the main thread (where the app UI runs), the app might be forcefully killed by the system due to being non-responsive.
You should use some asynchronous patterns such as delegates or callbacks.
You might also try using some promises library to linearize your code a bit, and make it look "sequential".
The easiest approach with callbacks would be to pass a completion block to your "recognize" function and call it with the result string when it finishes:
- (void)recognizeWithCompletion:(void (^)(NSString *resultString, NSError *error))completion {
...
recognitionTask = [speechRecognizer recognitionTaskWithRequest:recognitionRequest
resultHandler:^(SFSpeechRecognitionResult *result, NSError *error)
{
...
dispatch_async(dispatch_get_main_queue(), ^{
completion(result.bestTranscription.formattedString, error);
});
...
}];
...
}
Note that the 2nd parameter (NSError) - is an error in case the caller wants to react on that too.
Caller side of this:
// client side - add this to your UI code somewhere
__weak typeof(self) weakSelf = self;
[self recognizeWithCompletion:^(NSString *resultString, NSError *error) {
if (!error) {
[weakSelf processCommand:resultString];
}
}];
// separate method
- (void)processCommand:(NSString *command) {
// can do your processing here based on the text
...
}

use xcassets without imageNamed to prevent memory problems?

according to the apple documentation it is recommended to use xcassets for iOS7 applications and reference those images over imageNamed.
But as far as I'm aware, there were always problems with imageNamed and memory.
So I made a short test application - referencing images out of the xcassets catalogue with imageNamed and started the profiler ... the result was as expected. Once allocated memory wasn't released again, even after I removed the ImageView from superview and set it to nil.
I'm currently working on an iPad application with many large images and this strange imageView behavior leads to memory warnings.
But in my tests I wasn't able to access xcassets images over imageWithContentsOfFile.
So what is the best approach to work with large images on iOS7? Is there a way to access images from the xcassets catalogue in another (more performant) way? Or shouldn't I use xcassets at all so that I can work with imageWithContentsOfFile?
Thank you for your answers!
UPDATE: Cache eviction works fines (at least since iOS 8.3).
I decided to go with the "new Images.xcassets" from Apple, too. Things started to go bad, when I had about 350mb of images in the App and the App constantly crashed (on a Retina iPad; probably because of the size of the loaded images).
I have written a very simple test app where I load the images in three different types (watching the profiler):
imageNamed: loaded from an asset: images never gets released and the app crashes (for me I could load 400 images, but it really depends on the image size)
imageNamed: (conventionally included to the project): The memory usage is high and once in a while (> 400 images) I see a call to didReceiveMemoryWarning:, but the app is running fine.
imageWithContentsOfFile([[NSBundle mainBundle] pathForResource:...): The memory usage is very low (<20mb) because the images are only loaded once at a time.
I really would not blame the caching of the imageNamed: method for everything as caching is a good idea if you have to show your images again and again, but it is kind of sad that Apple did not implement it for the assets (or did not document it that it is not implemented). In my use-case, I will go for the non-caching imageWithData because the user won't see the images again.
As my app is almost final and I really like the usage of the loading mechanism to find the right image automatically, I decided to wrap the usage:
I removed the images.xcasset from the project-target-copy-phase and added all images "again" to the project and the copy-phase (simply add the top level folder of Images.xcassets directly and make sure that the checkbox "Add To Target xxx" is checked and "Create groups for any added folders" (I did not bother about the useless Contents.json files).
During first build check for new warnings if multiple images have the same name (and rename them in a consistent way).
For App Icon and Launch Images set "Don't use asset catalog" in project-target-general and reference them manually there.
I have written a shell script to generate a json-model from all the Contents.json files (to have the information as Apples uses it in its asset access code)
Script:
cd projectFolderWithImageAsset
echo "{\"assets\": [" > a.json
find Images.xcassets/ -name \*.json | while read jsonfile; do
tmppath=${jsonfile%.imageset/*}
assetname=${tmppath##*/}
echo "{\"assetname\":\"${assetname}\",\"content\":" >> a.json
cat $jsonfile >> a.json;
echo '},' >>a.json
done
echo ']}' >>a.json
Remove the last "," comma from json output as I did not bother to do it manually here.
I have used the following app to generate json-model-access code: https://itunes.apple.com/de/app/json-accelerator/id511324989?mt=12 (currently free) with prefix IMGA
I have written a nice category using method swizzling in order to not change running code (and hopefully removing my code very soon):
(implementation not complete for all devices and fallback mechanisms!!)
#import "UIImage+Extension.h"
#import <objc/objc-runtime.h>
#import "IMGADataModels.h"
#implementation UIImage (UIImage_Extension)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
Method imageNamed = class_getClassMethod(class, #selector(imageNamed:));
Method imageNamedCustom = class_getClassMethod(class, #selector(imageNamedCustom:));
method_exchangeImplementations(imageNamed, imageNamedCustom);
});
}
+ (IMGABaseClass*)model {
static NSString * const jsonFile = #"a";
static IMGABaseClass *baseClass = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *fileFilePath = [[NSBundle mainBundle] pathForResource:jsonFile ofType:#"json"];
NSData* myData = [NSData dataWithContentsOfFile:fileFilePath];
__autoreleasing NSError* error = nil;
id result = [NSJSONSerialization JSONObjectWithData:myData
options:kNilOptions error:&error];
if (error != nil) {
ErrorLog(#"Could not load file %#. The App will be totally broken!!!", jsonFile);
} else {
baseClass = [[IMGABaseClass alloc] initWithDictionary:result];
}
});
return baseClass;
}
+ (UIImage *)imageNamedCustom:(NSString *)name{
NSString *imageFileName = nil;
IMGAContent *imgContent = nil;
CGFloat scale = 2;
for (IMGAAssets *asset in [[self model] assets]) {
if ([name isEqualToString: [asset assetname]]) {
imgContent = [asset content];
break;
}
}
if (!imgContent) {
ErrorLog(#"No image named %# found", name);
}
if (is4InchScreen) {
for (IMGAImages *image in [imgContent images]) {
if ([#"retina4" isEqualToString:[image subtype]]) {
imageFileName = [image filename];
break;
}
}
} else {
if ( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ) {
for (IMGAImages *image in [imgContent images]) {
if ([#"iphone" isEqualToString:[image idiom]] && ![#"retina4" isEqualToString:[image subtype]]) {
imageFileName = [image filename];
break;
}
}
} else {
if (isRetinaScreen) {
for (IMGAImages *image in [imgContent images]) {
if ([#"universal" isEqualToString:[image idiom]] && [#"2x" isEqualToString:[image scale]]) {
imageFileName = [image filename];
break;
}
}
} else {
for (IMGAImages *image in [imgContent images]) {
if ([#"universal" isEqualToString:[image idiom]] && [#"1x" isEqualToString:[image scale]]) {
imageFileName = [image filename];
if (nil == imageFileName) {
// fallback to 2x version for iPad unretina
for (IMGAImages *image in [imgContent images]) {
if ([#"universal" isEqualToString:[image idiom]] && [#"2x" isEqualToString:[image scale]]) {
imageFileName = [image filename];
break;
}
}
} else {
scale = 1;
break;
}
}
}
}
}
}
if (!imageFileName) {
ErrorLog(#"No image file name found for named image %#", name);
}
NSString *imageName = [[NSBundle mainBundle] pathForResource:imageFileName ofType:#""];
NSData *imgData = [NSData dataWithContentsOfFile:imageName];
if (!imgData) {
ErrorLog(#"No image file found for named image %#", name);
}
UIImage *image = [UIImage imageWithData:imgData scale:scale];
DebugVerboseLog(#"%#", imageFileName);
return image;
}
#end

EXC_BAD_ACCESS during NSFileVersion call to removeOtherVersionsOfItemAtURL: inside coordinated write block

I'm using what seems to be a simple invocation of the NSFileVersion class method removeOtherVersionsOfItemAtURL: inside a coordinated writing block for some iCloud conflict resolution.
When my devices go into 'spaz mode', which is a technical term for repeatedly opening and closing the application on a few devices, an EXC_BAD_ACCESS exception is thrown internally. Code snippet:
- (void)compareVersionChanges:(NSFileVersion *)version {
if (![DataLoader iCloudPreferenceEnabled]) {
NSLog(#"Ignoring iCloud changes (version comparison) based on user preference");
return;
}
NSLog(#"compareVersionChanges");
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(aQueue, ^(void) {
NSError *readError = nil;
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:(id)self];
[coordinator coordinateReadingItemAtURL:[version URL] options:0 error:&readError byAccessor:^(NSURL *newURL) {
DataContext *loadedContext = nil;
NSData *data = [NSData dataWithContentsOfURL:newURL];
NSError *e = nil;
loadedContext = [self convertXmlDataToContext:data error:&e];
if (e) {
NSLog(#"Done loading, error: %#", e);
[[DataLoader applicationDelegate] displayError:e];
loadedContext = nil;
}
if (!loadedContext) {
return;
}
id appDelegate = [DataLoader applicationDelegate];
DataContext *inMemoryContext = nil;
if (appDelegate != nil && [appDelegate respondsToSelector:#selector(context)]) {
inMemoryContext = [appDelegate performSelector:#selector(context)];
}
if (inMemoryContext) {
NSLog(#"Performing iCloud context synchronizating...");
DataContextSynchronizer *synchronizer = [[DataContextSynchronizer alloc] init];
ChangeSet *changes = [synchronizer compareLocalContext:inMemoryContext andRemoteContext:loadedContext];
if ([[changes changes] count] > 0) {
[SelectionManager disable];
#synchronized(appDelegate) {
NSLog(#"Applying synchronization changes...");
[synchronizer applyChangeSet:changes toDataContext:inMemoryContext];
NSLog(#"Synchronization changes applied");
}
[SelectionManager enable];
if ([appDelegate respondsToSelector:#selector(setSkipRefreshSave:)]) {
[appDelegate performSelector:#selector(setSkipRefreshSave:) withObject:[NSNumber numberWithBool:YES]];
}
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(mainQueue, ^(void) {
[SelectionManager notifyListeners];
});
if ([appDelegate respondsToSelector:#selector(setSkipRefreshSave:)]) {
[appDelegate performSelector:#selector(setSkipRefreshSave:) withObject:[NSNumber numberWithBool:NO]];
}
[self save:[[DataLoader applicationDelegate] context]];
} else {
NSLog(#"No sync changes applicable.");
}
NSError *coordinateWriteRemoveError = nil;
[coordinator coordinateWritingItemAtURL:newURL options:NSFileCoordinatorWritingForDeleting error:&coordinateWriteRemoveError byAccessor:^(NSURL *theURL) {
theURL = [theURL copy];
NSError *removeOtherVersionsError = nil;
[NSFileVersion removeOtherVersionsOfItemAtURL:theURL error:&removeOtherVersionsError];
if (removeOtherVersionsError) {
NSLog(#"Error removing other versions: %#", removeOtherVersionsError);
}
}];
if (coordinateWriteRemoveError) {
NSLog(#"Error occurred coordinating write for deletion of other file versions: %#", coordinateWriteRemoveError);
}
}
}];
if (readError) {
NSLog(#"Done loading (outside block) error: %#", readError);
}
});
}
I thought a little syntax highlighting might make this easier to examine:
Link to image of code snippet and failure stack in Xcode
The error actually occurs on line 1404, and as you can see from the below screenshot, it's deep in Apple code territory.
Link to image of debugger
Before submitting a radar, I thought I'd check here to see if there's something I'm doing wrong? The extra [... copy] on line 1402 was just a quick check to make sure I'm not losing the reference to the block-provided argument, and will be removed.
Edit: An important note! I'm using ARC.
Edit 2: I've noticed that when calling:
[NSFileVersion otherVersionsOfItemAtURL:theURL]
The return value is nil, which indicates (via the documentation):
...or nil if there is no such file. The array does not contain the version object returned by the currentVersionOfItemAtURL: method.
So by checking the return value of this method before I make the call to removeOtherVersionsOfItemAtURL:, it has alleviated the issue. But I still find it strange that an EXC_BAD_ACCESS is thrown, rather than that method handling it properly.
I've noticed that when calling:
[NSFileVersion otherVersionsOfItemAtURL:theURL]
immediately prior to the call to removeOtherVersionsOfItemAtURL:, the return value is nil, which indicates (via the documentation):
Returns: An array of file version objects or nil if there is no such
file. The array does not contain the version object returned by the
currentVersionOfItemAtURL: method.
So by checking the return value of this method before I make the call to removeOtherVersionsOfItemAtURL:, it has alleviated the issue. But I still find it strange that an EXC_BAD_ACCESS is thrown by removeOtherVersionsOfItemAtURL:, rather than that method simply returning NO, or simply populating the provided NSError object.
I'll be filing a Radar and will update here when I hear back.

NSFileHandle & Writing Asynch to a file in iOS

I have a situation that I receive a byte data through Web Services request and want to write it to a file on my iOS device. I used to append all data (till end of data) in a memory variable and at the end writing the data using NSStream to a file in my iOS device using method:
stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent
It works fine for small size of data, but the problem is if I am receiving data via web services it could be a big chunk (couple MBs) and I don't want to collect all in memory to write it to the file, to make it efficent I think I have to switch to NSFileHandle to write data in a small chunk size to the same file in several times. Now my question is what is the best approach to do this? I mean how can I do write to the file in BACKGROUND using NSFileHandle? I use code like this:
- (void) setUpAsynchronousContentSave:(NSData *) data {
NSString *newFilePath = [NSHomeDirectory() stringByAppendingPathComponent:#"/Documents/MyFile.xml"];
if(![[NSFileManager defaultManager] fileExistsAtPath:newFilePath ]) {
[[NSFileManager defaultManager] createFileAtPath:newFilePath contents:nil attributes:nil];
}
if(!fileHandle_writer) {
fileHandle_writer = [NSFileHandle fileHandleForWritingAtPath:newFilePath];
}
[fileHandle_writer seekToEndOfFile];
[fileHandle_writer writeData:data];
}
but with passing a data size of 1-2 Mb to above method, do I need to make it running in background? FYI I'm writing in main thread.
Maybe you can try Grand Central Dispatch.
I spent some time trying it, bellow is my way to do it.
According to Apple's document, if our program need executing only one task at a time, we should create a "Serial Dispatch Queue".So, first declare a queue as iVar.
dispatch_queue_t queue;
create a serial dispatch queue in init or ViewDidLoad using
if(!queue)
{
queue = dispatch_queue_create("yourOwnQueueName", NULL);
}
When data occurs, call your method.
- (void) setUpAsynchronousContentSave:(NSData *) data {
NSString *newFilePath = [NSHomeDirectory() stringByAppendingPathComponent:#"/Documents/MyFile.xml"];
NSFileManager *fileManager = [[NSFileManager alloc] init];
if(![fileManager fileExistsAtPath:newFilePath ]) {
[fileManager createFileAtPath:newFilePath contents:nil attributes:nil];
}
if(!fileHandle_writer) {
self.fileHandle_writer = [NSFileHandle fileHandleForWritingAtPath:newFilePath];
}
dispatch_async( queue ,
^ {
// execute asynchronously
[fileHandle_writer seekToEndOfFile];
[fileHandle_writer writeData:data];
});
}
At last, we need to release the queue in ViewDidUnload or dealloc
if(queue)
{
dispatch_release(queue);
}
I combine these code with ASIHttp, and it works.
Hope it helps.

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