Capturing data in NSURLSession - objective-c

Let's say I want to access a variable called key that comes from outside the NSURLSession in my callback function (see code bellow). The problem is that key seems to get deleted right when I enter the session. Is there a way to capture the key variable (like with c++ lambdas) so I have access to it inside the session?
bool FacadeIOS::uploadRequest(const std::string& key,
InterfaceCallbackHTTP* callback){
// ...setup the request
// key is not yet deleted, still had correct value
LOG("key before session: {}", key);
[[[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:
^(NSData * _Nullable data,
NSURLResponse * _Nullable response,
NSError * _Nullable error) {
// key = ""
LOG("key after session: {}", key);
// Send to callback
if(callback != nullptr)
// The callback will be called with the wrong key value = ""
callback->didUploadFile(key);
}] resume];

You're passing a reference to a string. But you need the string to live longer than the function call. So you need a copy of the string, not a reference to it. Remove the & in the signature.
Alternately, you could make an explicit copy:
std::string key = key; // Make a new copy that shadows the reference.

Related

Error Handling Objective C during no response

In my app I use following methods to POST/GET data from a remote server.
postData = [self sendSynchronousRequest:request returningResponse:&response error:&error];
- (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error
{
NSError __block *err = NULL;
NSData __block *data;
BOOL __block reqProcessed = false;
NSURLResponse __block *resp;
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable _data, NSURLResponse * _Nullable _response, NSError * _Nullable _error) {
resp = _response;
err = _error;
data = _data;
reqProcessed = true;
}] resume];
while (!reqProcessed) {
[NSThread sleepForTimeInterval:0];
}
*response = resp;
*error = err;
return data;
}
I have basic error handling for no network connectivity, and app directs users to no network connectivity viewController. But I would like to account for situations when for instance server is down or data format of api has changed, Just wondering how would I catch such errors and prevent the app from crashing.
A couple of general points:
Don't do requests synchronously. Pass in a block with code that should run when the request completes. There's no reason to tie up a thread waiting for a completion handler to run, and it is even worse if that thread is basically continuously running as yours is.
If you absolutely must do that (why?), at least use a semaphore and wait on the semaphore. That way, you aren't burning a CPU core continuously while you wait.
The way you avoid crashing is to sanity check the data you get back, e.g.
Check the length of the data and make sure you didn't get nil/empty response data.
Assuming the server sends a JSON response, wrap your NSJSONSerialization parsing in an #try block, and if it fails or throws an exception, handle the error in some useful way.
Generally assume that all data is invalid until proven otherwise. Check to make sure the data makes sense.
If you're in charge of the server side, consider passing an API version as an argument, and as you modify things in the future, make sure that incompatible changes occur only when responding to clients that request a newer API version (or, alternatively, send a response that tells the client that it needs to be upgraded, and then refuse to provide data).

How do I return variables captured from a block to the caller in Objective-C

