Memory leak when using UIwebview - objective-c

Im in the final stages of my first iphone sdk project. I have been working hard to remove memory leaks from my app, and have mostly succeeded at it. But there I am struggling with one of them. I have a contact screen with a button that fetches a webview, but only if there is a network connection. If not an alert pops up. This works fine in practice but l-e-a-k-s.
All the leaks point to the same place in the code. Here is the first code sample (instruments points to the first of these lines):
BOOL nett=[self connectedToNetwork];
if (!nett)
{
errorView=[[UIAlertView alloc] initWithTitle:#"Netverksfeil" message:#"Nettet er nede" delegate:self
cancelButtonTitle:#"FillerĀ“n!" otherButtonTitles:nil];
[errorView show];
[errorView release];
}
else{
iCodeBrowserViewController *browserView=[[iCodeBrowserViewController alloc]initWithNibName:#"iCodeBrowserViewController" bundle:[NSBundle mainBundle]];
[[self navigationController] pushViewController:browserView animated:YES];
[browserView release];
}
I suppose that means that the leak is somewhere inside that function...
The next spot instruments points at is in this sample:
// Create zero addy
- (BOOL) connectedToNetwork{ struct sockaddr_in zeroAddress;
bzero(&zeroAddress, sizeof(zeroAddress));
zeroAddress.sin_len = sizeof(zeroAddress);
zeroAddress.sin_family = AF_INET;
// Recover reachability flags
SCNetworkReachabilityRef defaultRouteReachability = SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *)&zeroAddress);
SCNetworkReachabilityFlags flags;
BOOL didRetrieveFlags = SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags);
CFRelease(defaultRouteReachability);
if (!didRetrieveFlags)
{
printf("Error. Could not recover network reachability flags\n");
return 0;
}
BOOL isReachable = flags & kSCNetworkFlagsReachable;
BOOL needsConnection = flags & kSCNetworkFlagsConnectionRequired;
BOOL nonWiFi = flags & kSCNetworkReachabilityFlagsTransientConnection;
return ((isReachable && !needsConnection) || nonWiFi) ?
(([[[NSURLConnection alloc] initWithRequest:[NSURLRequest
requestWithURL: [NSURL URLWithString:#"http://www.apple.com/"]
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:20.0]
delegate:self]autorelease]) ? YES : NO) : NO;}
This line to be specific:
return ((isReachable && !needsConnection) || nonWiFi) ?
Can any of you see what is leaking in this code? I have copied this part from somewhere else, and managed to alter it slightly. But i must admit i dont understand all the code in that function...

Have you cleaned the project then run a "Build & Analyze"? Most of the time that will tell you about most of your memory issues as long as you've been using Objective C style functions. If you mix-and-match with C style functions it won't be as much help.
I'd guess that the NSURLRequest inside that line is the one that isn't getting released. Might help readability and maintainability to break that line up a bit.

Related

NSString cannot be released

