I'm setting in a ViewController a NSString property by fetching a JSON table and then in a different ViewController I want to get that same property.
What is happening is when I'm trying to get the property this is nil.
I know what is the problem, I'm accessing the property in the main thread while the JSON fetching is still in progress in another thread.
I'm using the AFNETWORKING 2.0 framework to access the JSON table.
How can I wait for the property set and then use it?
I really appreciate any help you can provide.
You can do this in different ways, you can post notification from AFnetworkingJSON operation success callback like this. And observer that notification where you want to access that property. You can also pass a completionHandler to the method which can be call from success or failure callbacks.
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"link"]];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
// Post notification from here
// call completion handler if you have any
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response,
}
];
[operation start];
Related
I'm writing a gallery with images which can be loaded by url with AFNetworking.
In Init method of the ImageView object I call a function that send a request. Here:
- (void) loadWithUrl:(NSURL *)url
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:TimeOut];
[request setHTTPShouldHandleCookies:NO];
[request setHTTPShouldUsePipelining:YES];
__weak AOWImageView *safeSelf = self;
m_operation = [AFImageRequestOperation imageRequestOperationWithRequest:request
imageProcessingBlock:nil
success:^
(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image)
{
[safeSelf setImage:image];
}
failure:^
(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error)
{
[safeSelf setNoImageLabelOpaque];
}];
[m_operation start];
}
If the ImageView is outside the visible part of the screen - (void) dealloc is called. I cancel operation loading image in this method so: [m_operation cancel];. I guess that the operations are not canceled because the memory is increasing and isn't released.
I think that there is retain cycle. I want to understand how to write it right. Thanks.
I don't see a retain cycle in the code snippet you posted, so if you are leaking memory, it may be due to another part of your app's code...
Regarding "best practices" for AFNetworking and loading images-
AFNetworking has a built in category on UIImageView that has convenience methods for setting an an image view's image via a URL.
See UIImageView+AFNetworking, specifically setImageWithURL: method and related. This also has the advantage of keeping a cache (so you don't have to fetch images again if requested multiple times), which AFAIK, doesn't appear to done by AFImageRequestOperation.
I wrote a POST method that needs to return a jSON to the viewcontroller that called her.
If i add in the success block return _jsonDictionary; I will get this error:
Incompatible block pointer types sending 'id (^)(NSURLRequest *__strong, NSHTTPURLResponse *__strong, __strong id)' to parameter of type 'void (^)(NSURLRequest *__strong, NSHTTPURLResponse *__strong, __strong id)'
I guessing that because it's asynchronous, adding a return will force it to be synchronous but, I want all my POST methods for my app to be in one class so getting the data out of the JSON to variables that are declared across my app using something like valueForKey makes things a bit complicated for me.
Is that bad design?
-(NSDictionary *)getData
{
_jsonDictionary = [[NSDictionary alloc]init];
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"%#/getSomething",MainURL ]];
[AFJSONRequestOperation addAcceptableContentTypes:[NSSet setWithObject:#"text/html"]];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"POST" path:nil parameters:nil];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON)
{
_jsonDictionary = JSON;
NSLog(#"jsonDictionary: %#",_jsonDictionary);
}
failure:^(NSURLRequest *request , NSURLResponse *response , NSError *error , id JSON)
{
NSLog(#"request: %#",request);
NSLog(#"Failed: %#",[error localizedDescription]);
}];
[httpClient enqueueHTTPRequestOperation:operation];
}
Another question, why do I get this warning at the end of the above code: Control reaches end of non-void function even if i change the name of the method in .m and in .h to -(void )getData ??
If your main concern is to "get back" your data, you have three ways (maybe more, but I can only things of those three):
in your getData method, you can post a NSNotification that your viewController subscribed to before calling getData
if you are using (or plan to use) a dataManager as a singleton, your viewController can KVO on the #property of the dataManager
my favorite: in your calling viewController, you construct and pass a block to your getData method that will be called (with or w/o the result). This is exactly what you are doing when building the AFJSONRequestOperation in your example.
The error you are getting is correct. The block is not supposed to return anything, and your return _jsonDictionary; is trying just that.
What you need to do is update _jsonDictionary inside the success block (like you already do), and then invoke another function to fire up events that refresh you UI (something like calling [self.tableView refreshData]).
you're totally mixing asyn and sync ... there IS valid no _jsonDictionary that getData could return. the _jsonDictionary is only filled asynchronously when the completion blocks are called.
you need to proceed from there.... call another method for example
as for the error/warning you see ... getData is supposed to return something (NSDictionary*) or a void* (almost eqaul to an id)
to not return stuff, it is void only.
I want to execute a series of AFJSONRequestOperation in order, and be able to interrupt the queue when one fails.
At the moment, the way I do it is not reliable, as sometimes the next operation will get a chance to start.
I have a singleton to call my api endpoint
AFJSONRequestOperation *lastOperation; // Used to add dependency
NSMutableArray *operations = [NSMutableArray array]; // Operations stack
AFAPIClient *httpClient = [AFAPIClient sharedClient];
[[httpClient operationQueue] setMaxConcurrentOperationCount:1]; // One by one
And then I add the operations this way
NSMutableURLRequest *request = ...; // define request
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
// Takes care of success
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
[[httpClient operationQueue] setSuspended:YES];
[[httpClient operationQueue] cancelAllOperations];
}];
[push:operation addDependency:lastOperation];
[operations $push:operation]; // This is using ConciseKit
lastOperation = operation;
// Repeat with other operations
// Enqueue a batch of operations
[httpClient enqueueBatchOfHTTPRequestOperations:operations ...
Trouble is, sometimes the operation following the one that fails still gets a chance to start.
So it seems that that having 1 concurrent operation max and a dependency chain isn't enough to tell the queue to wait until after the failure callback is fully executed.
What's the proper way to do this ?
Thanks
The failure callback is executed on the main thread and the operation (which is running on a background thread) doesn't wait for it. So, you'd need to do some editing to prevent the next operation from starting before the operation and its completion blocks have completed.
Or, instead of putting all the operations into the queue at the start, hold the list of operations in an array and just add the next operation following each success.
I'm new to using blocks in iOS and I am thinking that's probably the crux of my problem.
I just want to build a simple static DataManager class whose sole job is to fetch data from my Restful service.
I would call this from all my various UIViewControllers (or collectionview/table controllers)
In my class i have a function that looks like this
+ (NSArray *) SearchByKeyword: (NSString*) keyword {
__block NSArray* searchResults = [[NSArray alloc] init];
NSString *baseURL = #"http://someURL.com/api/search";
NSString *requestURL = [baseURL stringByAppendingString:keyword];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:baseURL]];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET"
path:requestURL
parameters:nil];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
searchResults = [JSON valueForKeyPath:#""];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Request Failed with Error: %#, %#", error, error.userInfo);
}];
[operation start];
return searchResults;
}
However, this keeps returning zero data. Can someone suggest the right way of doing this?
You are trying to use the results of an asynchronous task (the JSON operation) as the return value for a synchronous method call, so that is why you get no data.
You could provide your view controllers with an API that takes completion blocks and failure blocks, similar to the AF networking one. View controllers can then do what they need to do with the results when they are passed into the block.
Modifying your code from your question:
typedef void (^SearchCompletionBlock)(NSArray *results);
typedef void (^SearchFailureBlock)(NSError *error);
+ (void)searchByKeyword:(NSString*)keyword completionBlock:(SearchCompletionBlock)completionBlock failureBlock:(SearchFailureBlock)failureBlock;
{
NSString *baseURL = #"http://someURL.com/api/search";
NSString *requestURL = [baseURL stringByAppendingString:keyword];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:baseURL]];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET"
path:requestURL
parameters:nil];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
if (completionBlock) {
completionBlockc([JSON valueForKeyPath:#""]);
}
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Request Failed with Error: %#, %#", error, error.userInfo);
if (failureBlock) {
failureBlock(error);
}
}];
[operation start];
}
Then clients could pass completion blocks that stored the results and reloaded their views. Something like:
^ (NSArray *results) {
self.results = results;
[self.tableView reloadData];
}
Your JSON request operation is asynchronous, meaning that it will kick off the request ([operations start], then immediately return your results, which will be empty. When the completion block runs, it assigns your data but nothing is done with it. Your search method can't return an object unless it waits for the request to complete.
You've got a few options:
Pass in a completion block to the search method which does something with the results. The completion block is called in the completion block of the request, once all the service-specific stuff (processing JSON etc) is finished. (Block inception!)
Have the completion block of the request assign a property of the data manager, then call a delegate method or notification to let others know the results are available.
I'd prefer option 1.
I'm trying to use the AFNetworking UIImageView call to load images from a URL as shown below:
[self.image setImageWithURL:[NSURL URLWithString:feed.imageURL] placeholderImage: [UIImage imageNamed:#"logo"]];
The placeholder image always shows up, but the actual image from "feed.imageURL" never does. I've verified that the URL is actually correct. I even hardcoded it to make sure, and still nothing.
My basic app setup is a tab controller...and in viewDidLoad, I call a method "fetchFeed" which performs the HTTP request to gather my JSON data.
My request block looks like:
AFJSONRequestOperation *operation = [AFJSONRequestOperation
JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
[self parseDictionary:JSON];
isLoading = NO;
[self.tableView reloadData];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Error: %#", error);
[self showNetworkError];
isLoading = NO;
[self.tableView reloadData];
}];
operation.acceptableContentTypes = [NSSet setWithObjects:#"application/json", #"text/json", #"text/javascript", #"text/html", nil];
[queue addOperation:operation];
Turns out the server I was requesting the image from was sending content-type "image/jpg" and by default AFNetworking does not support this file type.
I changed the class method in AFImageRequestOperation to look like:
+ (NSSet *)defaultAcceptableContentTypes {
return [NSSet setWithObjects:#"image/tiff", #"image/jpeg", #"image/gif", #"image/png", #"image/ico", #"image/x-icon" #"image/bmp", #"image/x-bmp", #"image/x-xbitmap", #"image/x-win-bitmap", #"image/jpg", nil];
}
and it fixed my problem.
You can manage to accept what content-type you want with this library simply changing the request like this:
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:yourURL];
[request addValue:#"image/*" forHTTPHeaderField:#"Accept"];
And call the AFNetworking method:
AFJSONRequestOperation *operation = [AFJSONRequestOperation
JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
}];
This way you will be able to override the content-type without changing the library.
AFNetworking doesn't support image/jpg MIME TYPE by default.
You can support it without modifying the AFNetworking Library
[AFImageRequestOperation addAcceptableContentType:#"image/jpg"];
All operations that manipulate the UI must be performed on the main thread. So you may need to use 'performSelectorOnMainThread:' when reloading your tableview data in the completion block.
[self.tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO]
I had a similar problem but it turned out that I was passing a URL which contained spaces in it. When I properly encoded the URL using stringByAddingPercentEscapesUsingEncoding: the images now load.