I'm in the process of trying to create a singleton class for weather functions (all encompassed) so that I can change/update/recall weather data throughout my entire app in a single allocated object.
I have it working except for one little weird bug. I'm using MKReverseGeocoder on devices running iOS < 5. CLGeocoder works fine on iOS 5+. Basically, this is what happens in the app:
In the app delegate, at launch, I create the shared instance of my weather singleton. This instance doesn't do anything unless there is a UIView that expects to report weather results. When a view that reports weather is loaded, if the location isn't already statically set in the preferences, the app attempts to pull your current location.
This works, and I can log the coordinates of the devices current location. After that is done, I immediately go into trying to reverse geolocate those coordinates. The method MKReverseGeocoder start does get executed and I can log that the instance's isQuerying property is true, so I know it's attempting to geolocate the coords. (Yes, I set the delegate as my shared instance and the instance is of type MKReverseGeocoderDelegate).
Now this is the weird part. If I am launching the app for the first time, and I add a weather UIView to the screen for the first time, MKReverseGeocoder starts but never calls the delegate method. If I then close the app and open it again (second time), the current location get's looked up, and MKReverseGeocoder does call the delegate methods and everything works. It just doesn't want to work on the first launch, no matter how many times I call it (I have a button that can initiate the lookup).
It's totally baffling. The iOS 5 CLGeocoder works fine on initial launch and every subsequent launch. the MKReverseGeocoder does not work (because it doesn't call the delegate methods) on initial launch, but does on subsequent launches.
Below is the relevant code:
-(void)reverseGeocoder:(MKReverseGeocoder *)geocoder didFailWithError:(NSError *)error{
NSLog(#"error getting location:%#", error);
self.reverseGeocoder = nil;
[[NSNotificationCenter defaultCenter] postNotificationName:#"errorGettingLocation" object:nil userInfo:[NSDictionary dictionaryWithObject:error forKey:#"error"]];
}
- (void)reverseGeocoder:(MKReverseGeocoder *)geocoder didFindPlacemark:(MKPlacemark *)pm
{
//update placemark and get weather from correct placemark
placemark = pm;
NSLog(#"singleton placemark: %#",placemark);
[self getLocationFromPlacemark];
self.reverseGeocoder = nil;
}
- (void) getReverseGeoCode:(CLLocation *)newLocation
{
NSLog(#"reversGeocode starting for location: %#", newLocation);
NSString *ver = [[UIDevice currentDevice] systemVersion];
float ver_float = [ver floatValue];
if (ver_float < 5.0) {
//reverseGeocoder = nil;
self.reverseGeocoder = [[MKReverseGeocoder alloc] initWithCoordinate:newLocation.coordinate];
self.reverseGeocoder.delegate = self;
[self.reverseGeocoder start];
if(self.reverseGeocoder.isQuerying){
NSLog(#"self.reverseGeocoder querying");
NSLog(#"self.reverseGeocoder delegate %#", self.reverseGeocoder.delegate);
NSLog(#"self %#", self);
}else {
NSLog(#"geocoder not querying");
}
}
else {
[reverseGeocoder5 reverseGeocodeLocation:newLocation completionHandler:^(NSArray *placemarks, NSError *error){
if([placemarks count]>0){
placemark = [placemarks objectAtIndex:0];
[self getLocationFromPlacemark];
}
else{
if (error) {
NSLog(#"error reverseGeocode: %#",[error localizedDescription]);
}
}
}];
}
}
Also, I am setting reverserGeocoder as a (nonatomic, strong) property and synthesizing it. Remember, this works fine after a clean launch the second time (when there is a weather UIView already loaded). The calls to log aren't even getting hit (which is why I'm assuming the delegate methods aren't getting hit).
Any input would be GREATLY appreciated!
Thanks!
I ended up figuring this out... well, kind of.
Instead of using MKReverseGeocoder, I'm just doing a static lookup to google maps ala How to deal with MKReverseGeocoder / PBHTTPStatusCode=503 errors in iOS 4.3?
Works perfectly.
MKReverseGeocoder doesn't exist anymore... in iOS 5
You have to import CoreLocation and use CLGeocoder. Below is just some example sample code :
CLGeocoder *geoCoder = [[CLGeocoder alloc] init];
CLLocation *location = [[CLLocation alloc] initWithLatitude:37.33188 longitude:-122.029497];
[geoCoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemark, NSError *error) {
CLPlacemark *place = [placemark objectAtIndex:0];
if (error) {
NSLog(#"fail %#", error);
} else {
NSLog(#"return %#", place.addressDictionary);
}
}];
Related
I am making iOS 9 Safari AdBlocker app.
I am using the module of AdBlockPlusSafari.
App works good on simulator.
But when try to run it on device(iPhone6), it fails to reload contentBlocker.
[SFContentBlockerManager
reloadContentBlockerWithIdentifier:self.contentBlockerIdentifier
completionHandler:^(NSError *error) {
if (error) {
NSLog(#"Error in reloadContentBlocker: %#", error);
}
dispatch_async(dispatch_get_main_queue(), ^{
wSelf.reloading = NO;
[wSelf checkActivatedFlag];
if (completion) {
completion(error);
}
});
}];
it gives error
Error Domain=ContentBlockerErrorDomain Code=3 "(null)"
It caused by accessing the values in NSUserDefault (App Group).
- (instancetype)init
{
if (self = [super init])
{
_bundleName = [[[[[NSBundle mainBundle] bundleIdentifier] componentsSeparatedByString:#"."] subarrayWithRange:NSMakeRange(0, 2)] componentsJoinedByString:#"."];
NSString *group = [NSString stringWithFormat:#"group.%#.%#", _bundleName, #"AdBlockerPro"];
NSLog(#"Group name: %#", group);
_adblockProDetails = [[NSUserDefaults alloc] initWithSuiteName:group];
[_adblockProDetails registerDefaults:
#{ AdblockProActivated: #NO,
AdblockProEnabled: #YES
}];
_enabled = [_adblockProDetails boolForKey:AdblockProEnabled];
_activated = [_adblockProDetails boolForKey:AdblockProActivated];
}
return self;
}
The App Group name in host app and safari extension is same.
But in Safari extension, when app accesses the setting in NSUserDefault, it gives me the error.
In Project setting/Capabilities, I did all for App Group. In app id, it involves app group name exactly.
This happens on only device. On simulator, it works good. I can't find the reason of this error.
Please help me if you are experienced this.
Looking forward to your help.
I found the reason myself.
I have put something (NSMutableDictionary) in app group container and did something to write file to extension bundle.
It is prohibited by Apple.
So I deleted all from AdBlockManager (interacts with app group) except flag variables (Boolean type).
And I proceeded the file management using NSFileManager.
http://www.atomicbird.com/blog/sharing-with-app-extensions
Finally, app extension is working for me on device.
Good luck!
I'm trying to create a Core Data, document based app but with the limitation that only one document can be viewed at a time (it's an audio app and wouldn't make sense for a lot of docs to be making noise at once).
My plan was to subclass NSDocumentController in a way that doesn't require linking it up to any of the menu's actions. This has been going reasonably but I've run into a problem that's making me question my approach a little.
The below code works for the most part except if a user does the following:
- Tries to open a doc with an existing 'dirty' doc open
- Clicks cancel on the save/dont save/cancel alert (this works ok)
- Then tries to open a doc again. For some reason now the openDocumentWithContentsOfURL method never gets called again, even though the open dialog appears.
Can anyone help me work out why? Or perhaps point me to an example of how to do this right? It feels like something that must have been implemented by a few people but I've not been able to find a 10.7+ example.
- (BOOL)presentError:(NSError *)error
{
if([error.domain isEqualToString:DOCS_ERROR_DOMAIN] && error.code == MULTIPLE_DOCS_ERROR_CODE)
return NO;
else
return [super presentError:error];
}
- (id)openUntitledDocumentAndDisplay:(BOOL)displayDocument error:(NSError **)outError
{
if(self.currentDocument) {
[self closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(openUntitledDocumentAndDisplayIfClosedAll: didCloseAll: contextInfo:)
contextInfo:nil];
NSMutableDictionary* details = [NSMutableDictionary dictionary];
[details setValue:#"Suppressed multiple documents" forKey:NSLocalizedDescriptionKey];
*outError = [NSError errorWithDomain:DOCS_ERROR_DOMAIN code:MULTIPLE_DOCS_ERROR_CODE userInfo:details];
return nil;
}
return [super openUntitledDocumentAndDisplay:displayDocument error:outError];
}
- (void)openUntitledDocumentAndDisplayIfClosedAll:(NSDocumentController *)docController
didCloseAll: (BOOL)didCloseAll
contextInfo:(void *)contextInfo
{
if(self.currentDocument == nil)
[super openUntitledDocumentAndDisplay:YES error:nil];
}
- (void)openDocumentWithContentsOfURL:(NSURL *)url
display:(BOOL)displayDocument
completionHandler:(void (^)(NSDocument *document, BOOL documentWasAlreadyOpen, NSError *error))completionHandler NS_AVAILABLE_MAC(10_7)
{
NSLog(#"%s", __func__);
if(self.currentDocument) {
NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:[url copy], #"url",
[completionHandler copy], #"completionHandler",
nil];
[self closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(openDocumentWithContentsOfURLIfClosedAll:didCloseAll:contextInfo:)
contextInfo:(__bridge_retained void *)(info)];
} else {
[super openDocumentWithContentsOfURL:url display:displayDocument completionHandler:completionHandler];
}
}
- (void)openDocumentWithContentsOfURLIfClosedAll:(NSDocumentController *)docController
didCloseAll: (BOOL)didCloseAll
contextInfo:(void *)contextInfo
{
NSDictionary *info = (__bridge NSDictionary *)contextInfo;
if(self.currentDocument == nil)
[super openDocumentWithContentsOfURL:[info objectForKey:#"url"] display:YES completionHandler:[info objectForKey:#"completionHandler"]];
}
There's a very informative exchange on Apple's cocoa-dev mailing list that describes what you have to do in order to subclass NSDocumentController for your purposes. The result is that an existing document is closed when a new one is opened.
Something else you might consider is to mute or stop playing a document when its window resigns main (i.e., sends NSWindowDidResignMainNotification to the window's delegate), if only to avoid forcing what might seem to be an artificial restriction on the user.
I know it's been a while, but in case it helps others....
I had what I think is a similar problem, and the solution was to call the completion handler when my custom DocumentController did not open the document, e.g.:
- (void)openDocumentWithContentsOfURL:(NSURL *)url display:(BOOL)displayDocument completionHandler:(void (^)(NSDocument * _Nullable, BOOL, NSError * _Nullable))completionHandler {
if (doOpenDocument) {
[super openDocumentWithContentsOfURL:url display:displayDocument completionHandler:completionHandler];
} else {
completionHandler(NULL, NO, NULL);
}
}
When I added the completionHandler(NULL, NO, NULL); it started working for more than a single shot.
I've been experimenting with CLLocationManager's startMonitoringSignificantLocationChanges and I've run into some problems with Core Data. It turns out that since iOS 5.0, Core Data defaults to using NSFileProtectionCompleteUntilFirstUserAuthentication. This means that if a passcode is set, the persistent store is unavailable from the time the device is turned on until the time the passcode is first entered. If you're using location updates, it's possible your app may get launched during that time, and Core Data will get an error trying to load the persistent store.
Obviously switching to NSFileProtectionNone would be the easiest way to solve this. I'd prefer not to though—I'm not storing anything super sensitive in the database, but these location updates aren't super critical either.
I know I can use [[UIApplication sharedApplication] isProtectedDataAvailable] to check whether the data has been unlocked yet, and I can use applicationProtectedDataWillBecomeUnavailable: in my application delegate to respond appropriately once it is unlocked. This seems messy to me though—I'll have to add in a bunch of extra checks to make sure nothing goes wrong if the persistent store is unavailable, re-setup a bunch of things once it does become available, and so on. And that extra code doesn't offer much benefit—the app still won't be able to do anything if it launches in this state.
So I guess I'm just not sure which is the more "proper" way to deal this:
Switch to NSFileProtectionNone.
Add in the extra checks to skip over things if the store is unavailable, and use applicationProtectedDataWillBecomeUnavailable: to set things up again once it is.
If the app is launched in the background ([[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground) and protected data is unavailable ([[UIApplication sharedApplication] isProtectedDataAvailable] == NO)) just call exit(0) (or something similar) to quit the app. On one hand this seems like the simplest solution, and I don't really see any downsides. But it also seems… "wrong"? I guess I can't decide if it's a clean solution or just a lazy one.
Something else I'm just not thinking of?
After thinking this over for a while I've come up with a solution I'm happy with. One thing to consider with the exit(0) option is that if the user takes a while to unlock the device, the app could be continually loading, quitting, and reloading. Whereas if you simply prevent the app from doing much, it will probably only have to load once, and will most likely be more efficient. So I decided to try my option 3 and see how messy it really was. It turned out to be simpler than I thought.
First I added a BOOL setupComplete property to my app delegate. This gives me an easy way to check if the app was fully launched at various points. Then in application:didFinishLaunchingWithOptions: I attempt to initialize the managed object context, then do something like this:
NSManagedObjectContext *moc = [self managedObjectContext];
if (moc) {
self.setupComplete = YES;
[self setupWithManagedObjectContext:moc];
} else {
UIApplication *app = [UIApplication sharedApplication];
if ([app applicationState] == UIApplicationStateBackground && ![app isProtectedDataAvailable]) {
[app beginIgnoringInteractionEvents];
} else [self presentErrorWithTitle:#"There was an error opening the database."];
}
setupWithManagedObjectContext: is just a custom method that finishes setting up. I'm not sure the beginIgnoringInteractionEvents is necessary, but I added it to be on the safe side. That way when the app is brought to the front, I can be sure the interface is frozen until setup is complete. It might avoid a crash if an eager user is tapping anxiously.
Then in applicationProtectedDataDidBecomeAvailable: I call something like this:
if (!self.setupComplete) {
NSManagedObjectContext *moc = [self managedObjectContext];
if (moc) {
self.setupComplete = YES;
[self setupWithManagedObjectContext:moc];
UIApplication *app = [UIApplication sharedApplication];
if ([app isIgnoringInteractionEvents]) [app endIgnoringInteractionEvents];
} else [self presentErrorWithTitle:#"There was an error opening the database."];
}
That finishes the setup and re-enables the interface. That's most of the work, but you'll also need to check through your other code to make sure nothing that relies on Core Data is getting called before your persistent store is available. One thing to watch out for is that applicationWillEnterForeground and applicationDidBecomeActive may get called before applicationProtectedDataDidBecomeAvailable if the user launches the app from this background state. So in various places I've added if (self.setupComplete) { … } to make sure nothing runs before it's ready. I also had a couple of places where I needed to refresh the interface once the database was loaded.
In order to (partially) test this without a lot of driving around, I temporarily modified application:didFinishLaunchingWithOptions: to not set up the database:
NSManagedObjectContext *moc = nil; // [self managedObjectContext];
if (moc) {
self.setupComplete = YES;
[self setupWithManagedObjectContext:moc];
} else {
UIApplication *app = [UIApplication sharedApplication];
// if ([app applicationState] == UIApplicationStateBackground && ![app isProtectedDataAvailable]) {
[app beginIgnoringInteractionEvents];
// } else [self presentErrorWithTitle:#"There was an error opening the database."];
}
Then I moved my code in applicationProtectedDataDidBecomeAvailable: over to applicationWillEnterForeground:. That way I could launch the app, make sure nothing unexpected happens, press the home button, open the app again, and make sure everything was working. Since the actual code requires moving a significant distance and waiting five minutes each time, this gave me a good way to approximate what was happening.
One last thing that tripped me up was my persistent store coordinator. A typical implementation might look something like this:
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator != nil) return _persistentStoreCoordinator;
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"Test.sqlite"];
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
return _persistentStoreCoordinator;
}
This is loosely based on Apple's sample code, which does explain in comments that you need to handle the error appropriately. My own code does a bit more than this, but one thing I hadn't considered is that if there's an error loading the persistent store, this will return a non-nil result! That was allowing all my other code to proceed as though it was working correctly. And even if persistentStoreCoordinator was called again, it would just return the same coordinator, without a valid store, instead of trying to load the store again. There are various ways you could deal with this, but to me it seemed best to not set _persistentStoreCoordinator unless it was able to add the store:
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator != nil) return _persistentStoreCoordinator;
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"Test.sqlite"];
NSError *error = nil;
NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if ([coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
_persistentStoreCoordinator = coordinator;
} else {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
return _persistentStoreCoordinator;
}
I have experienced, that you have to check
[[UIApplication sharedApplication] isProtectedDataAvailable]
and process
applicationProtectedDataWillBecomeUnavailable
to be sure you don't access a protected file.
Checking for
managedObjectContext
did not work for me.
I have a straightforward NSDocument-based Mac OS X app in which I am trying to implement iCloud Document storage. I'm building with the 10.7 SDK.
I have provisioned my app for iCloud document storage and have included the necessary entitlements (AFAICT). The app builds, runs, and creates the local ubiquity container Documents directory correctly (this took a while, but that all seems to be working). I am using the NSFileCoordinator API as Apple recommended. I'm fairly certain I am using the correct UbiquityIdentifier as recommended by Apple (it's redacted below tho).
I have followed Apple's iCloud Document storage demo instructions in this WWDC 2011 video closely:
Session 107 AutoSave and Versions in Lion
My code looks almost identical to the code from that demo.
However, when I call my action to move the current document to the cloud, I experience liveness problems when calling the -[NSFileManager setUbiquitous:itemAtURL:destinationURL:error:] method. It never returns.
Here is the relevant code from my NSDocument subclass. It is almost identical to Apple's WWDC demo code. Since this is an action, this is called on the main thread (as Apple's demo code showed). The deadlock occurs toward the end when the -setUbiquitous:itemAtURL:destinationURL:error: method is called. I have tried moving to a background thread, but it still never returns.
It appears that a semaphore is blocking while waiting for a signal that never arrives.
When running this code in the debugger, my source and destination URLs look correct, so I'm fairly certain they are correctly calculated and I have confirmed the directories exist on disk.
Am I doing anything obviously wrong which would lead to -setUbiquitous never returning?
- (IBAction)moveToOrFromCloud:(id)sender {
NSURL *fileURL = [self fileURL];
if (!fileURL) return;
NSString *bundleID = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleIdentifier"];
NSString *appID = [NSString stringWithFormat:#"XXXXXXX.%#.macosx", bundleID];
BOOL makeUbiquitous = 1 == [sender tag];
NSURL *destURL = nil;
NSFileManager *mgr = [NSFileManager defaultManager];
if (makeUbiquitous) {
// get path to local ubiquity container Documents dir
NSURL *dirURL = [[mgr URLForUbiquityContainerIdentifier:appID] URLByAppendingPathComponent:#"Documents"];
if (!dirURL) {
NSLog(#"cannot find URLForUbiquityContainerIdentifier %#", appID);
return;
}
// create it if necessary
[mgr createDirectoryAtURL:dirURL withIntermediateDirectories:NO attributes:nil error:nil];
// ensure it exists
BOOL exists, isDir;
exists = [mgr fileExistsAtPath:[dirURL relativePath] isDirectory:&isDir];
if (!(exists && isDir)) {
NSLog(#"can't create local icloud dir");
return;
}
// append this doc's filename
destURL = [dirURL URLByAppendingPathComponent:[fileURL lastPathComponent]];
} else {
// get path to local Documents folder
NSArray *dirs = [mgr URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
if (![dirs count]) return;
// append this doc's filename
destURL = [[dirs objectAtIndex:0] URLByAppendingPathComponent:[fileURL lastPathComponent]];
}
NSFileCoordinator *fc = [[[NSFileCoordinator alloc] initWithFilePresenter:self] autorelease];
[fc coordinateWritingItemAtURL:fileURL options:NSFileCoordinatorWritingForMoving writingItemAtURL:destURL options:NSFileCoordinatorWritingForReplacing error:nil byAccessor:^(NSURL *fileURL, NSURL *destURL) {
NSError *err = nil;
if ([mgr setUbiquitous:makeUbiquitous itemAtURL:fileURL destinationURL:destURL error:&err]) {
[self setFileURL:destURL];
[self setFileModificationDate:nil];
[fc itemAtURL:fileURL didMoveToURL:destURL];
} else {
NSWindow *win = ... // get my window
[self presentError:err modalForWindow:win delegate:nil didPresentSelector:nil contextInfo:NULL];
}
}];
}
I don't know if these are the source of your problems, but here are some things I'm seeing:
-[NSFileManager URLForUbiquityContainerIdentifier:] may take a while, so you shouldn't invoke it on the main thread. see the "Locating the Ubiquity Container" section of this blog post
Doing this on the global queue means you should probably use an allocated NSFileManager and not the +defaultManager.
The block passed to the byAccessor portion of the coordinated write is not guaranteed to be called on any particular thread, so you shouldn't be manipulating NSWindows or presenting modal dialogs or anything from within that block (unless you've dispatched it back to the main queue).
I think pretty much all of the iCloud methods on NSFileManager will block until things complete. It's possible that what you're seeing is the method blocking and never returning because things aren't configured properly. I'd double and triple check your settings, maybe try to simplify the reproduction case. If it still isn't working, try filing a bug or contacting DTS.
Just shared this on Twitter with you, but I believe when using NSDocument you don't need to do any of the NSFileCoordinator stuff - just make the document ubiquitous and save.
Hmm,
did you try not using a ubiquity container identifier in code (sorry - ripped out of a project so I've pseudo-coded some of this):
NSFileManager *fm = [NSFileManager defaultManager];
NSURL *iCloudDocumentsURL = [[fm URLForUbiquityContainerIdentifier:nil] URLByAppendingPathComponent:#"Documents"];
NSURL *iCloudFileURL = [iCloudDocumentsURL URLByAppendingPathComponent:[doc.fileURL lastPathComponent]];
ok = [fm setUbiquitous:YES itemAtURL:doc.fileURL destinationURL:iCloudRecipeURL error:&err];
NSLog(#"doc moved to iCloud, result: %d (%#)",ok,doc.fileURL.fileURL);
And then in your entitlements file:
<key>com.apple.developer.ubiquity-container-identifiers</key>
<array>
<string>[devID].com.yourcompany.appname</string>
</array>
Other than that, your code looks almost identical to mine (which works - except I'm not using NSDocument but rolling it all myself).
If this is the first place in your code that you are accessing iCloud look in Console.app for a message like this:
taskgated: killed yourAppID [pid 13532] because its use of the com.apple.developer.ubiquity-container-identifiers entitlement is not allowed
Anytime you see this message delete your apps container ~/Library/Containers/<yourAppID>
There may also be other useful messages in Console.app that will help you solve this issue.
I have found that deleting the app container is the new Clean Project when working with iCloud.
Ok, So I was finally able to solve the problem using Dunk's advice. I'm pretty sure the issue I was having is as follows:
Sometime after the WWDC video I was using as a guide was made, Apple completed the ubiquity APIs and removed the need to use an NSFileCoordinator object while saving from within an NSDocument subclass.
So the key was to remove both the creation of the NSFileCoordinator and the call to -[NSFileCoordinator coordinateWritingItemAtURL:options:writingItemAtURL:options:error:byAccessor:]
I also moved this work onto a background thread, although I'm fairly certain that was not absolutely required to fix the issue (although it was certainly a good idea).
I shall now submit my completed code to Google's web crawlers in hopes of assisting future intrepid Xcoders.
Here's my complete solution which works:
- (IBAction)moveToOrFromCloud:(id)sender {
NSURL *fileURL = [self fileURL];
if (!fileURL) {
NSBeep();
return;
}
BOOL makeUbiquitous = 1 == [sender tag];
if (makeUbiquitous) {
[self displayMoveToCloudDialog];
} else {
[self displayMoveFromCloudDialog];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self doMoveToOrFromCloud:makeUbiquitous];
});
}
- (void)doMoveToOrFromCloud:(BOOL)makeUbiquitous {
NSURL *fileURL = [self fileURL];
if (!fileURL) return;
NSURL *destURL = nil;
NSFileManager *mgr = [[[NSFileManager alloc] init] autorelease];
if (makeUbiquitous) {
NSURL *dirURL = [[MyDocumentController instance] ubiquitousDocumentsDirURL];
if (!dirURL) return;
destURL = [dirURL URLByAppendingPathComponent:[fileURL lastPathComponent]];
} else {
// move to local Documentss folder
NSArray *dirs = [mgr URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
if (![dirs count]) return;
destURL = [[dirs firstObject] URLByAppendingPathComponent:[fileURL lastPathComponent]];
}
NSError *err = nil;
void (^completion)(void) = nil;
if ([mgr setUbiquitous:makeUbiquitous itemAtURL:fileURL destinationURL:destURL error:&err]) {
[self setFileURL:destURL];
[self setFileModificationDate:nil];
completion = ^{
[self hideMoveToFromCloudDialog];
};
} else {
completion = ^{
[self hideMoveToFromCloudDialog];
NSWindow *win = [[self canvasWindowController] window];
[self presentError:err modalForWindow:win delegate:nil didPresentSelector:nil contextInfo:NULL];
};
}
dispatch_async(dispatch_get_main_queue(), completion);
}
I have hit the proverbial wall trying to figure out how to populate an NSImage with data returned from an asynchronous NSURLConnection in my desktop app (NOT an iPhone application!!).
Here is the situation.
I have a table that is using custom cells. In each custom cell is an NSImage which is being pulled from a web server. In order to populate the image I can do a synchronous request easily:
myThumbnail = [[NSImage alloc] initWithContentsOfFile:myFilePath];
The problem with this is that the table blocks until the images are populated (obviously because it's a synchronous request). On a big table this makes scrolling unbearable, but even just populating the images on the first run can be tedious if they are of any significant size.
So I create an asynchronous request class that will retrieve the data in its own thread as per Apple's documentation. No problem there. I can see the data being pulled and populated (via my log files).
The problem I have is once I have the data, I need a callback into my calling class (the custom table view).
I was under the impression that I could do something like this, but it doesn't work because (I'm assuming) that what my calling class really needs is a delegate:
NSImage * myIMage;
myImage = [myConnectionClass getMyImageMethod];
In my connection class delegate I can see I get the data, I just don't see how to pass it back to the calling class. My connectionDidFinishLoading method is straight from the Apple docs:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// do something with the data
// receivedData is declared as a method instance elsewhere
NSLog(#"Succeeded! Received %d bytes of data",[receivedData length]);
// release the connection, and the data object
[connection release];
[receivedData release];
}
I am hoping this is a simple problem to solve, but I fear I am at the limit of my knowledge on this one and despite some serious Google searches and trying many different recommended approaches I am struggling to come up with a solution.
Eventually I will have a sophisticated caching mechanism for my app in which the table view checks the local machine for the images before going out and getting them form the server and maybe has a progress indicator until the images are retrieved. Right now even local image population can be sluggish if the image's are large enough using a synchronous process.
Any and all help would be very much appreciated.
Solution Update
In case anyone else needs a similar solution thanks to Ben's help here is what I came up with (generically modified for posting of course). Bear in mind that I have also implemented a custom caching of images and have made my image loading class generic enough to be used by various places in my app for calling images.
In my calling method, which in my case was a custom cell within a table...
ImageLoaderClass * myLoader = [[[ImageLoaderClass alloc] init] autorelease];
[myLoader fetchImageWithURL:#"/my/thumbnail/path/with/filename.png"
forMethod:#"myUniqueRef"
withId:1234
saveToCache:YES
cachePath:#"/path/to/my/custom/cache"];
This creates an instance of myLoader class and passes it 4 parameters. The URL of the image I want to get, a unique reference that I use to determine which class made the call when setting up the notification observers, the ID of the image, whether I want to save the image to cache or not and the path to the cache.
My ImageLoaderClass defines the method called above where I set what is passed from the calling cell:
-(void)fetchImageWithURL:(NSString *)imageURL
forMethod:(NSString *)methodPassed
withId:(int)imageIdPassed
saveToCache:(BOOL)shouldISaveThis
cachePath:(NSString *)cachePathToUse
{
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:imageURL]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
// Create the connection with the request and start loading the data
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
if (theConnection) {
// Create the NSMutableData that will hold
// the received data
// receivedData is declared as a method instance elsewhere
receivedData = [[NSMutableData data] retain];
// Now set the variables from the calling class
[self setCallingMethod:methodPassed];
[self setImageId:imageIdPassed];
[self setSaveImage:shouldISaveThis];
[self setImageCachePath:cachePathToUse];
} else {
// Do something to tell the user the image could not be downloaded
}
}
In the connectionDidFinishLoading method I saved the file to cache if needed and made a notification call to any listening observers:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(#"Succeeded! Received %d bytes of data",[receivedData length]);
// Create an image representation to use if not saving to cache
// And create a dictionary to send with the notification
NSImage * mImage = [[NSImage alloc ] initWithData:receivedData];
NSMutableDictionary * mDict = [[NSMutableDictionary alloc] init];
// Add the ID into the dictionary so we can reference it if needed
[mDict setObject:[NSNumber numberWithInteger:imageId] forKey:#"imageId"];
if (saveImage)
{
// We just need to add the image to the dictionary and return it
// because we aren't saving it to the custom cache
// Put the mutable data into NSData so we can write it out
NSData * dataToSave = [[NSData alloc] initWithData:receivedData];
if (![dataToSave writeToFile:imageCachePath atomically:NO])
NSLog(#"An error occured writing out the file");
}
else
{
// Save the image to the custom cache
[mDict setObject:mImage forKey:#"image"];
}
// Now send the notification with the dictionary
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:callingMethod object:self userInfo:mDict];
// And do some memory management cleanup
[mImage release];
[mDict release];
[connection release];
[receivedData release];
}
Finally in the table controller set up an observer to listen for the notification and send it off to the method to handle re-displaying the custom cell:
-(id)init
{
[super init];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:#selector(updateCellData:) name:#"myUniqueRef" object:nil];
return self;
}
Problem solved!
My solution is to use Grand Central Dispatch (GCD) for this purpose, you could save the image to disc too in the line after you got it from the server.
- (NSView *)tableView:(NSTableView *)_tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
SomeItem *item = [self.items objectAtIndex:row];
NSTableCellView *cell = [_tableView makeViewWithIdentifier:tableColumn.identifier owner:self];
if (item.artworkUrl)
{
cell.imageView.image = nil;
dispatch_async(dispatch_queue_create("getAsynchronIconsGDQueue", NULL),
^{
NSURL *url = [NSURL URLWithString:item.artworkUrl];
NSImage *image = [[NSImage alloc] initWithContentsOfURL:url];
cell.imageView.image = image;
});
}
else
{
cell.imageView.image = nil;
}
return cell;
}
(I am using Automatic Reference Counting (ARC) therefore there are no retain and release.)
Your intuition is correct; you want to have a callback from the object which is the NSURLConnection’s delegate to the controller which manages the table view, which would update your data source and then call -setNeedsDisplayInRect: with the rect of the row to which the image corresponds.
Have you tried using the initWithContentsOfURL: method?