There is a method in the Google Maps SDK that uses callbacks like these, I guess it´s blocks? anyways I have never used these before. I just want to see if the method requestPanoramaNearCoordinate returns a valid Panorama near the coordinate i give it. How do i use the callback to see what the callback returns? If it returns a valid Panorama at the coordinate I want to return it - if not I´ll recursively call the method until a valid location is found
- (void) requestPanoramaNearCoordinate: (CLLocationCoordinate2D)coordinate
callback: (GMSPanoramaCallback)callback
Retrieves information about a panorama near the given coordinate. This
is an asynchronous request, callback will be called with the result.
CLLocationCoordinate2D ranLatLng = CLLocationCoordinate2DMake(ranLatitude, ranLongitude);
if ([panoService requestPanoramaNearCoordinate:ranLatLng callback:<#^(GMSPanorama *panorama, NSError *error)callback#>)
The panorama parameter contains the GMSPanorama object that the request found for you. It doesn't seem to be documented very clearly, but the usual pattern in Cocoa is that if nothing appropriate was found, the result will be nil, and the NSError object will give you a little information about why. As for re-making the request, that would look something like this I guess:
// Necessary if GMSPanoService is ill-behaved and keeps the Block
// around after calling it.
__weak GMSPanoramaService weak_panoService = panoService;
__block GMSPanoramaCallback cb;
cb = [^(GMSPanorama *panorama, NSError *error){
if( !panorama ){
// Do something with error if desired
CLLocationCoordinate2D newLatLng = /* Calc new coordinates somehow */;
[/*weak_*/panoService requestPanoramaNearCoordinate:newLatLng
callback:cb];
}
else {
// Got a panorama, do something with it
cb = nil; // Release Block
}
} copy];
[panoService requestPanoramaNearCoordinate:ranLatLng
callback:cb];
It's a block. You make the api call like this:
[aPanoramaService requestPanoramaNearCoordinate:aCoordiante callback:^(GMSPanorama *panorama, NSError *error) {
NSLog(#"the service returned a panorama=%# and an error=%#", panorama, error);
}];
One way to do the recursion is to build a method with a similar signature to the google method. An important point is to find a way for the recursion to stop, possibly because you get tired of searching.
- (void)findPanoramaAtLocation:(CLLocationCoordinate2D)location withCompletion:(void (^)(GMSPanorama *, NSError *))completion {
[self.panoramaService requestPanoramaNearCoordinate:location callback:^(GMSPanorama *panorama, NSError *error) {
if (error) {
completion(nil, error);
} else if (panorama) {
completion(panorama, nil);
} else {
if (/* should we continue searching? */) {
// compute a new location to try
double latitude = // compute new latitude
double longitude = // compute new longitude
CLLocationCoordinate2D newLocation = CLLocationCoordinate2DMake(latitude, longitude);
[self findPanoramaAtLocation:newLocation withCompletion:completion];
} else {
// no panorama and no error means we gave up
completion(nil, nil);
}
}
}];
}
This will run, calling the "callback" (it's a block) when either a panorama location is found, when an error occurs, or when your logic determines that it ought to stop searching.
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'm learning to develop iOS applications and now I'm reading some Objective-C source code.
This is a method to get user profile.
+ (void)getProfile:(void (^)(NSString *message))completion {
NSDictionary *dic = #{#"module":#"profile"};
[[self defaultManager] POST:KBaseUrl parameters:dic success:^(AFHTTPRequestOperation *operation, id responseObject) {
if ([self jsonOKForResponseObject:responseObject] && [self checkLogin:responseObject]) {
[ProfileManager sharedInstance].rank = responseObject[#"Variables"][#"space"][#"group"][#"grouptitle"];
[ProfileManager sharedInstance].credit = responseObject[#"Variables"][#"space"][#"credits"];
[ProfileManager sharedInstance].gender = responseObject[#"Variables"][#"space"][#"gender"];
completion(nil);
} else {
completion(#"fail");
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
completion(#"fail");
}];
}
My question is about the completion block.
I suppose that the completion block returns void and receives an NSString parameter.
In the block, what does completion(nil) mean?
Does that mean the block completion calls it self and send nil as parameter?
Doesn't that conflict with the parameter's type NSString*?
I'm not quite familiar with block in ObjC. Can anyone give a hint?
Yes you are right. It calls itself and sends nil as a parameter and it doesn't conflict with the NSString parameter. You are just passing nil to the NSString param.
You can call the above method like:
[YourClass getProfile:^(NSString *message) {
//message will be nill if you pass completion(nil);
}];
So when you pass the nill in the completion block, the message in the above method call will be nil!
The completion block is to notify you that your method call is complete, and at this point you can let that method pass certain paramteres , and if we consider your method:
+ (void)getProfile:(void (^)(NSString *message))completion {
NSDictionary *dic = #{#"module":#"profile"};
[[self defaultManager] POST:KBaseUrl parameters:dic success:^(AFHTTPRequestOperation *operation, id responseObject) {
if ([self jsonOKForResponseObject:responseObject] && [self checkLogin:responseObject]) {
.....
completion(#"hey sucess");
} else {
completion(#"if loop failed");
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
completion(#"some error occured");
}];
}
and when you call the method getProfile:
[YourClass getProfile:^(NSString *message) {
//Execution will reach here when the method call for getPRofile is complete and you have a result which you just sent through the completion block as a parameter which is a string and is waiting for you to process.
//you can do more from here
if([message isEqualToString:#"hey success"]){
//do something
}
if([message isEqualToString:#"if loop failed"]){
//do something
}
if([message isEqualToString:#"some error occured"]){
//do something
}
}];
As per #rmaddy comment, iis always a good practice to use BOOL to indicate the status success or fail rather than depending on a string as string can get localized/changed. We shold use the string to get more description of the error.
So your block should be:
+ (void)getProfile:(void (^)(BOOL status,NSString *message))completion {
.....
completion(YES,#"hey success");
}
and you can call it like":
[YourClass getProfile:^(BOOL status, NSString *message) {
if(status){
//get the success message
}else{
//get the fail message
}
}];
Blocks had a lot of type like :-
As a local variable:
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};
As a property:
#property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);
As a method parameter:
- (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName;
As an argument to a method call:
[someObject someMethodThatTakesABlock:^returnType (parameters) {...}];
As a typedef:
typedef returnType (^TypeName)(parameterTypes);
TypeName blockName = ^returnType(parameters) {...};
read more about it from here
Completion block does not return anything. It is just a piece of code to be executed, thats all. Though, you can give it some input where you call it so you can use the result elsewhere.
NSString *message is the input for your block so when you call your function getProfile as:
[MyClass getProfile:^(NSString *message) {
// write code to be executed when getProfile function finishes its job and sends message here.
}];
[MyClass getProfile:nil];
When used like this you're preferring not to do anything when getProfile finishes its job.
You are probably mixing your network manager's function's completion block with the one you wrote.
So, I'm trying to get all the facebook pictures I'm tagged so I can see them on the iPad, but I wanted to make this function so I can call it everytime I would need to get the url's. The problem is, after I call this function, the array is nil, because the values I get are inside a block. How do I make an array to store the data I get for later use?
-(NSArray *)getFacebookTaggedPictures
{
__block NSArray *taggedPictures = [[NSArray alloc]init];
[FBRequestConnection startWithGraphPath:#"me/photos" completionHandler:^(FBRequestConnection *connection, id result, NSError *error)
{
if(!error)
{
taggedPictures = [(NSArray *)[result data]copy];
//NSLog(#"the tagged pictures are: %#",result);
}
}];
return taggedPictures;
}
As you have correctly noticed, the array that you return is empty because your method returns before the response from the API has been received and parsed into taggedPictures inside the completionHandler block.
You must save the array inside that block. I'd recommend changing your method as follows:
-(NSArray *)getFacebookTaggedPicturesWithCompletion:(void (^)(NSArray* photos))completion;
where the calling methods would pass the appropriate completion block to handle the obtained pictures. (i.e. save them to disk, display them, do some processing on them etc.):
void (^loggerBlock)(NSArray* photos) = ^(NSArray *array) {
NSLog(#"obtained photos: %#",[array description]);
//save here instead of logging
};
[self getFacebookTaggedPicturesWithCompletion:loggerBlock];
Hope this helps.
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.
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)