I'm working with an app that requests data from an OAuth2.0 protected server. When I use the GTM OAuth Library to retrieve data, the program continues to run while the data is being downloaded in the background. I need some sort of mechanism to either force my application to wait until the didFinishWithData selector is called,or I need a way to notify my ViewController of the download's completion, so I can then utilize the data immediately.
I've tried conditional blocks, but those aren't doing it for me. I've also tried polling the object whose data I'm interested in, but if I do that, the data never seems to download. I've heard I can somehow utilize the Notification Center to accomplish this task, so I'll look more into that while I'm waiting for replies here.
Here is basically what is going on:
-(void) getAlert{
// Define the URL of the API module we'd like to utilize.
NSURL *url = [[NSURL alloc] initWithString:#"https://access.active911.com/interface/open_api/api/alerts"];
// Constructs a an HTTP request object to send to the server in order to obtain data.
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setValue:#"1" forHTTPHeaderField:#"alert_days"];
// This fetcher sends the request along with the authentication header in a recognizable manner.
GTMHTTPFetcher *fetcher = [[GTMHTTPFetcher alloc] initWithRequest:request];
// Attach the OAuth credentials for the fetcher's use.
[fetcher setAuthorizer:auth];
// Execute the operation.
[fetcher waitForCompletionWithTimeout:10];
NSLog(#"About to get alert");
[fetcher beginFetchWithDelegate:self didFinishSelector:#selector(responseHandler:finishedWithData:finishedWithError:)];
NSLog(#"got alert");
}
-(void)responseHandler:(id)valueNotUsed finishedWithData:(NSData *)data finishedWithError:(NSError *)error{
// Retrieve the server data in a usable object
// All that's being done here is conversion to an NSDictionary
// followed by the creation of subdictionaries from that dictionary
// until our final value can be picked directly out of the resulting dict
NSData *jsonData = [[NSData alloc] initWithData:data];
NSError *dictError;
NSDictionary* json = [NSJSONSerialization
JSONObjectWithData:jsonData //1
options:kNilOptions
error:&dictError];
NSDictionary *token = [json objectForKeyedSubscript:#"message"];
NSArray *alerts = [token objectForKeyedSubscript:#"alerts"];
NSDictionary *alertData = alerts[0];
mapCode = [alertData objectForKeyedSubscript:#"map_code"];
NSString *city = [alertData objectForKeyedSubscript:#"city"];
NSLog(#"Map code: '%#' with city '%#' and access token %#", mapCode, city, accessToken);
}
And I need to pass the mapCode to my view controller.
Thanks for the help!
First off, please rethink about having the UI halt while you fetch results from the server. This can create an extremely bad UX for the app and only should be done if absolutely necessary.
Second, does your responseHandler method work? And do you only need mapCode in the VC that responseHandler is in?
If so, you don't even need to use Notifications. Simply do:
-(void)responseHandler:(id)valueNotUsed finishedWithData:(NSData *)data finishedWithError:(NSError *)error{
...
...
mapCode = [alertData objectForKeyedSubscript:#"map_code"];
[self updateVCWithMapCode:mapCode];
}
That will call the method after the response has been received. Passing it explicitly too so you don't need to have mapCode be a property as well.
Related
I need to access data stored in AsyncStorage from iOS native Objective C code.
This is needed to get the data in sync instead of sending App event to JS and then send it back to native code.
I've just been faced with the same problem.
My solution was to move the code native side.
On iOS:
#import <React/RCTAsyncLocalStorage.h>
#import <React/RCTBridgeModule.h>
RCTResponseSenderBlock completion = ^(NSArray *response) {
NSString *theme = response[1][0][0][1];
// Setup RCTRootView with initialProperties
};
RCTAsyncLocalStorage *storage = [[RCTAsyncLocalStorage alloc] init];
dispatch_async(storage.methodQueue, ^{
[storage performSelector:#selector(multiGet:callback:) withObject:#[#"theme"] withObject:completion];
});
You could additionally use dispatch_semaphore_wait to perform this synchronously
Update:
I needed the variable in the global state not just in the component props so the above doesn't go far enough.
I've had to work this into the React Native source at the point that the Javascript source is loaded.
NSString *javascriptPrepend = [NSString stringWithFormat:#"var ThemeMode = '%#';", self.theme];
NSMutableData *prependData = [[javascriptPrepend dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
[prependData appendData:sourceCode];
sourceCode = prependData;
I'll see if they're open to a PR to allow this kind of functionality and post back here if I get it through.
Enhancing #luke’s solution – fixing an issue with the location of the data in the response; converting the JSON data to NSDictionary and ensuring the safety of the code – here is the complete method:
+(void)jsonFromLocalRNStrogeForKey:(NSString *)key completion:(void (^)(NSDictionary * _Nullable, NSError * _Nullable))completion
{
RCTResponseSenderBlock rnCompletion = ^(NSArray *response) {
NSString *jsonAsString;
if (response.count > 1) {
NSArray *response1 = response[1];
if (response1.count > 0) {
NSArray *response2 = response1[0];
if (response2.count > 1) {
jsonAsString = response2[1];
}
}
}
NSData *jsonAsData = [jsonAsString dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
NSDictionary *json = [NSJSONSerialization
JSONObjectWithData:jsonAsData options:NSJSONReadingMutableContainers error:&error];
completion(json, error);
};
RCTAsyncLocalStorage *storage = [RCTAsyncLocalStorage new];
dispatch_async(storage.methodQueue, ^{
[storage performSelector:#selector(multiGet:callback:) withObject:#[key] withObject:rnCompletion];
});
}
My current solution. Which technically isn't a direct answer to the question but does offer a work-around for sharing data between Javascript and native code, is to use NSUserDefaults.
Using this package, react-native-default-preferences to easily persist the data into NSUserdefaults and then on the native side easily retrieve them as normal.
For my purposes I am persisting a token retrieved in Javascript, to be used later by extensions of the app. This means I also persist to a UserDefaults suite using the same key as my app.
React Native (Javascript):
DefaultPreference.setName('group.myApp');
DefaultPreference.set('TOKEN', token);
Objective-C:
NSString *TOKEN = [[[NSUserDefaults alloc] initWithSuiteName:#"group.myApp"] stringForKey:#"TOKEN"];
I think you can use the code from the implementation AsyncStorage for this purpose. You can see it here. It basically loads the files that store the data using a NSFileManager and then parses them accordingly. In your case, have a look for instance at the multiGet method and then trace all the required functions present in the file. With that you should be able to re-implement them (or adapt) to fit your needs.
I am using objective-C to write an app which needs to dispatch 100 web request and the response will be handled in the call back. My question is, how can I execute web req0, wait for call back, then execute web req1 and so on?
Thanks for any tips and help.
NSURL *imageURL = [[contact photoLink] URL];
GDataServiceGoogleContact *service = [self contactService];
// requestForURL:ETag:httpMethod: sets the user agent header of the
// request and, when using ClientLogin, adds the authorization header
NSMutableURLRequest *request = [service requestForURL:imageURL
ETag: nil
httpMethod:nil];
[request setValue:#"image/*" forHTTPHeaderField:#"Accept"];
GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
fetcher.retryEnabled = YES;
fetcher.maxRetryInterval = 0.3;
fetcher.minRetryInterval = 0.3;
[fetcher setAuthorizer:[service authorizer]];
[fetcher beginFetchWithDelegate:self
didFinishSelector:#selector(imageFetcher:finishedWithData:error:)];
}
- (void)imageFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error {
if (error == nil) {
// got the data; display it in the image view. Because this is sample
// code, we won't be rigorous about verifying that the selected contact hasn't
// changed between when the fetch began and now.
// NSImage *image = [[[NSImage alloc] initWithData:data] autorelease];
// [mContactImageView setImage:image];
NSLog(#"successfully fetched the data");
} else {
NSLog(#"imageFetcher:%# failedWithError:%#", fetcher, error);
}
}
You can't simply call this code in a loop as GTMHTTPFetcher works asynchronously so the loop, as you see, will iterate and start all instances without any delay.
A simple option is to put all of the contacts into a mutable array, take the first contact from the array (remove it from the array) and start the first fetcher. Then, in the finishedWithData callback, check if the array contains anything, if it does remove the first item and start a fetch with it. In this way the fetches will run serially one after the other.
A better but more complex solution would be to create an asynchronous NSOperation (there are various guides on the web) which starts a fetch and waits for the callback before completing. The benefit of this approach is that you can create all of your operations and add them to an operation queue, then you can set the max concurrent count and run the queue - so you can run multiple fetch instances at the same time. You can also suspend the queue or cancel the operations if you need to.
Basically my app will retrieve an array of Data from database and upload it to the server(one at a time/ one after the other). I want to stop all the request when one of the data failed to upload (please check on the comments).
Code:
// 0 means need to upload to server
NSString *condition = [NSString stringWithFormat:#"isUploaded=\"0\""];
// array of ID on my database to be uploaded
NSArray *arrayOfID = [Registered distinctValuesWithAttribute:#"registeredID" predicate:[NSPredicate predicateWithFormat:condition]];
// loop every index and upload it to server
for (int i=0; i<arrayOfID.count && !isBreak; i++) {
// get the entity using ID
NSString *condition = [NSString stringWithFormat:#"registeredID=\"%#\"",[arrayOfID objectAtIndex:i]];
Registered *entity = [Registered getWithPredicate:[NSPredicate predicateWithFormat:condition]];
if (entity) {
__weak ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[self setRequest:request withEntity:entity]; // set delegate,POST,etc.
[request setCompletionBlock:^{
// returns dictionary (success/failed)
NSDictionary *dict = [[request responseString] JSON];
if ([[dict valueForKey:#"status"] isEqualToString:#"success"]) {
// set IsUploaded to 1 after successful upload to server
[entity setIsUploaded:[NSNumber numberWithInt:1]];
[Registered commit];
// Any necessary ideas that would make my code better
// and continue the POST request and proceed to the next entity???
}
else {
// I want to cancel all the request here and get out to this loop
}
}];
[request setFailedBlock:^{
// I want to cancel all the request here and get out to this loop
}];
// start startSynchronous
[request startSynchronous];
} else {
[GlobalMethods ShowAlertView:#"Database Error" message:#"Please try again later"];
isBreak=YES;
}
}
You could tweak your code slightly and use the provided ASINetworkQueue class. From the documentation, if one request in the queue fails, by default the rest are cancelled automatically.
You can use a break statement to immediately exit a loop. However, because the callbacks are asynchronous, putting one in the failure block will not have the effect you're looking for...assuming the failure block is fired in reaction to a server response and not something in the API itself, that loop will complete and fire off all your requests--and the enclosing method will return--long before any of your requests has the time to come back from the server and call its failure block (this is networking code, after all). So you're not going to be able to use a failure block to interrupt the creation of additional requests in the loop; that's not how asynchronous calls work.
I am making an jsonstring. When i execute it, it works when i do it in my browser. I do this by logging the exact url and copy it in the browser. Than i get the HTTP Get that i want, but in the iPhone i only get a Bad Login.
- (IBAction)getDown:(id)sender { //perform get request
NSLog(#"beginnen met versturen");
//NSString * _barCode = [[NSUserDefaults standardUserDefaults] objectForKey:#"phoneNumber"];
//build up the request that is to be sent to the server
//NSString*jsonString = [[NSString alloc] initWithFormat:#"{\"barcode\":\"%#\"}", _barCode];
NSString*jsonString = [[NSString alloc] initWithFormat:#"{\"barcode\":\"123456\"}"];
NSString *str = [NSString stringWithFormat: #"http://server.nl/scan.php?data=%#",jsonString];
NSLog(#"%#", str);
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:str]];
NSLog(#"url: %#", request);
[request setHTTPMethod:#"GET"];
// [request addValue:#"getValues" forHTTPHeaderField:#"METHOD"]; //selects what task the server will perform
NSLog(#"met value: %#", request);
//initialize an NSURLConnection with the request
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
if(!connection){
NSLog(#"Connection Failed");
}
}
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{ // executed when the connection receives data
receivedData = data;
/* NOTE: if you are working with large data , it may be better to set recievedData as NSMutableData
and use [receivedData append:Data] here, in this event you should also set recievedData to nil
when you are done working with it or any new data received could end up just appending to the
last message received*/
}
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{ //executed when the connection fails
NSLog(#"Connection failed with error: %#",error.localizedDescription);
}
-(void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge{
/*This message is sent when there is an authentication challenge ,our server does not have this requirement so we do not need to handle that here*/
}
-(void)connectionDidFinishLoading:(NSURLConnection *)connection{
NSLog(#"Request Complete,recieved %d bytes of data",receivedData.length);
//[self.delegate requestReturnedData:receivedData];//send the data to the delegate
NSData *data = receivedData;
NSDictionary *dictionary = [NSDictionary dictionaryWithJSONData:data];
NSLog(#"%#",dictionary.JSONString ); ; // set the textview to the raw string value of the data recieved
NSString *value1 = [dictionary objectForKey:#"barcode"];
NSLog(#"%#", value1);
NSString *value2 = [dictionary objectForKey:#"product"];
NSLog(#"%#",dictionary);
NSLog(#"%#", value2);
}
Here's the log:
2013-01-10 16:31:46.550 Scanner[14875:907] http://server.nl/scan.php?data={"barcode":"123456"}
2013-01-10 16:31:46.551 Scanner[14875:907] url: <NSMutableURLRequest (null)>
2013-01-10 16:31:46.553 Scanner[14875:907] met value: <NSMutableURLRequest (null)>
**2013-01-10 16:31:46.556 Scanner[14875:907] Connection failed with error: bad URL**
When i delete the complete json from the string i get no bad url. So there might be the problem. Anyone know what i am doing wrong?
You need to encode it, before perfoming an URL request.
Best and most elegant solution would be adding a category over NSString for example, something like this:
- (NSString*)URLEncode {
// Should not be encoded:-_.
return [(NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)self, NULL, CFSTR(";/?:#&=+$,!*'()<>#%\"{}|\\^[]`~"), kCFStringEncodingUTF8) autorelease];
//
}
And when you make the request:
NSString *str = [NSString stringWithFormat: #"http://server.nl/scan.php?data=%#",jsonString];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:[str URLEncode]];
If you don't want to use additional files (even thought that would be recommended), add this method to your class:
- (NSString*)URLEncode:(NSString )yourURL {
// Should not be encoded:-_.
return [(NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)self, NULL, CFSTR(";/?:#&=+$,!'()<>#%\"{}|\\^[]`~"), kCFStringEncodingUTF8) autorelease];
}
and use
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:[self URLEncode:str]];
I don't have much information right now, I apologize, I'm in a bit of a hurry and just saw your question. But I saw your question and I remember working on a project which was essentially an HTML-based remote control for the iphone, and when the user clicked on some of the buttons for the remote, it followed the urls that opened up identical pages but had server-side code to instruct the server to pause, play, stop, etc... I DO remember that the iPhone had a bug that caused it to not be able to parse all of my URLs, even though they were correctly formatted and worked on a desktop client. That is why I switched over to POST requests (where user clicks instead activated javascript functions that set hidden form variables and then submitted forms rather than directly navigating to long URLS). Anyways, I know this may not directly apply to you, but the point is that I did find a bug in the iPhone's URL parsing, so it might not be your fault. I'll look up any new information I can find a little later. Good luck.
I am using the great RestKit Framework for an iPhone Application.
I have got a method, where I send requests to a webservice. Sometimes four or more requests per 30 seconds.
My sendMethod looks like:
- (void) sendLocation {
NSString *username = [userDefaults objectForKey:kUsernameKey];
NSString *password = [userDefaults objectForKey:kPasswordKey];
NSString *instance = [userDefaults objectForKey:kInstanceKey];
NSString *locationname = self.location.locationname;
NSString *url = [[NSString alloc] initWithFormat:#"http://www.someadress.com/%#", instance];
RKClient *client = [RKClient clientWithBaseURL:url username:username password:password];
// Building my JsonObject
NSDictionary *locationDictionary = [NSDictionary dictionaryWithObjectsAndKeys: username, #"username", locationname, #"locationname", nil];
NSDictionary *jsonDictionary = [NSDictionary dictionaryWithObjectsAndKeys:locationDictionary, #"location", nil];
NSString *JSON = [jsonDictionary JSONRepresentation];
RKParams *params = [RKRequestSerialization serializationWithData:[JSON dataUsingEncoding:NSUTF8StringEncoding]MIMEType:RKMIMETypeJSON];
[client post:#"/locations" params:params delegate:self];
}
Sometimes (especially when sending more requests after another) the value of the count property of the RKRequestQueue Object is > 1.
When my application enters background and then enters foreground the requests in the queue (when foreground is entered) are sent to my Webservice and the delegate
- (void)request:(RKRequest*)request didLoadResponse:(RKResponse*)response {)
for all requests will be called.
So the question is:
Why doesnt RestKit send some requests immediately (my Webservice doesnt receive anything while a request is stored in the queue)???
Does anyone know a solution or had/has the same problem?
I noticed this line where you create the RKClient:
RKClient *client = [RKClient clientWithBaseURL:url username:username password:password];
This basically creates new instance each time sendLocation method is called - are you sure this is your desired behavior? If the url, username and password does not change, you can access previously created client by calling [RKClient sharedClient]. In your current approach, new request queue is created for each new client.
Now back to the point. Take a look on this property of the RKRequestQueue:
/**
* The number of concurrent requests supported by this queue
* Defaults to 5
*/
#property (nonatomic) NSUInteger concurrentRequestsLimit;
As you can see this defaults to 5, so if you have more than that in any of your queues they will wait until the ongoing requests are processed. You also mentioned that the requests stack up when the application is moved to background, and then when it enters the foreground all of them are dispatched. You can control how your requests behave like this:
- (void)backgroundUpload {
RKRequest* request = [[RKClient sharedClient] post:#"somewhere" delegate:self];
request.backgroundPolicy = RKRequestBackgroundPolicyNone; // Take no action with regard to backgrounding
request.backgroundPolicy = RKRequestBackgroundPolicyCancel; // If the app switches to the background, cancel the request
request.backgroundPolicy = RKRequestBackgroundPolicyContinue; // Continue the request in the background
request.backgroundPolicy = RKRequestBackgroundPolicyRequeue; // Cancel the request and place it back on the queue for next activation
}
I have found this solution here. Scroll down to Background Upload/Download section.