I have a class where I request information from a provider class, in which after finalizing the job (asynchronous httpRequest block) needs to invoke a method [- (void) updateCountries] in the requester class. If I am not wrong this code worked in iOS 7, but now in iOS 8 it does not.
Can you please help me to understand why?
Methods in requester class:
- (void) viewWillAppear:(BOOL)animated {
//get countries to pickerView
webAPI = [[WebAPI alloc] init];
[webAPI retrieveCountries:self];
}
- (void) updateCountries {
//update countries content for pickerView
locationDAO = [[LocationDAO alloc] init];
countriesArray = [locationDAO getCountries];
[pickerView reloadAllComponents];
}
Lines in method in provider class where error happens:
SEL updateCountries = sel_registerName("updateCountries:");
[requester performSelectorOnMainThread:updateCountries withObject:nil waitUntilDone:YES];
If you need to checkout the entire method in the provider class, here it is:
- (void) retrieveCountries:(id)requester {
// NSLog(#"engine report: firing retrieveCountries http get");
NSString *urlAsString = kRetrieveCountriesListAPI;
NSURL *url = [NSURL URLWithString:urlAsString];
NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:url];
[urlRequest setTimeoutInterval:30.0f];
[urlRequest setHTTPMethod:#"GET"];
[urlRequest setValue:#"application/json" forHTTPHeaderField:#"Content-type"];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if ([data length] >0 && error == nil){
NSString *response = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"engine report: retrieveCountries server response: %#", response);
NSArray *level0 = [[NSArray alloc] initWithObjects:[NSJSONSerialization JSONObjectWithData:[[NSData alloc] initWithData:data] options:kNilOptions error:&error], nil];
NSArray *level1 = [level0 objectAtIndex:0];
LocationDAO *locationDAO = [[LocationDAO alloc] init];
[locationDAO deleteAllFromCountries];
for (int i = 0; i < [level1 count]; i++) {
CountryVO *countryVO = [[CountryVO alloc] init];
countryVO.myID = [[[level1 objectAtIndex:i] objectForKey:#"id"] integerValue];
countryVO.name = [[level1 objectAtIndex:i] objectForKey:#"country_name"];
[locationDAO saveCountryToDatabase:countryVO];
}
SEL updateCountries = sel_registerName("updateCountries:");
[requester performSelectorOnMainThread:updateCountries withObject:nil waitUntilDone:YES];
dispatch_async(dispatch_get_main_queue(), ^(void){
});
} else if ([data length] == 0 && error == nil){
NSLog(#"Nothing was downloaded.");
} else if (error != nil) {
NSLog(#"Error happened = %#", error);
} }];
}
THANK YOU A WHOLE LOT
Remove the : from the selector specification:
SEL updateCountries = sel_registerName("updateCountries");
Your method updateCountries doesn't take any arguments. So, when creating the selector, you should only write updateCountries (instead of updateCountries: which would indicate that this method takes an argument).
The reason why your app crashes is that when you try to perform this selector, the internals of your app are looking for a method called updateCountries on requester that takes one argument. This method doesn't exist, which is why the app crashes.
Related
I have View Controller where I get data from web, parse Json, and pass string to another View Controller. If I use synchronous NSURLConnection, everything works just fine.
But if I switch to the asynchronous, then method (void)prepareForSegue:(UIStoryboardSegue *) calls before parsing Json data which I got from web.
Just jump over _jsonArray = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil] method. Any thoughts? Thank you in advance for your help. Here is my code:
-(void)getClothInfo {
NSString *allowedClothSizeToServer = [_foreignSizeToServer stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
NSString *getDataURL = [NSString stringWithFormat:#"http://xsdcompany.com/jsoncloth.php?foreignSize=%#",allowedClothSizeToServer];
NSURL *url = [NSURL URLWithString:getDataURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"GET"];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler: ^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (connectionError) {
[self showAlertWithMessage2:#"Server is Unavialable"];
} else {
_jsonArray = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
//Loop trough our jsonArray
for (int i=0; i<_jsonArray.count; i++) {
//Create our size object
_usSizeFromServer = [[_jsonArray objectAtIndex:i] objectForKey:#"usSizeCloth"];
}
}
}];
}
- (IBAction)getIt:(id)sender {
// Validate data
if ([self validData] == NO)
{
return;
}
[self getClothInfo];
[self showNextViewController];
}
-(void) showNextViewController {
[self performSegueWithIdentifier:#"GetCLothInfo" sender:nil];
}
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
ResultViewController *resultViewController = [segue destinationViewController];
resultViewController.foreignSizeToResult = [[NSString alloc] initWithFormat:#"%# size for %# is %#", [_pickerProcessor selectedCountry].countryName, [_pickerProcessor selectedCloth].clothName, [_pickerProcessor selectedSize].sizeName];
resultViewController.dataForUsSize = [[NSString alloc] initWithFormat:#"Your US size for %# is %#", [_pickerProcessor selectedCloth].clothName, _usSizeFromServer];
}
You have two options. You could call showNextViewController from the completion block inside the getClothInfo method. Or better, add a completion block parameter to your getClothInfo method and call that from the completion block for the NSURLConnection.
Something like this:
-(void)getClothInfo:(void ^(void))completion {
NSString *allowedClothSizeToServer = [_foreignSizeToServer stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
NSString *getDataURL = [NSString stringWithFormat:#"http://xsdcompany.com/jsoncloth.php?foreignSize=%#",allowedClothSizeToServer];
NSURL *url = [NSURL URLWithString:getDataURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"GET"];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler: ^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (connectionError) {
[self showAlertWithMessage2:#"Server is Unavialable"];
} else {
_jsonArray = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
//Loop trough our jsonArray
for (int i=0; i<_jsonArray.count; i++) {
//Create our size object
_usSizeFromServer = [[_jsonArray objectAtIndex:i] objectForKey:#"usSizeCloth"];
}
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
}
}];
}
- (IBAction)getIt:(id)sender {
// Validate data
if ([self validData] == NO)
{
return;
}
[self getClothInfo:^ {
[self showNextViewController];
}];
}
It seems like you want your json data to be downloaded before you segue, in that case the synchronous NSURLConnection makes sense
When you make an asynchronous NSURLConnection call, it means that the subsequent code will be executed ( in this case the performSegue).
It would help if you could explain what your expected behavior is
Register for notification when response is obtained from the connection using
[[NSNotificationCenter defaultCenter] postNotificationName:#"ResponseObtained" object:_jsonArray];
in the second view controller add observer for notification
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(handleResponse:)
name:#"ResponseObtained"
object:nil];
You can access _jasonArray in handleResponse method with
- (void)handleResponse:(NSNotification *)notif{
NSDictionary *result = [notif object]; }
I'm working on an update of an iOS app that another developer created. He was using ASIHTTPRequest to handle http requests. However, the version of the app I have to work with crashes. Since ASIHTTPRequest is not being updated anymore, I thought then that I should switch to using AFNetworking, but it's far too complicated for me to figure out.
So finally I thought I could just use NSURLConnection on its own.
Here is the original code that I'm trying to replace:
-(NSArray*)readUTF16LEFeed:(NSURL*) urlToRead{
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:urlToRead];
[request startSynchronous];
NSError *error = [request error];
if (!error) {
lastModified = [NSDate date];
NSData *response = [request responseData];//UTF-16LE
NSString* responseString = [[NSString alloc] initWithData:response encoding:NSUTF16LittleEndianStringEncoding];
DLog(#"string is: %#",responseString);
responseString = [responseString stringByReplacingOccurrencesOfString:#"ISO-8859-1" withString:#"UTF16-LE"];
NSData* data = [responseString dataUsingEncoding:NSUTF16LittleEndianStringEncoding];
return [self parseNamesFromXML:data];
}
return nil;
}
And here is what I'm trying to use instead:
-(NSArray*)readUTF16LEFeed:(NSURL*) urlToRead{
NSURLRequest *request = [NSURLRequest requestWithURL:urlToRead];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error ) {
if ([data length] >0 && error == nil)
{
// DO YOUR WORK HERE
lastModified = [NSDate date];
// NSData *response = [request responseData];//UTF-16LE
NSString* responseString = [[NSString alloc] initWithData:data encoding: NSUTF16LittleEndianStringEncoding];
DLog(#"string is: %#",responseString);
responseString = [responseString stringByReplacingOccurrencesOfString:#"ISO-8859-1" withString:#"UTF16-LE"];
NSData* data = [responseString dataUsingEncoding:NSUTF16LittleEndianStringEncoding];
return [self parseNamesFromXML:data];
}
else if ([data length] == 0 && error == nil)
{
NSLog(#"Nothing was downloaded.");
}
else if (error != nil){
NSLog(#"Error = %#", error);
}
}];
return nil;
}
However, I'm getting the error "Incompatible block pointer types sending NSArray to parameter of type void. And also "Control may reach end of non-void block."
How can I get this to work?
Make return type of your method as void. Dont return anything. Just call [self parseNamesFromXML:data]; method.
-(void)readUTF16LEFeed:(NSURL*) urlToRead{
NSURLRequest *request = [NSURLRequest requestWithURL:urlToRead];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error ) {
if ([data length] >0 && error == nil)
{
// DO YOUR WORK HERE
lastModified = [NSDate date];
// NSData *response = [request responseData];//UTF-16LE
NSString* responseString = [[NSString alloc] initWithData:data encoding: NSUTF16LittleEndianStringEncoding];
DLog(#"string is: %#",responseString);
responseString = [responseString stringByReplacingOccurrencesOfString:#"ISO-8859-1" withString:#"UTF16-LE"];
NSData* data = [responseString dataUsingEncoding:NSUTF16LittleEndianStringEncoding];
[self parseNamesFromXML:data];
}
else if ([data length] == 0 && error == nil)
{
NSLog(#"Nothing was downloaded.");
}
else if (error != nil){
NSLog(#"Error = %#", error);
}
}];
}
In parseNamesFromXML method, process your results and assign results array to a property. You can access it where ever you want.
-(void) parseNamesFromXML:(NSData *)xmlData
{
NSError *error;
//DLog(#"data is %#", [[NSString alloc] initWithData:xmlData encoding:NSUTF8StringEncoding ]);
GDataXMLDocument *doc = [[GDataXMLDocument alloc] initWithData:xmlData options:0 error:&error];
if (doc)
{
self.dataArray = [self parseXmlDoc:doc];
}
}
The completion block is called asynchronously, so by the time your code reaches return [self parseNamesFromXML:data]; the methods scope is already done (meaning the method returned nil.
Try using [NSURLConnection sendSynchronousRequest:returningResponse:error:] instead, since the original code is also synchronous.
Edit:
As Julian Król suggested, if you return something within a block, it will be counted as the return value of this block, not of the original method. But since the block does not have a return value, you'll get the compiler error.
I get an self.usersArray with 2 elements in the format:
(
{
userCreated = "2012-01-05 12:27:22";
username = Simulator;
},
{
userCreated = "2013-01-01 14:27:22";
username = "joey ";
}
)
This is gotten in a completion block after which I call another method to fetch points for these 2 users through a helper class:
-(void)getPoints{
self.usersPointsArray = [[NSMutableArray alloc] init];
for (NSDictionary *usersDictionary in self.usersArray) {
[SantiappsHelper fetchPointsForUser:[usersDictionary objectForKey:#"username"] WithCompletionHandler:^(NSArray *points){
if ([points count] > 0) {
[self.usersPointsArray addObject:[points objectAtIndex:0]];
}
NSLog(#"self.usersPointsArray %#", self.usersPointsArray);
}];
}
}
The final self.usersPointsArray log looks like:
(
{
PUNTOS = 5;
username = Simulator;
},
{
PUNTOS = 2;
username = joey;
}
)
But the problem is that the way the call for points is structured, the self.usersPointsArray is returned twice, each time with an additional object, due to the for loop, I know.
Here is the Helper class method:
+(void)fetchPointsForUser:(NSString*)usuario WithCompletionHandler:(Handler2)handler{
NSURL *url = [NSURL URLWithString:#"http://myserver.com/myapp/readpoints.php"];
NSDictionary *postDict = [NSDictionary dictionaryWithObjectsAndKeys:usuario, #"userNa", nil];
NSData *postData = [self encodeDictionary:postDict];
// Create the request
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"POST"];
[request setValue:[NSString stringWithFormat:#"%d", postData.length] forHTTPHeaderField:#"Content-Length"];
[request setValue:#"application/x-www-form-urlencoded charset=utf-8" forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:postData];
__block NSArray *pointsArray = [[NSArray alloc] init];
dispatch_async(dispatch_get_main_queue(), ^{
// Peform the request
NSURLResponse *response;
NSError *error = nil;
NSData *receivedData = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
if (error) {
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
NSLog(#"HTTP Error: %d %#", httpResponse.statusCode, error);
return;
}
return;
}
NSString *responseString = [[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding];
pointsArray = [NSJSONSerialization JSONObjectWithData:[responseString dataUsingEncoding:NSASCIIStringEncoding] options:0 error:nil];
if (handler)
handler(pointsArray);
});
}
I cannot use the self.usersPointsArray with the initial objects, only with the finalized object. It wont always be 2 elements, i actually dont know how many it will be.
What would be the way to structure it so I get a final call when the self.usersPointsArray is complete and then I reload my tableview?
I think of your problem as a standard consumer-producer problem. You can create a queue count for the amount of items that will be processed (int totalToProcess=self.usersArray.count). Each time the completion handler is hit, it will do totalToProcess--. When totalToProcess reaches 0 you have processed all of the elements in your queue and can refresh your table.
If I understand your question correctly I believe this solves your problem. If not, hopefully I can with a bit more information.
JUST started doing work with blocks... very confusing. I am using a block like this:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *myDictionary = [[mySingleton arrayPeopleAroundMe] objectAtIndex:indexPath.row];
NSMutableString *myString = [[NSMutableString alloc] initWithString:#"http://www.domain.com/4DACTION/PP_profileDetail/"];
[myString appendString:[myDictionary objectForKey:#"userID"]];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:[myString stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection
sendAsynchronousRequest:urlRequest
queue:queue
completionHandler: ^( NSURLResponse *response,
NSData *data,
NSError *error)
{
[[mySingleton dictionaryUserDetail] removeAllObjects];
[[mySingleton arrayUserDetail] removeAllObjects];
if ([data length] > 0 && error == nil) // no error and received data back
{
NSError* error;
NSDictionary *myDic = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
[mySingleton setDictionaryUserDetail:[myDic mutableCopy]];
NSArray *myArray = [myDic objectForKey:#"searchResults"];
[mySingleton setArrayUserDetail:[myArray mutableCopy]];
[self userDetailComplete];
} else if
([data length] == 0 && error == nil) // no error and did not receive data back
{
[self serverError];
} else if
(error != nil) // error
{
[self serverError];
}
}];
}
Once the connection is completed, this is called:
-(void)userDetailComplete {
ViewProfile *vpVC = [[ViewProfile alloc] init];
[vpVC setPassedInstructions:#"ViewDetail"];
[[self navigationController] pushViewController:vpVC animated:YES];
}
which caused this error to pop up:
"Tried to obtain the web lock from a thread other than the main thread or the web thread. This may be a result of calling to UIKit from a secondary thread."
The only way I got rid of the error was by changing userDetailComplete to this:
-(void)userDetailComplete {
dispatch_async(dispatch_get_main_queue(), ^{
ViewProfile *vpVC = [[ViewProfile alloc] init];
[vpVC setPassedInstructions:#"ViewDetail"];
[[self navigationController] pushViewController:vpVC animated:YES];
});
}
My question: is a new thread started automatically every time a block is used? Are there any other pitfalls I should aware of when using blocks?
Blocks do not create threads. They are closures; they just contain runnable code that can be run at some future point.
This is running on a background thread because that's what you asked it to do:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection
sendAsynchronousRequest:urlRequest
queue:queue
...
You created a new queue and then asked NSURLConnection to call you back on that queue. If you want to be called back on the main thread, pass [NSOperationQueue mainQueue]. That's usually waht you want.
When I start the simulator and the application starts and I click on the UI I get EXC_BAD_ACCESS for NSString *strLength = [NSString stringWithFormat:#"%d", [postStr length]]; and for [req setHTTPBody:[_postStr dataUsingEncoding:NSUTF8StringEncoding. I dont know why this happens. If I uninstall the app but keep the simulator open and run it again I get no errors. Any help would be great. Code is below.
#import "LocavoreRetroAPIAdapter.h"
//Class extention declares a method that is private to the class
#interface LocavoreRetroAPIAdapter ()
-(NSMutableURLRequest *)initRequest:(NSURL *)url method:(NSString *)method;
#end
#implementation LocavoreRetroAPIAdapter
//Called when this class is first initialized
-(id) initWithName:(NSString *)postStr webService:(NSString *)webService spinner: (UIActivityIndicatorView *)spinner{
if(self = [super init]){
_postStr = postStr;
_baseURL = #"http://base/api/";
_webService = webService;
_spinner = spinner;
_result = nil;
}
return self;
}
//Request to Locavore API restful web services
-(void) conn:(NSString *)method{
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{
__block NSDictionary *resultBlock = nil;
dispatch_sync(concurrentQueue, ^{
/* Download the json here */
//Create webservice address
NSString *webService = [_baseURL stringByAppendingString:_webService];
//Create the url
NSURL *url = [NSURL URLWithString:webService];
//Create error object
NSError *downloadError = nil;
//Create the request
NSMutableURLRequest *req = [self initRequest:url method:method];
if(req != nil){
//Request the json data from the server
NSData *jsonData = [NSURLConnection
sendSynchronousRequest:req
returningResponse:nil
error:&downloadError];
NSError *error = nil;
id jsonObject = nil;
if(jsonData !=nil){
/* Now try to deserialize the JSON object into a dictionary */
jsonObject = [NSJSONSerialization
JSONObjectWithData:jsonData
options:NSJSONReadingAllowFragments
error:&error];
}
//Handel the deserialized object data
if (jsonObject != nil && error == nil){
NSLog(#"Successfully deserialized...");
if ([jsonObject isKindOfClass:[NSDictionary class]]){
resultBlock = (NSDictionary *)jsonObject;
//NSLog(#"Deserialized JSON Dictionary = %#", resultBlock);
}
else if ([jsonObject isKindOfClass:[NSArray class]]){
NSArray *deserializedArray = (NSArray *)jsonObject;
NSLog(#"Deserialized JSON Array = %#", deserializedArray);
} else {
/* Some other object was returned. We don't know how to deal
with this situation, as the deserializer returns only dictionaries
or arrays */
}
}
else if (error != nil){
NSLog(#"An error happened while deserializing the JSON data.");
}else{
NSLog(#"No data could get downloaded from the URL.");
[self conn:method];
}
}
});
dispatch_sync(dispatch_get_main_queue(), ^{
/* Check if the resultBlock is not nil*/
if(resultBlock != nil){
/*Set the value of result. This will notify the observer*/
[self setResult:resultBlock];
[_spinner stopAnimating];
}
});
});
}
//Configure the request for a post/get method
- (NSMutableURLRequest *)initRequest:(NSURL *)url method:(NSString *)method{
//Create the request
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
//Get the string length
NSString *strLength = [NSString stringWithFormat:#"%d", [_postStr length]];
//Specific to requests that use method post/get
//Configure the request
if([method isEqualToString:#"POST"]){
[req addValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content- Type"];
[req addValue:strLength forHTTPHeaderField:#"Content-Length"];
[req setHTTPMethod:#"POST"];
}else if([method isEqualToString:#"GET"]){
[req addValue:#"text/xml; charset=utf-8" forHTTPHeaderField:#"Content-Type"];
[req addValue:strLength forHTTPHeaderField:#"Content-Length"];
[req setHTTPMethod:#"GET"];
}else{
return nil;
}
//Set the HTTP Body
[req setHTTPBody:[_postStr dataUsingEncoding:NSUTF8StringEncoding]];
//Return the request
return req;
}
//Called when this object is destroyed
- (void)dealloc {
NSLog(#"DEALLOC LocavoreRetroAPIAdapter");
[super dealloc];
[_baseURL release];
[_result release];
}
#end
Familiarize yourself the with memory management and object lifetime rules. Your code is crashing because you do not retain (or copy) the arguments within your init... method, and they are being deallocated. Change your init... method to:
-(id) initWithName:(NSString *)postStr webService:(NSString *)webService spinner: (UIActivityIndicatorView *)spinner{
if(self = [super init]){
_postStr = [postStr copy];
_baseURL = #"http://base/api/";
_webService = [webService copy];
_spinner = [spinner retain];
}
return self;
}
Be sure to release the three instance variables you are now copying or retaining in your dealloc method. Also call [super dealloc] as the last step in that method but that's not the source of your problem right now.
//Called when this object is destroyed
- (void)dealloc {
NSLog(#"DEALLOC LocavoreRetroAPIAdapter");
[_postStr release];
[_webService release];
[_spinner release];
[_result release];
[super dealloc];
}
Notice I removed the call to [_baseURL release] from your dealloc as you did not retain it so you do not own the object. If you didn't create an object with alloc or new, and didn't call retain or copy on it, then you don't own the object, so you must not release it.