I'm having trouble returning an asynchronous response/error pair captured in a block back to caller. Here is the code:
- (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error
{
__block NSData* b_data = nil;
__block NSURLResponse* b_response = nil;
__block NSError* b_error = nil;
dispatch_semaphore_t wait = dispatch_semaphore_create(0);
[self sendRequest:request completion:^(NSHTTPURLResponse* c_response, NSData* c_data, NSError* c_error) {
b_data = c_data;
b_response = c_response;
b_error = c_error;
dispatch_semaphore_signal(wait);
}];
dispatch_semaphore_wait(wait, DISPATCH_TIME_FOREVER);
response = &b_response; // ERROR: Assigning NSURLResponse *__strong * to NSURLResponse *__autoreleasing * changes retain/release properties of pointer
error = &b_error; // ERROR: Assigning NSError *__strong * to NSError *__autoreleasing * changes retain/release properties of pointer
return b_data;
}
Basically, I'm converting an async request to a synchronous one. I need to return the response/error pair back to the caller through a pointer-to-a-pointer. However, I'm running into some problems with ARC and blocks.
I would like to copy the response/error out of the block and return it to the caller without changing the method signature (there is a slew of legacy code that calls this method - I'm trying to gingerly replace NSURLConnection with NSURLSession).
Is there a nice way I can extract the results and return them to the caller?
Your two errors have nothing to do with the block. Those lines should be:
if (response) {
*response = b_response;
}
if (error) {
*error = b_error;
}
The if checks are required incase the caller passes nil for the parameter. You will get a crash trying to dereference a nil pointer.
It's also unnecessary to copy the response/error pair to local __block variables. You can write them directly to the parameter after checking for nil.

NSDictionary multiple values with same key?

I'm trying to create an NSDictionary that looks like it has duplicate keys, but the values are different.
The reason for this is due tot he web service that I am sending that data it - it accepts a multi-form post message and needs values like this:
SomeKey: value1
SomeKey: value2
SomeKey value 3
And not like this:
SomeKey: value1, value2, value 3...
I have the values in an NSArray - however I am not sure how to get them into an NSDictionary that looks like the above - maybe some sort of object?
Edit
So, here is how I make a web service request to our main server.
Networking Library: AFNetworking 2.0
From viewControllerA
This code runs after a previous web service request has been made:
NSDictionary *results = [[BBDataStore sharedDataStore]fetchuserIdResult];
BBSeller *seller = [[BBSeller alloc]init];
seller.alias = results[#"userAlias"];
seller.userId = results[#"userid"];
[self.sellerListIds addObject:seller.userId];
[self.sellerListAliases addObject:seller];
This splits the data into two arrays:
self.sellerListIds
Is for the userId which I am really interested in. Its just a NSString
self.sellerListAliases
Is just to display the username on screen to the user - nothing more, really. The web service I need to send this data to, only looks at userIds.
Right - then I call a method
[self setupSearchParameters:seller.userId];
Which looks like this:
-(void)setupSearchParameters: (NSString *)userId
{
self.sellerListidString = userId;
[self.parameters setObject:self.sellerListidString forKey:self.typeOfSellerSearch];
}
This allows one userId for the key: self.typeOfSellerSearch
Now, when the user presses confirm button to execute the web service search it calls a method in my data start with the self.parameters dictionary and adds other values to an existing dictionary - which include other search keywords.
Finally, it calls the network request method - which looks like this:
-(void)fetch:(NSDictionary *)urlParameters
{
if (!self.webService.isBusy){
BBWebService *webService = [[BBWebService alloc]initWithURL:self.url RequestType:GET_REQUEST UrlParameters:urlParameters];
self.webService = webService;
self.webService.webServiceId = wsID;
[self.webService setDelegate:self];
}
}
That calls this method:
-(instancetype)initWithURL:(NSString *)url RequestType:(NSString *)requestType UrlParameters:(NSDictionary *)urlParameters
{
[self getSessionManager];
if ([requestType isEqualToString:#"GET"]){
self.isBusy = YES;
[self.manager GET:url parameters:urlParameters
success:^(NSURLSessionDataTask *task, id responseObject){
NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
int statusCode = (int)response.statusCode;
[self requestDone:responseObject StatusCode:statusCode];
}failure:^(NSURLSessionDataTask *task, NSError *error){
NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
int statusCode = (int)response.statusCode;
[self requestFailed:error StatusCode:statusCode];
}];
return self;
}
From there AFNetworking 2.0 takes care of constructing the parameters into the URL and makes the request. When it comes back successful, I take the data and parse it.
Right now -this works. However, I need it to work with more than one key / value pair.
If I try #Selvin's code the web service doesn't work correctly. I spoke with the Java Develeopers and they say the web service expects this:
Key / value 1
Key / Value 2
Key / Value 3
.....
it won't work with this:
key / value1, value2, value 3///
Nor does it seem to work with this:
key2 / some random value -> key / value1
As it does't know what key2 is so it ignores it.
NSArray *paramsArray = #[#{#"SomeKey": #"value1"},
#{#"SomeKey": #"value2"},
#{#"SomeKey": #"value3"},
#{#"OtherKey": #"value4"}];
[paramsArray enumerateObjectsUsingBlock:^(NSDictionary *paramDict, NSUInteger idx, BOOL *stop){
[paramDict enumerateKeysAndObjectsUsingBlock:^(NSString *key,NSString *value, BOOL *stop){
//apply the key and value on the HTTP request
}];
}];
The framework you are using accepts the NSDictionary format. You need to edit the framework to suit your needs.
Edit
You need to edit the AFNetworking code where it applies the parameters. Your initWithURL method takes urlParameters as NSDictionary. But according to my answer you need to pass NSArray with collection of NSDIctionary elements.
What needs to be done inorder to get this work is, in AFNetworking code the urlParameters has to be checked and handled. if urlParameters is NSDictionary then let the code behave as it is. If its NSArray then you need to do the "Applying HTTP parameters" yourself.
Can you make it in such a way that the NSDictionary is inside the array?
For example:
[
{SomeKey:value1},
{SomeKey:value2},
{SomeKey:value3}
]

Using NSURLConnection Asynchronous URL Request with Block Parameter

I'm attempting to appropriately use NSURLConnection's sendAsynchronousRequest method but I'm having trouble with the block parameter. I built the block above the call and am using __block variables to attempt to retrieve the data collected within it. I've confirmed that the data within the block is correct so the block is working just fine, leaving the retrieval of the block data the problem. My assignments and return statement at the end of the method are not being handed the correct values and I assume it is because the the block has yet to finish executing before the assignment is taking place. How can I "wait" until the block has finished executing before performing the assignments without interrupting the main thread?
- (NSData *) sendAsynchronousURLRequest: (NSMutableURLRequest *)request withResponse: (NSURLResponse *)response andError:(NSError *)error
{
// Prepare the block handler.
__block NSURLResponse *retrievedBlockResponse;
__block NSError *retrievedBlockError;
__block NSData *retrivedBlockData;
void(^requestHandler)(NSURLResponse*, NSData*, NSError*) =
^(NSURLResponse *blockResponse, NSData *blockData, NSError *blockError)
{
retrievedBlockResponse = blockResponse;
retrivedBlockData = blockData;
retrievedBlockError = blockError;
// NOTE: Correct data is here...
NSLog(#"Data Inside Block:\n%#", [NSMutableDictionary DictionaryFromJSONData:blockData errorReport:nil]);
};
// Send the asynchronous request (operationQueue initialized elsewhere in class).
[NSURLConnection sendAsynchronousRequest:request queue:operationQueue completionHandler:requestHandler];
// NOTE: Reference to data cannot be established here!
// Establish references to block variables and return NSData object.
response = retrievedBlockResponse;
error = retrievedBlockError;
return retrivedBlockData;
}

Using objective-c to get coldfusion generated JSON

By going to a url test.com/test.cfm I am able to output the following:
{"COLUMNS":["OBJID","USERNAME","USERPASSWORD","USERFNAME","USERMI","USERLNAME","COMPANY","TBLCOMPANYID","TBLTITLE","USERADDRESS1","USERADDRESS2","USERCITY","USERSTATE","USERZIP","USERSIGNATUREFILE","USERBUSINESSNUMBER","USERBUSINESSEXT","USERFAXNUMBER","USERCELLNUMBER","USEROTHERNUMBER","USEREMAIL1","USEREMAIL2","USEREMAIL3","DEFAULTPROJECTID","SIGNATURE","SIGNATUREUPLOADBY","SORTORDER","DISABLESTATUS","UUID","SITEID","PROGRAMID"],
"DATA":[[1,"test",11214.0,"admin","","admin","adf Inc.",1,1,"admin","","","California","","","",null,"","","","admin#test.com","","",0,null,null,0,false,"468373c5-1234-1234-1234-3133a2bb1679",62,1]]}
To iterate through this I will first need to get the data using this?
NSMutableData *receivedData;
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:#"test.com/test.cfm"]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
// create the connection with the request
// and start loading the data
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:receivedData];
if (theConnection) {
// Create the NSMutableData to hold the received data.
// receivedData is an instance variable declared elsewhere.
NSLog(#"test = %#",receivedData);
} else {
// Inform the user that the connection failed.
}
Am I on the right track? The output says null...
You've not assigned anything to receivedData; it'll either be nil (under ARC) or an undefined value which may or may not be nil (under non-ARC). You've created an object that could be used to initiate a URL connection but done nothing with it. You're also probably not getting a valid NSURL because you've failed to specify the URI scheme (ie, the http://).
Probably the easiest thing to do (assuming at least iOS 5 and/or OS X v10.7) would be to use NSURLConnection's +sendAsynchronousRequest:queue:completionHandler: and then NSJSONSerialization to parse the result. E.g.
NSURLRequest *theRequest =
[NSURLRequest
requestWithURL:[NSURL URLWithString:#"http://test.com/test.cfm"]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
// or just use [NSURLRequest requestWithURL:[NSURL ...]], since the
// protocol cache policy and a 60s timeout are the default values anyway
[NSURLConnection
sendAsynchronousRequest:theRequest
queue:[NSOperationQueue mainQueue]
completionHandler:
^(NSHTTPURLResponse *urlResponse, NSData *data, NSError *error)
{
// catch all for connection errors...
if(
(urlResponse.statusCode < 200) ||
(urlResponse.statusCode >= 300) || // bad status code, e.g. 404
error || // error is non-nil would imply some other error
![data length] // data returned was empty
)
{
// report some sort of connection error
return;
}
NSError *jsonError = nil;
id <NSObject> returnedJSONObject =
[NSJSONSerialization
JSONObjectWithData:data options:0 error:&jsonError];
if(jsonError)
{
// server returned unintelligible JSON...
return;
}
NSLog(#"Got object %#", returnedJSONObject);
// then, e.g.
if(![jsonObject isKindOfClass:[NSDictionary class]])
{
// server didn't return a dictionary
return;
}
NSArray *columns = [jsonObject objectForKey:#"COLUMNS"];
if(![columns isKindOfClass:[NSArray class]])
{
// server returned no COLUMNS object, or that
// object wasn't an array
return;
}
NSLog(#"columns are %#", columns);
/* etc, etc */
}];
The class type checking stuff quickly gets quite tedious if you don't find a way to automate it but that's all validation stuff not directly related to your question.
What the above achieves is that it sends an asynchronous (ie, non-blocking) request for the contents of the URL. Results are accumulated on the main queue (ie, the same place you'd normally do user interactions). Once that entire HTTP operation is complete the code you specified at the bottom is called and that validates and parses the response. It does so synchronously so will block but that's not worth worrying about unless profiling shows it's worth worrying about.
The built-in parser is used for the JSON and everything down to 'Got object' is really just ensuring that the fetch and parse succeeded. It may not be complete — if you can think of anything else that might go wrong then don't assume I've ignored it on purpose.
At that point you just have an object of unknown type but it'll normally be a dictionary or an array because of the fundamentals of JSON. So the example code tests that it really is a dictionary then uses the normal NSDictionary interface to obtain an object for the 'COLUMNS' key. If you attempted to call objectForKey: on an array you'd raise an exception since arrays don't implement that method.
It's then fairly rote — the code checks that the object stored as 'COLUMNS' was an array. Per the JSON rules it could have been another dictionary or a string or one of a few other things. Possibly of interest is that the code calls isKindOfClass: to test that an object was found and that it was an array in a single call; that works because it's explicitly permissible to send any message to nil and the result will always be nil, which looks the same as a BOOL NO.