Consider the following method and the caller code block. The method analyses a NSString
and extracts a "http://" string which it passes out by reference as an auto release object.
Without releasing g_scan_result, the program works as expected. But according to non-arc rules, g_scan_result should be released since a retain has been called against it.
My question are :
Why g_scan_result cannot be released ?
Is there anything wrong the way g_scan_result is handled in the posted coding below ?
Is it safe not to release g_scan_result as long as the program runs correctly and the XCode Memory Leak tool does not show leakage ?
Which XCode profile tools should I look into to check and under which subtitle ?
Hope somebody knowledgeable could help.
- (long) analyse_scan_result :(NSString *)scan_result target_url :(NSString **)targ_url {
NSLog (#" RES analyse string : %#", scan_result);
NSRange range = [scan_result rangeOfString:#"http://"
options:NSCaseInsensitiveSearch];
if (range.location == NSNotFound) {
*targ_url = #"";
NSLog(#"fnd string not found");
return 0;
}
NSString *sub_string = [scan_result substringFromIndex : range.location];
range = [sub_string rangeOfString : #" "];
if (range.location != NSNotFound) {
sub_string = [sub_string substringToIndex : range.location];
}
NSLog(#" fnd sub_string = %#", sub_string);
*targ_url = sub_string;
return [*targ_url length];
}
The following is the caller code block, also note that g_scan_result has been declared and initialized (on another source file) as :
NSString *g_scan_result = nil;
Please do send a comment or answer if you have suggestions or find possible errors in code posted here (or above). Xcode memory tools does not seem to show any memory leak. But it may be because I do not know where to look as am new to the memory tools.
{
long url_leng = [self analyse_scan_result:result target_url:&targ_url];
NSLog(#" TAR target_url = %#", targ_url);
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Scanned Result"
message:result
delegate:g_alert_view_delegate
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
if (url_leng) {
// ****** The 3 commented off statements
// ****** cannot be added without causing
// ****** a crash after a few scan result
// ****** cycles.
// ****** NSString *t_url;
if (g_system_status.language_code == 0)
[alert addButtonWithTitle : #"Open"];
else if (g_system_status.language_code == 1)
[alert addButtonWithTitle : #"Abrir"];
else
[alert addButtonWithTitle : #"Open"];
// ****** t_url = g_scan_result;
g_scan_result = [targ_url retain];
// ****** [t_url release];
}
targ_url = nil;
[alert show];
[alert release];
[NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:#selector(activate_qr_scanner:)
userInfo:nil
repeats:NO
];
return;
}
I think the mystery has been solved. Thanks for all who are kind enough to have looked into this. The reason is before adding the UIAlertView, coding had been done (on another source file) to assign a raw output string to g_scan_result and display it directly on the current view. So when g_scan_result got released, it is releasing the wrong NSString assigned by some outdated code.
In summary, the wrong NSString got released which is the source of the problem.
The solution is just to remove a single outdated statement. The statement from the old implementation was left there as I thought it wouldn't do any harm (and may even helped to make the variable populated continuously). But it turned out to be a very silly mistake. The only excuse is having very little sleep lately. Being able to find an excuse does serve a purpose. Just hope that it doesn't have to be done very often ...

Memory warning in the iPad 1st generation and crash

i am developing an iPad application witch has a functionality downloading books. The books size are about 180 Mo. The books are in server and the have the extension .zip. I download the book (.zip) then i unzip it and then remove the .zip. I am doing like this :
- (BOOL)downloadBookWithRequest:(BookDownloadRequest*)book
{
if (![book isValid])
{
NSLog(#"Couldn't launch download since request had missing parameter");
return NO;
}
if ([self bookIsCurrrentlyDownloadingWithID:book.ID]) {
NSLog(#"Book already downloaded");
return NO;
}
ASIHTTPRequest *download = [[ASIHTTPRequest alloc] initWithURL:book.URL];
download.userInfo = book.dictionary;
download.downloadDestinationPath = [self downloadPathForBookID:book.ID];
download.downloadProgressDelegate = self.downloadVC.downloadProgress;
download.shouldContinueWhenAppEntersBackground = YES;
[self.downloadQueue addOperation:download];
[download release];
// Update total requests
self.requestsCount++;
[self refreshDownloadsCount];
if(self.downloadQueue.isSuspended)
[self.downloadQueue go];
[self.downloadVC show];
return YES;
}
- (void)requestFinished:(ASIHTTPRequest*)request
{
NSString *bookStoragePath = [[BooksManager booksStoragePath] stringByAppendingPathComponent:request.downloadDestinationPath.lastPathComponent.stringByDeletingPathExtension];
NSString *bookZipPath = request.downloadDestinationPath;
// Tell not to save the zip file into iCloud
[BooksManager addSkipBackupAttributeToItemAtPath:bookZipPath];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *removeExistingError = nil;
if ([fileManager fileExistsAtPath:bookStoragePath])
{
[fileManager removeItemAtPath:bookStoragePath error:&removeExistingError];
if (removeExistingError)
{
[self bookDidFailWithRequest:request errorMessageType:DownloadErrorTypeFILE_ERROR];
NSLog(#"ERROR: Couldn't remove existing book to unzip new download (%#)", removeExistingError);
} else
NSLog(#"INFO: Removed existing book to install new download");
}
ZipArchive* zip = [[ZipArchive alloc] init];
if([self isCompatibleWithFileAtPath:bookZipPath] && [zip UnzipOpenFile:bookZipPath])
{
BOOL unzipSucceeded = [zip UnzipFileTo:bookStoragePath overWrite:YES];
if (!unzipSucceeded)
{
[self bookDidFailWithRequest:request errorMessageType:DownloadErrorTypeFILE_ERROR];
NSLog(#"ERROR: Couldn't unzip file %#\n to %#",bookZipPath,bookStoragePath);
} else {
[self bookDidInstallWithRequest:request];
NSLog(#"INFO: Successfully unziped downloaded file");
}
[zip UnzipCloseFile];
}
else
{
[self bookDidFailWithRequest:request errorMessageType:DownloadErrorTypeFILE_ERROR];
NSLog(#"ERROR: Unable to open zip file %#\n",bookZipPath);
}
[self removeZipFileAtPath:bookZipPath];
[zip release];
}
-(BOOL) removeZipFileAtPath:(NSString*) bookZipPath {
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:bookZipPath])
{
NSError *removeZipFileError = nil;
[fileManager removeItemAtPath:bookZipPath error:&removeZipFileError];
if (removeZipFileError) {
NSLog(#"ERROR: Couldn't remove existing zip after unzip (%#)", removeZipFileError);
return NO;
}
else {
NSLog(#"INFO: Removed zip downloaded after unzip");
return YES;
}
}
return NO;
}
My Problem is : This code is working fine with iPhone 4/iPhone 4s/ iPad 2G /iPad3G, but it crash with an iPad 1st Generation (when unzipping the book) and the crash reporter says that the are Memory warning.
How question is, how i can optimize this code to avoid the memory warning and avoid the crash ? Thanks for your answers;
Edit : I have found that the problem is caused by this portion of code :
NSData *bookData = [[NSData alloc]initWithContentsOfFile:bookPath];
the bookPath is the path to the .zip ( about 180 Mo) and when i am in iPad 1G this line crash my application i.e. : i receive memory warnings and the system kill the App. Du you know how i can avoid this. I an using this line to calculate the MD5 of the book (.zip)
I have a category in NSData like this :
#import <CommonCrypto/CommonDigest.h>
#implementation NSData(MD5)
- (NSString*)MD5
{
// Create byte array of unsigned chars
unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH];
// Create 16 byte MD5 hash value, store in buffer
CC_MD5(self.bytes, self.length, md5Buffer);
// Convert unsigned char buffer to NSString of hex values
NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
for(int i = 0; i < CC_MD5_DIGEST_LENGTH; i++)
[output appendFormat:#"%02x",md5Buffer[i]];
return output;
}
How i can avoid the crash ? thanks
EDIT:
So, it seems that the culprit is loading into memory the whole file in order to calculate its MD5 hash.
The solution to this would be calculating the MD5 without having to load into memory the whole file. You can give a look at this post explaining how to compute efficiently an MD5 or SHA1 hash, with the relative code. Or if you prefer, you can go directly to github and grab the code.
Hope it helps.
OLD ANSWER:
You should inspect your app, especially the ZipArchive class, for memory leaks or not-released memory. You can use Instruments' Leaks and Memory Allocation tools to profile your app.
The explanation of the different behavior between iPad1 and the rest of devices may lay with their different memory footprint, as well as with different memory occupation states of the devices (say, you iPad 1 has less free memory when you run the app then the iPad 2 because of the state other apps you ran on the iPad 1 left the device in). You might think of rebooting the iPad 1 to inspect its behavior out of a fresh start.
In any case, besides the possible explanation of the different behaviors, the ultimate cause is how your app manages memory and Instruments is the way to go.
I don't agree with Sergio.
You are saying the app crashes when you init the NSData object with a 180mb zip archive.
Well it's natural you run out of memory, since the 1st gen iPad has half the memory of the 2nd gen... (256MB vs 512)
The solution is to split the zip archive in smaller parts and process them one by one.

Analyzer claiming an object was released when it wasn't

I am getting a static analysis error in this code which doesn't make any sense to me. The error is:
Reference-counted object is used after it is released
This is glue code to allow for PNG loading in a game originally written in C++.
int pngLoad(const char *filename, pngInfo *info, int format, GLuint *textureName)
{
char fullPath[Engine::Settings::MaxPath];
strcpy(fullPath, filename);
appendAppBundlePath(fullPath);
NSString *path = [NSString stringWithCString:fullPath encoding:NSUTF8StringEncoding];
NSData *data = [[NSData alloc] initWithContentsOfFile:path];
UIImage *image = [[UIImage alloc] initWithData:data];
[data release];
Texture2D *loadedTex = [Texture2D alloc];
// ##### Analyzer claims the object is released here: #####
[loadedTex initWithImage:image format:format];
int didLoad;
// ##### Error is here: #####
if (loadedTex.contentSize.width == 0 || loadedTex.contentSize.height == 0)
{
didLoad = 0;
}
else
{
didLoad = 1;
*textureName = loadedTex.name;
// return texture info
info->ScaleFactor = loadedTex.scaleFactor;
info->Width = (float)image.size.width / (float)info->ScaleFactor;
info->Height = (float)image.size.height / (float)info->ScaleFactor;
info->Alpha = 1;
info->PaddedWidth = loadedTex.pixelsWide;
info->PaddedHeight = loadedTex.pixelsHigh;
}
[loadedTex release];
[image release];
return didLoad;
}
If I use Texture2D *loadedTex = [[Texture2D alloc] retain]; this warning is removed, but then an warning that I've leaked an object comes up, so something is seriously weird here.
initWithImage:format: used to contain a [self release] which shouldn't have been there, which I removed when I found this warning. However, even after a full clean and rebuild, I still get the warning. Am I doing something else wrong? Is something not getting properly cleaned up by the Clean command in Xcode?
The analyzer may be right, at least in a general way.
Texture2D *loadedTex = [Texture2D alloc];
[loadedTex initWithImage:image format:format];
In general, "init" might actually discard the object passed in and return a different one. Whether or not this is the case for "Texture2D" is something I don't know, but if the analyzer is going for the general case then it is right.
You should be able to work around that by using
Texture2D *loadedTex = [Texture2D alloc];
loadedTex=[loadedTex initWithImage:image format:format];
Or by simply combining the two calls, as it is done in most Objective-C examples.
You should always combine the alloc and initXXX call when creating objects, in this case
Texture2D *loadedTex = [[Texture2D alloc] initWithImage:image format:format];
An init method need not return the same object it was called with, it is allowed to return a different object.
In this case your result from loadedTex = [Texture2D alloc] would be released and initWithImage would return a different object (which you discard).

Calling Obj-C Code from JavaScript via Console: Arguments get dropped?

Having a heck of a time with this one.
I've got a super-simple Cocoa app containing one WebView, a WebScripting API defined in the page, and a single NSObject defined on that API. When I turn on the debugger tools (in the embedded WebView), I can see the API on the JavaScript window object, and I can see my "api" property defined on that -- but when I call the API's "get" method, the arguments aren't being serialized -- when the Obj-C method gets called, the arguments are missing. See below, which hopefully illustrates:
I've combed through the docs, I've (apparently) set the appropriate methods to expose everything that needs to be exposed, and I can see the method being called. There has to be something stupid I'm missing, but as a relative newbie to this environment, I'm not seeing it.
Thanks in advance for your help!
Have you set WebKitDeveloperExtras to YES in your default user defaults when you send -[NSUserDefaults registerDefaults:]?
Depending on what version of Xcode you're using you could be getting a known error. If you're using LLDB on anything but the most recent version, it might not be giving you the right variables in the debugger. The solution has been to use GDB instead of LLDB until Apple fixes the problem. But I think they fixed the problem in the latest version. I'd change the debugger to use GDB and see if you're getting the right variables in Xcode. (Product-> Edit Scheme...-> Run -> Debugger). I came across this problem in iOS, though, so I don't know its applicability to OSX. Worth a try anyway.
I originally came across the problem here: https://stackoverflow.com/a/9485349/1147934
I process javascript in the main thread of my app from a local file stored in the apps directory. I check for beginning and ending tokens for the js functions I am executing and whether the function contains a variable.
Hopefully this can give you some good ideas for your issue. You could also do alerts in the js to see if the values post correctly as you run the app (I am sure you thought of that already, but it's worth mentioning.) Happy coding! I hope this helps!
in the .h file define:
NSMutableString *processedCommand;
NSArray *commandArguments;
In the .m file:
// tokens
#define kOpenToken #"<%%"
#define kCloseToken #"%%>"
// this will throw
-(void)executeJScriptCommand:(NSString *)aCommand {
[self performSelectorOnMainThread:#selector(executeThisCommand:) withObject:aCommand waitUntilDone:YES];
}
// this will throw
-(NSString *)executeCommand:(NSString *)command {
NSString *aCommand = [[[command stringByReplacingOccurrencesOfString:kOpenToken withString:#""]
stringByReplacingOccurrencesOfString:kCloseToken withString:#""]
stringByTrimmingLeadingAndTrailingWhitespaces];
if ([aCommand hasPrefix:#"="])
{
// variable. get value
[self getVariableFromCommand:aCommand];
}
else {
[self executeThisCommand:aCommand];
}
NSString *returnValue = [NSString stringWithString:processedCommand];
self.processedCommand = nil;
self.commandArguments = nil;
return returnValue;
}
-(void)executeThisCommand:(NSString *)aCommand {
BOOL hasError = NO;
// clear result
self.processedCommand = nil;
self.commandArguments = nil;
BOOL isFromJS = NO;
NSString *function = nil;
NSMutableArray *commandParts = nil;
#try {
// first, break the command into its parts and extract the function that needs to be called, and the (optional) arguments
commandParts = [[NSMutableArray alloc] initWithArray:[aCommand componentsSeparatedByString:#":"]];
if ([[[commandParts objectAtIndex:0] lowercaseString] isEqualToString:#"js-call"]) {
isFromJS = YES;
[commandParts removeObjectAtIndex:0];
}
// get our function, arguments
function = [[commandParts objectAtIndex:0] retain];
[commandParts removeObjectAtIndex:0];
if ([commandParts count] > 0){
if (isFromJS == YES) {
NSString *arguments = [[commandParts objectAtIndex:0] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
if ([arguments length] > 0) {
self.commandArguments = [arguments JSONValue];
}
}
else {
self.commandArguments = [NSArray arrayWithArray:commandParts];
}
}
// build invoke
SEL sel = NSSelectorFromString(function);
if ([self respondsToSelector:sel]) {
[self performSelectorOnMainThread:sel withObject:nil waitUntilDone:YES];
// using invocation causes a SIGABORT because the try/catch block was not catching the exception.
// using perform selector fixed the problem (i.e., the try/catch block now correctly catches the exception, as expected)
}
else {
[appDelegate buildNewExceptionWithName:#"" andMessage:[NSString stringWithFormat:#"Object does not respond to selector %#", function]];
}
}
#catch (NSException * e) {
hasError = YES;
[self updateErrorMessage:[NSString stringWithFormat:#"Error processing command %#: %#", aCommand, [e reason]]];
}
#finally {
[function release];
[commandParts release];
}
if (hasError == YES) {
[appDelegate buildNewExceptionWithName:#"executeThisCommand" andMessage:self.errorMessage];
}
}
// this can return nil
-(NSString *)getQueryStringValue:(NSString *)name {
NSString *returnValue = nil;
if (queryString != nil) {
returnValue = [queryString objectForKey:[name lowercaseString]];
}
return returnValue;
}

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