I am using the CoreLocation's geocoder to get the CLLocation coordinates for multiple map items. The geocoder calls a completion block on completion for each item.
How do I create a similar block functionality which is called when all of these containing asynchronous geocoder calls have been completed? (I could use a manual counter. But there must be a more elegant solution)
Here's my geocoding function so far. It loops through an array of location items and starts a new geocoding process for each.
-(void)geoCodeAllItems {
for (EventItem* thisEvent in [[EventItemStore sharedStore] allItems]) {
if (![thisEvent eventLocationCLLocation]){ //Only geocode if the item has no location data yet
CLGeocoder *geocoder = [[CLGeocoder alloc]init];
[geocoder geocodeAddressString:[thisEvent eventLocationGeoQuery] completionHandler:^(NSArray *placemarks, NSError *error) {
if (error){
NSLog(#"\t Geo Code - Error - Failed to geocode";
return;
}
if (placemarks)
{
if ([placemarks count] > 1) NSLog(#"\t Geo Code - Warning - Multiple Placemarks (%i) returned - Picking the first one",[placemarks count]);
CLPlacemark* placemark = [[CLPlacemark alloc]initWithPlacemark:[placemarks objectAtIndex:0]];
CLLocationCoordinate2D placeCoord = [[placemark location]coordinate];
[thisEvent setEventLocationCLLocation:[[CLLocation alloc]initWithLatitude:placeCoord.latitude longitude:placeCoord.longitude]];
[[EventItemStore sharedStore] saveItems];
} else {
NSLog(#"\t Geo Code - Error - No Placemarks decoded");
}
}];
geocoder = nil;
}
}
}
This basically works however due to the asynchronous fashion I don't know when all geocoding activity has finally ended.
My feeling is, I either have to create an block for this or use Grand Central Dispatch but I am not really sure. I appreciate any help on this to find the right approach.
You can use a GCD dispatch group to do this. Also, I think you can make multiple requests with a single CLGeocoder.
Since we might not need to create the group and the geocoder at all, we'll create them lazily:
-(void)geocodeAllItems {
dispatch_group_t group = NULL;
CLGeocoder *geocoder = nil;
We loop over the items, skipping the ones that have already been geocoded:
for (EventItem *item in [[EventItemStore sharedStore] allItems]) {
if (item.eventLocationCLLocation)
continue;
Now that we've found one that needs geocoding, we create the geocoder and the dispatch group if we need to:
if (!geocoder) {
geocoder = [[CLGeocoder alloc] init];
group = dispatch_group_create();
}
We'll use a helper method to geocode just this item:
[self geocodeItem:item withGeocoder:geocoder dispatchGroup:group];
}
Now that we've gone through all the items, we'll check whether we geocoded any:
if (group) {
If we geocoded any, then there are blocks in the dispatch group. We'll ask the group to execute a notification block when it becomes empty:
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(#"note: all geocoding requests have completed");
});
Finally, we need to release the group to balance the +1 retain count returned by dispatch_group_create:
dispatch_release(group);
}
}
Here's the helper method that geocodes just one item:
- (void)geocodeItem:(EventItem *)item withGeocoder:(CLGeocoder *)geocoder dispatchGroup:(dispatch_group_t)group {
We “enter” the group. This increments the group's membership counter atomically:
dispatch_group_enter(group);
Then we can start the geocoding:
[geocoder geocodeAddressString:item.eventLocationGeoQuery completionHandler:^(NSArray *placemarks, NSError *error) {
if (error) {
NSLog(#"error: geocoding failed for item %#: %#", item, error);
} else {
if (placemarks.count == 0) {
NSLog(#"error: geocoding found no placemarks for item %#", item);
} else {
if (placemarks.count > 1) {
NSLog(#"warning: geocoding found %u placemarks for item %#: using the first", item, placemarks.count);
}
CLPlacemark* placemark = placemarks[0];
thisEvent.eventLocationCLLocation = placemarks[0].location;
[[EventItemStore sharedStore] saveItems];
}
}
In the geocoding completion block, after all the work is done, we “leave” the group, which decrements its membership count:
dispatch_group_leave(group);
}];
}
When the membership count goes to zero, the group will execute the notification block we added at the end of geocodeAllItems.
Take a look at NSBlockOperation used with NSOperationQueue
create an NSBlockOperation and then add all of the separate tasks as execution blocks addExecutionBlock: . Set your completion block setCompletionBlock: which is in the NSOperation super class and it will get called when all of the tasks are finished. They will by default not be run on the main thread so if you want your completion block to be executed on the main thread you will have to explicitly tell it to do so. Add the NSBlockOperation to a NSOperationQueue addOperation:(NSOperation *)operation
Reference to the Concurrency Guide page on Operation Queues
Recommend: WWDC 2012 video session 225 (skip to 16:55)
Related
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
...
}
I am looking at the Ray Wenderlich tutorial on using dispatch queues to get notified when a group of tasks complete. http://www.raywenderlich.com/63338/grand-central-dispatch-in-depth-part-2
The first code shown under "Code that works" is straight from the tutorial. The Alert view(final completion block) get executed after all 3 downloads complete.
I tried to play around with it and moved the dispatch async down in the "Code that does not work" to see what will happen if dispatch_group_create() and dispatch_group_enter() happen on different queues. In this case, the dispatch_group_enter() does not seem to register because the dispatch_group_wait() immediately completes and alert view(final completion block) is executed even before all the downloads have completed.
Can someone explain whats happening in the second case? (This is just for my understanding of how dispatch group works and I realize thats its better to put the entire function in the global concurrent queue to avoid blocking the main thread).
Code that works
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),^{
__block NSError *error;
dispatch_group_t downloadGroup = dispatch_group_create();
for (NSInteger i = 0; i < 3; i++)
{
NSURL *url;
switch (i) {
case 0:
url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];
break;
case 1:
url = [NSURL URLWithString:kSuccessKidURLString];
break;
case 2:
url = [NSURL URLWithString:kLotsOfFacesURLString];
break;
default:
break;
}
dispatch_group_enter(downloadGroup);
__block Photo *photo = [[Photo alloc] initwithURL:url
withCompletionBlock:^(UIImage *image, NSError *_error) {
if (_error) {
error = _error;
}
NSLog(#"Finished completion block for photo alloc for URL %# and photo is %#",url,photo) ;
dispatch_group_leave(downloadGroup);
}];
[[PhotoManager sharedManager] addPhoto:photo];
NSLog(#"Finished adding photo to shared manager for URL %# and photo is %#",url,photo) ;
}
dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER); // 5
dispatch_async(dispatch_get_main_queue(), ^{
if (completionBlock) {
NSLog(#"Executing completion block after download group complete") ;
completionBlock(error);
}
}) ;
}) ;
}
EDITED Code that does not work with extra NSLog statements
Code that does not work
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
__block NSError *error;
dispatch_group_t downloadGroup = dispatch_group_create();
for (NSInteger i = 0; i < 3; i++)
{
NSURL *url;
switch (i) {
case 0:
url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];
break;
case 1:
url = [NSURL URLWithString:kSuccessKidURLString];
break;
case 2:
url = [NSURL URLWithString:kLotsOfFacesURLString];
break;
default:
break;
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),^{
dispatch_group_enter(downloadGroup);
NSLog(#"Enetered group for URL %#",url) ;
__block Photo *photo = [[Photo alloc] initwithURL:url
withCompletionBlock:^(UIImage *image, NSError *_error) {
if (_error) {
error = _error;
}
NSLog(#"Finished completion block for photo alloc for URL %# and photo is %#",url,photo) ;
dispatch_group_leave(downloadGroup);
}];
[[PhotoManager sharedManager] addPhoto:photo];
NSLog(#"Finished adding photo to shared manager for URL %# and photo is %#",url,photo) ;
}) ;
}
NSLog(#"Executing wait statement") ;
dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER); // 5
dispatch_async(dispatch_get_main_queue(), ^{
if (completionBlock) {
NSLog(#"Executing completion block after download group complete") ;
completionBlock(error);
}
}) ;
}
The "dispatch_group_enter() does not seem to register" because it hasn't actually been called yet by the time that dispatch_group_wait() is called. Or, rather, it's not guaranteed to have been called. There's a race condition.
This isn't specifically about different queues. It's about concurrency and asynchronicity.
dispatch_async() just means "add a task to a list" with an implicit understanding that something, somewhere, somewhen will take tasks off of that list and execute them. It returns to its caller immediately after the task has been put on the list. It does not wait for the task to start running, let alone complete running.
So, your for loop runs very quickly and by the time it exits, it may be that none of the tasks that it has queued have started. Or, if any have started, it may be that they haven't finished entering the group.
Your code may complete its call to dispatch_group_wait() before anything has entered the group.
Usually, you want to be sure that all relevant calls to dispatch_group_enter() have completed before the call to dispatch_group_wait() is made. The easiest way to do that is to have them all happen synchronously in one execution context. That is, don't put calls to dispatch_group_enter() inside blocks that are dispatched asynchronously.
I'm experimenting with Firebase's FDataSnapshot to pull in data and I would like it to write its data to my core data using MagicalRecord.
According to Firebases "best practice" blog I need to keep a reference to the "handle" so it can be cleaned up later on. Further, they mention to put the FDSnapshot code in viewWillAppear.
I am wanting a callback so that when its finished doing its thing to update core data.
But I'm really note sure how to do that; its doing two things and giving a return at the same time.
// In viewWillAppear:
__block NSManagedObjectContext *context = [NSManagedObjectContext MR_context];
self.handle = [self.ref observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
if (snapshot.value == [NSNull null])
{
NSLog(#"Cannot find any data");
}
else
{
NSArray *snapshotArray = [snapshot value];
// cleanup to prevent duplicates
[FCFighter MR_truncateAllInContext:context];
for (NSDictionary *dict in snapshotArray)
{
FCFighter *fighter = [FCFighter insertInManagedObjectContext:context];
fighter.name = dict[#"name"];
[context MR_saveToPersistentStoreWithCompletion:^(BOOL contextDidSave, NSError *error){
if (error)
{
NSLog(#"Error - %#", error.localizedDescription);
}
}];
}
}
}];
NSFetchRequest *fr = [[NSFetchRequest alloc] initWithEntityName:[FCFighter entityName]];
fr.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]];
self.fighterList = (NSArray *) [context executeFetchRequest:fr error:nil];
[self.tableView reloadData];
In the above code, the core data reading does not wait for the firebase to complete.
Thus, my query -- how would I best combine a completion handler so that when it is complete to update core data, and reload the tableview.
Many thanks
This is a common issue when working with Asynchronous data.
The bottom line is that all processing of data returned from an async call (in this case, the snapshot) needs to be done inside the block.
Anything done outside the block may happen before the data is returned.
So some sudo code
observeEvent withBlock { snapshot
//it is here where snapshot is valid. Process it.
NSLog(#"%#", snapshot.value)
}
Oh, and a side note. You really only need to track the handle reference when you are going to do something else with it later. Other than that, you can ignore the handles.
So this is perfectly valid:
[self.ref observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
//load your array of tableView data from snapshot
// and/or store it in CoreData
//reload your tableview
}
I am trying to simply return a user's state. I understand that I need to use reverseGeocodeLocation. I would like to return the state as an NSString in the same way that I am returning the user latitude below:
- (NSString *)getUserLatitude
{
NSString *userLatitude = [NSString stringWithFormat:#"%f",
locationManager.location.coordinate.latitude];
return userLatitude;
}
I currently have this code, but I cannot get it to work. It may be because I am using (void). I am just a bit lost.
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation
*)newLocation fromLocation:(CLLocation *)oldLocation {
CLGeocoder * geoCoder = [[CLGeocoder alloc] init];
[geoCoder reverseGeocodeLocation:newLocation completionHandler:^(NSArray *placemarks,
NSError *error) {
for (CLPlacemark * placemark in placemarks) {
NSString *userState = [placemark locality];
return userState;
}
}];
}
Anyone have any ideas? Thank you!
You have to do something with the retrieved locality in the completion block. This code is executed asynchronously long after the method (with void return) has returned itself.
Usually you would call some sort of method on your own view controller or model class that passes the retrieved information.
Replace the return userState, it does not match the return type of the block.
Instead put something like:
[myViewController didFinishGettingState:userState];
You should look into block and GCD basics so that you can appreciate how this asynchnonicity works.
You are probably not understanding the way your completionHandler is working. The reverseGeocodeLocation:completionHandler: takes an handler, which is a function that will be executed when the lookup is completed and invoked with the placemarks and error as parameters.
What you have to do is to perform something meaningful in that block.
I would start checking whether any error occurred, then I would call a method for failing or success as follows
[geoCoder reverseGeocodeLocation:newLocation completionHandler:^(NSArray *placemarks, NSError *error) {
if (error != nil) {
// Something bad happened...
[self didFailRetrievingUserState:error];
} else {
// Check whether the placemark retrieved is unique
if (placemarks.count > 1) {
NSMutableArray * states = [NSMutableArray array];
for (CLPlacemark * placemark in placemarks) {
NSString * userState = [placemark locality];
[states addObject:userState];
}
[self didFinishRetrievingUserStates:states];
} else {
[self didFinishRetrievingUserState:[placemarks[0] locality]];
}
}
}];
Then of course you need to implement the three methods we are calling in the block above
- (void)didFailRetrievingUserState:(NSError *)error {
// Show error
}
- (void)didFinishRetrievingUserStates:(NSArray *)userStates {
// Do something reasonable with the multiple possible values
}
- (void)didFinishRetrievingUserState:(NSString *)userState {
// Do something reasonable with the only result you have
}
Clearly the above code is meant as a suggestion. You can make different decisions, like for instance handling all the logic inside the handler block, or not discriminating between the unique/not unique cases.
In general it's just important that you understand that the handler block is not supposed to return anything since it's a void function. It's just supposed to do something, and this something may be invoking your "delegate" methods as defined in the example.
So,here a simple method with a block
-(void)getPointsInRange:(double)radius nearByPoint:(SGPoint *)nearByPoint
{
SGStorageQuery *query = [SGStorageQuery queryWithPoint:nearByPoint layer:SimpleGeoMainLayerName];
[query setRadius:radius];
[mainClient retain];
[mainClient getRecordsForQuery:query
callback:[SGCallback callbackWithSuccessBlock:
^(id response) {
// you've got records!
// to create an array of SGStoredRecord objects...
NSArray *records = [NSArray arrayWithSGCollection:response type:SGCollectionTypeRecords];
NSLog(#"records received:%i",[records count]);
[self arrayOfPointsReceived:records];
} failureBlock:^(NSError *error) {
// handle failure
NSLog(#"getPointsInRange error:%#",[error description]);
}]];
}
the method connects to some SDK and returns an NSArray with results.
i want to find a way that the getPointsInRange method will return the NSArray.
so its signature will be -(NSArray*)getPointsInRange...
I can do it simply with delegate, but i'd like to do it all within one function.
It seems to me like you want to keep your cake and eat it, too. Or have a method that calls asynchronous code and at the same time returns the results synchronously. You can turn the method into a synchronous one, if that’s what you want:
- (void) computeSomethingAndReturnSynchronously
{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self doSomeAsynchronousOperationWithCompletion:^{
// take the call results here
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_release(semaphore);
}
This will run the asynchronous code and then block the execution until the results from the async call are available. Does that help? (I should add that I would much rather keep the code asynchronous and return the NSArray in another completion block.)