AFNetworking + MBProgressHUD unable to update HUD status - ios

In the viewDidLoad method of my viewController I wrote something like this:
- (void)viewDidLoad {
[super viewDidLoad];
self.hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
self.hud.labelText = #"Loading";
[serverData loadNewsData:self ];
}
Basically I show the HUD and start a method loadNewsData on another class, passing the viewController.
The method in the other class is like this:
-(void)loadNewsData:(LoadViewController*) loadViewController
{
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:[siteAddress stringByAppendingString:getNewsList] parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
loadViewController.hud.detailsLabelText = #"updating news";
[NewsModel setNewsData:responseObject];
loadViewController.hud.detailsLabelText = #"Finished";
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
When the request of the AFHTTPRequestOperation starts I was expecting to see the sub menu in the HUD to change in "Updating news". But nothing happens. I just see Finished at the end of the process. The requests takes like 10sec so I was expecting to see "updating news", but nothing.
I try also to this:
dispatch_async(dispatch_get_main_queue(), ^{
loadViewController.hud.detailsLabelText = #"updating news"; });
Thinking I have to be in the UI queue to update some UI stuff, but it did not work either.
I'm a newbie probably I miss something.
thanks for any good advices.

Try to move loadViewController.hud.detailsLabelText = #"updating news"; before you start GET request. If you keep this inside the success block you won't see the text.

This link should help you out... NSURLConnection
It describes the NSURLConnection delegate method to use and how to use it for the exact situation your in. I understand that this isn't an AFNetworking solution but if your still looking for an AFNetworking solution, check out this method - (void)setDownloadProgressBlock:(void (^)(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead))block;.
Hope this gets you in the right track.

Related

How to stop call which is sent in operation queue in Objective-C

I am working on iOS App, and I am using AFNetworking for interacting with server API.
My issue is I want to send call and don't want to restrict user until response get from server, so issue is crash. When user move back to that particular screen lets say I have listing screen where I am getting data which is taking 6-7 seconds and meanwhile user move back to previous screen and when data come from API and call back that delete to listing screen but user move backed to that screen then App crashes
Here below is code for fetching data call.
+ (void) getRequestForDocumentListing:(NSDictionary *)headerParams urlQuery: (NSString*)action parameters:(NSDictionary*)params
onComplete:(void (^)(id json, id code))successBlock
onError:(void (^)(id error, id code))errorBlock
{
NSString *authorizationValue = [self setAuthorizationValue:action];
NSString *selectedLanguage = [ApplicationBaseViewController getDataFromDefaults:#"GLOBALLOCALE"];
NSString *language = selectedLanguage;
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
//set headers values
[manager.requestSerializer setValue:#"application/json" forHTTPHeaderField:#"Accept"];
[manager.requestSerializer setValue:language forHTTPHeaderField:#"Accept-Language"];
[manager.requestSerializer setValue:authorizationValue forHTTPHeaderField:#"authorization"];
[manager.requestSerializer setValue:#"x-folder" forHTTPHeaderField:#"inbox"];
[manager GET:action parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"document listing success");
NSInteger statusCode = [operation.response statusCode];
NSNumber *statusObject = [NSNumber numberWithInteger:statusCode];
successBlock(responseObject, statusObject);
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
NSInteger statusCode = [operation.response statusCode];
NSNumber *statusObject = [NSNumber numberWithInteger:statusCode];
id responseObject = operation.responseData;
id json = nil;
id errorMessage = nil;
if (responseObject) {
json = [NSJSONSerialization JSONObjectWithData:responseObject options:kNilOptions error:&error];
errorMessage = [(NSDictionary*)json objectForKey:#"Message"];
}else{
json = [error.userInfo objectForKey:NSLocalizedDescriptionKey];
errorMessage = json;
}
errorBlock(errorMessage, statusObject);
}];
}
What I need is to stop call in ViewdidDisappear View delegate
- (AFHTTPRequestOperation *)GET:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithHTTPMethod:#"GET" URLString:URLString parameters:parameters success:success failure:failure];
[self.operationQueue addOperation:operation];
return operation;
}
How to solve this particular issue?
I got your point, I think the problem is not about the AFNetWorking or download, it is about how you organize your view controllers.
In short, you need to make sure the synchronization of the data and view.
What cause your crash is when users do some operation(eg. delete, move...), the data is not the same with what view shows.
Let's play back an example:
An array with 12 objects and show it with a table view.
User call a web request to change the array. As we know, it needs time.
User leave and come back again. In this view, table view shows with the old array.
At this point, web request comes back. The array is modified to 10 object.But at this time, the call back dose not cause the table view to load the new data.
When user do some operation, just like delete the 11st object in the table view. Actually, there is no 11st object in array.
So crash comes.
How to deal with it is to keep the synchronization of the data and view.
First get a reference to the Operation object by
AFHTTPRequestOperation *operation = [manager GET:action parameters:nil success:^...blah blah blah...];
Then you can set the completion block to nil when you move away from this screen.
[operation setCompletionBlock:nil];
Please note that even though you move away from the screen, the request may actually execute successfully. However, your app will not crash now.
Thanks RuchiraRandana and childrenOurFuture for your answer, I got help from your answers and finally I come to solution where I am not going to cancel operation and set nil delegate, because my others operation are also in working which is trigger on other screen.
I create a just BOOL and set YES default value in singleton class and also set to no in - (void)dealloc on that particular class and in API class where I am triggering that delegate I added that check.
if ([SHAppSingleton sharedInstance].isDocListControllerPop == YES) {
[delegate documentListResponse:documentList andStatusCode:code];
}
I know this might not be perfect solution but this resolved my issue.
Thanks

How to Modify an Instance in Another Class with AFNetworking - Creating API Client

Having this issue, because I'm trying to develop my code. Before, I was using AFNetworking methods in the classes, but I got 4 of them. Instead of that repeatin sequence, I wanted to have APIClient, which has the methods. I implemented some methods but my issue is about just two of them.
So, in APIClient.m I have the followings:
+(void)GetCurrencyInformationFrom:(NSString *)URLString to:(NSArray *) array inThe:(UITableView *) tableView{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; // User informations.
NSString *accessToken = [defaults objectForKey:#"accessToken"];
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.responseSerializer = [AFJSONResponseSerializer serializer];
[manager.requestSerializer setValue:accessToken forHTTPHeaderField:#"Token"];
NSLog(#"access token: %#", accessToken);
NSLog(#"id: %#", [defaults objectForKey:#"ID"]);
[manager GET:URLString parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[self update:array withDictionary:responseObject inThe:tableView];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error 2: %#", error);
}];
}
+(void)update:(NSArray *)array withDictionary:(NSDictionary *)dictionary inThe:(UITableView *) tableView{
NSLog(#"Data Count: %lu", [dictionary[#"Data"] count]);
array = [[NSMutableArray alloc] initWithArray:dictionary[#"Data"]];
NSLog(#"Array Count: %lu", [array count]);
[tableView reloadData];
}
Those methods are called in Table View classes. For example, one of my classes I called within the viewload those:
NSString *URL = #"http://api-dvzalt.azurewebsites.net/api/Currency/Doviz";
[APIClient GetCurrencyInformationFrom:URL to:currencyArray inThe: tableView];
For debugging, I am printing the Data count and Array count (Both you can find in update:withDictionary:inThe: method) and number of rows (in the table class). It's normal to number of rows to be zero at the beginning since it is asychronous, however, after I reload my tableView, i.e. after everything is done (see [tableView reloatData] in update:withDictionary:inThe method) number of rows remains zero, where Data and Array's count are 20. And of course, with zero rows, nothing showed up on the table. So, basically my problem is the currencyArray I'm giving to method doesn't change after it comes back to the tableView again even it is changing in the APIClient class.
I feel like it is a simple mistake, but I can't see where it is. Glad if you can help me to find it.
Thanks!
Did you make sure that your UITableView has the correct data source set?
What do your -numberOfSectionsInTableView: and -tableView:numberOfRowsInSection: methods look like?
I don't really like answering my question. However in this case, I bet there are too many newbie people around and searching for the same problem I have. I couldn't find any solution for 4 days. Have been searching on net and asking here, but nowhere I found the solution I needed. Maybe it will be not the case for you, but certainly it will at least take 1 day long, if it is the first time. So, I will give a try to make beginners like me understand deeply.
At first, creating and having a class like APIClient is generally a good idea. It is just a class that you can easily use when you are going to take data from internet. However, things are getting complicated for beginners since we are mostly got used to synchronous execution.
If you are up trying modify your any instance in any class, what you have to do is, simply, not to give that instance to APIClient (or the class that has blocks) like me, instead trying to take any needed information from the APIClient. Like, if we can achieve the information coming from the APIClient, it is easy to that instance in the instance's own class. E.g. giving currencyArray to APIClient, and trying to update the array in APIClient is hard. On the other hand, it is easy to just taking the responceObject from APIClient, which will be exactly the JSON data coming from the URL you have.
Long story short, in that manner, I changed my code in the APIClient.m into this:
+(void)GetCurrencyInformationFrom:(NSString *)URLString to:(NSArray *) array inThe:(UITableView *) tableView{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; // User informations.
NSString *accessToken = [defaults objectForKey:#"accessToken"];
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.responseSerializer = [AFJSONResponseSerializer serializer];
[manager.requestSerializer setValue:accessToken forHTTPHeaderField:#"Token"];
NSLog(#"access token: %#", accessToken);
NSLog(#"id: %#", [defaults objectForKey:#"ID"]);
[manager GET:URLString parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
if(success) success(operation, responseObject)
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if(failure) failure(operation, error)
}];
}
Note that I have no longer update:withDictionary:inThe: method, since the plan is taking the information to the instance's class, and update it there. not here.
For updating purpose, let's call in the viewLoad of the instance's class. So, the method will look like this:
[APIClient GetCurrencyInformationFrom:URL
success:^(AFHTTPRequestOperation *operation, id responseObject) {
[self updateCurrencyArrayWithDictionary:responseObject];
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error 2: %#", error);
}];
Again for updating purpose, I also added update method here instead of APIClient, which is not really necessary actually; instead we would have block type instance.
-(void)updateCurrencyArrayWithDictionary:(NSDictionary *)dictionary{
currencyArray= [[NSMutableArray alloc] initWithArray:dictionary[#"Data"]];
[tableView reloadData];
}
Please note that the line dictionary[#"Data"]]; would probably change according to your data.
So, that's it. This is just a simple example of how to create API client for the networking purpose of our application.
Hope, I can help someone in the future with this post.

iOS Keep Order of Async Queries

- (void)loadItems {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager.requestSerializer setValue:#"text/html" forHTTPHeaderField:#"Content-Type"];
[manager GET:#"someurl"
parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[self reloadData];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
- (void)textFieldDidChange {
[_filteredArray removeAllObjects];
[self loadItems];
}
I am trying to implement instant search by making an API call every time a character changes. Since, the first few calls have less letters, they return more results, making the first few async calls finish slower than than the last few, meaning that if I type in hello quickly, I will end up getting the search results for h instead of the whole word since the last call to finish is the one for h. I need to keep the order of these calls, and make sure that the last query is not overwritten. I understand that I must use a queue structure. However doing something like this in textFieldDidChange doesn't seem to work:
dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
[self loadItems];
});
dispatch_group_notify(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
[self reloadData];
});
I think I need to use some sort of combination of dispatch_group_enter(group); and dispatch_group_leave(group);. However I still can't get the calls to stop overwriting the last call. I'm not sure if there is also a way to just cancel out all the other started calls with the last one, or if I have to wait for all of them to finish in order. Any help would be appreciated.
This was my solution. I just ended up using a counter that I pass into my loadItems function. While that counter updates, the async call still has its own value in it, so I just compare the two, and make sure to only reloadData if the async call's counter is equal to the latest one.
- (void)loadItems:(int)queryInt {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager.requestSerializer setValue:#"text/html" forHTTPHeaderField:#"Content-Type"];
[manager GET:#"someurl"
parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (searchQueryCounter - 1 == queryInt) {
[self reloadDatawithAnimation];
} else {
return;
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
- (void)textFieldDidChange {
[_filteredArray removeAllObjects];
[self loadItems:searchQueryCounter];
searchQueryCounter = searchQueryCounter + 1;
}
You might better address this by canceling the prior requests, not only preventing prior requests reporting results, but also ensuring that system resources are not consumed by requests that are no longer needed:
#interface ViewController () <UITextFieldDelegate>
#property (nonatomic, strong) AFHTTPRequestOperationManager *manager;
#property (nonatomic, weak) NSOperation *previousOperation;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// no need to instantiate new request operation manager each time;
// do it at some logical point of initialization (e.g. in `viewDidLoad`
// for view controllers, etc.).
self.manager = [AFHTTPRequestOperationManager manager];
[self.manager.requestSerializer setValue:#"text/html" forHTTPHeaderField:#"Content-Type"];
}
- (void)loadItems {
[self.previousOperation cancel];
typeof(self) __weak weakSelf = self; // probably should use weakSelf pattern, too
NSOperation *operation = [self.manager GET:#"someurl" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[weakSelf reloadData];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if ([error.domain isEqualToString:NSURLErrorDomain] && [error code] != NSURLErrorCancelled) {
NSLog(#"Error: %#", error);
}
}];
self.previousOperation = operation;
}
- (void)textFieldDidChange {
[_filteredArray removeAllObjects];
[self loadItems];
}
#end
I actually worked on a very similar problem last week and came up with an approach you might find useful.
I submit each request using performSelector:withObject:afterDelay with a slight delay (I've been experimenting with values ranging from 0.5 to 1.0 seconds. Somewhere from .66 to .75 seems like a good compromise value.)
With each new request, I cancel the previous pending performSelector call. That way nothing gets sent until the user stops typing for a short period of time. It's not perfect, but it reduces the amount of useless queries for word fragments. The code looks something like this:
static NSString *methodWord = nil;
[[self class] cancelPreviousPerformRequestsWithTarget: self
selector: #selector(handleWordEntered:)
object: methodWord];
methodWord = word;
[self performSelector: #selector(handleWordEntered:)
withObject: methodWord
afterDelay: .667];
The method handleWordEntered: actually sends the request to the server.
If the user types a letter, then another letter in less than 2/3 second, the previous pending request is cancelled and a new request is set to fire 2/3 of a second later. As long as the user keeps typing letters every 2/3 second, nothing is sent. As soon as the user pauses more than 2/3 second, a request is sent. Once the performSelector:withObject:afterDelay fires it can't be cancelled any more, so that request goes to the network and the reply is parsed.

Sending NSOperationQueue to UITableView as a DataSource

i have written code to downloading data from server using NSOperationQueue and NSOperation. and now i want to show progress on UserInterface. i used UITableView and used NSOpeartionQueue as a datasource in tableview delegate
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [[[Downloadmanager sharedInstance] downloadOperationQueue] count];
}
and bind NSOperation`s properties to UITableViewCell.
1) Is this a fisible solution to sending NSOperationQueue as a datasource to tableview delegate ?
2) How to implement notification to reload tableview when NSOperation's state changes?
Thanks.
I don't think it's the proper way of showing progress using NSOperationQueue as a datasource to tableview. You can use networking library like AFNetworking for downloading data and use setDownloadProgressBlock: method for showing progress. Refer this link for the code download progress.
It's easy to reload tableview when the download completes, just call [tableView reloadData] in completionblock.
Here is the code which shows image downloading using AFNetworking which you can easily change for data download.(refer this gist)
- (void)downloadMultiAFN {
// Basic Activity Indicator to indicate download
UIActivityIndicatorView *loading = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
[loading startAnimating];
[self.imageView.superview addSubview:loading];
loading.center = self.imageView.center;
// Create a request from the url, make an AFImageRequestOperation initialized with that request
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:self.picUrl]];
AFImageRequestOperation *op = [[AFImageRequestOperation alloc] initWithRequest:request];
// Set a download progress block for the operation
[op setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
if ([op.request.URL.absoluteString isEqualToString:#"http://www.pleiade.org/images/hubble-m45_large.jpg"]) {
self.progressBar.progress = (float) totalBytesRead/totalBytesExpectedToRead;
} else self.progressBar2.progress = (float) totalBytesRead/totalBytesExpectedToRead;
}];
// Set a completion block for the operation
[op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
self.imageView.image = responseObject;
self.image = responseObject;
if ([op.request.URL.absoluteString isEqualToString:#"http://www.pleiade.org/images/hubble-m45_large.jpg"]) {
self.progressBar.progress = 0;
} else self.progressBar2.progress = 0;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {}];
// Start the image download operation
[op start];
// Remove the activity indicator
[loading stopAnimating];
[loading removeFromSuperview];
}
That is an interesting idea, but I don't think it's a good practice make such a "high coupling" - linking model so tightly to the view.
I'd approach it as - download the data on the background thread as you already do - with NSOperationQueue but save it to some kind of an object; say NSMutableArray that serves as the data source for the table view.
Every time a single operation ends (use completion handlers or KVO to get informed) - update the table view. The update can be done two ways - reloading or updating. I'll leave the choice up to you - you can read further discussion about that in this question.

How to wait for asyn operation in iOS unit test using NSConditionLock

I have a unit test in which I need to wait for an async task to finish. I am trying to use NSConditionLock as it seems to be a pretty clean solution but I cannot get it to work.
Some test code:
- (void)testSuccess
{
loginLock = [[NSConditionLock alloc] init];
Login login = [[Login alloc] init];
login.delegate = self;
// The login method will make an async call.
// I have setup myself as the delegate.
// I would like to wait to the delegate method to get called
// before my test finishes
[login login];
// try to lock to wait for delegate to get called
[loginLock lockWhenCondition:1];
// At this point I can do some verification
NSLog(#"Done running login test");
}
// delegate method that gets called after login success
- (void) loginSuccess {
NSLog(#"login success");
// Cool the delegate was called this should let the test continue
[loginLock unlockWithCondition:1];
}
I was trying to follow the solution here:
How to unit test asynchronous APIs?
My delegate never gets called if I lock. If I take out the lock code and put in a simple timer it works fine.
Am I locking the entire thread and not letting the login code run and actually make the async call?
I also tried this to put the login call on a different thread so it does not get locked.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[login login];
});
What am I doing wrong?
EDIT adding login code. Trimmed do the code for readability sake. Basically just use AFNetworking to execute a POST. When done will call delegate methods.
Login make a http request:
NSString *url = [NSString stringWithFormat:#"%#/%#", [_baseURL absoluteString], #"api/login"];
[manager POST:url parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (_delegate) {
[_delegate loginSuccess];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (_delegate) {
[_delegate loginFailure];
}
}];
The answer can be found in https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking/AFHTTPRequestOperation.m.
Since you are not setting the completionQueue property of the implicitly created AFHTTPRequestOperation, it is scheduling the callbacks on the main queue, which you are blocking.
Unfortunately, many answers (not all) in the given SO thread ("How to unit test asynchronous APIs?") are bogus and contain subtle issues. Most authors don't care about thread-safity, the need for memory-barriers when accessing shared variables, and how run loops do work actually. In effect, this leads to unreliable and ineffective code.
In your example, the culprit is likely, that your delegate methods are dispatched on the main thread. Since you are waiting on the condition lock on the main thread as well, this leads to a dead lock. One thing, the most accepted answer that suggests this solution does not mention at all.
A possible solution:
First, change your login method so that it has a proper completion handler parameter, which a call-site can set in order to figure that the login process is complete:
typedef void (^void)(completion_t)(id result, NSError* error);
- (void) loginWithCompletion:(completion_t)completion;
After your Edit:
You could implement your login method as follows:
- (void) loginWithCompletion:(completion_t)completion
{
NSString *url = [NSString stringWithFormat:#"%#/%#", [_baseURL absoluteString], #"api/login"];
[manager POST:url parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (completion) {
completion(responseObject, nil);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (completion) {
completion(nil, error);
}
}];
Possible usage:
[self loginWithCompletion:^(id result, NSError* error){
if (error) {
[_delegate loginFailure:error];
}
else {
// Login succeeded with "result"
[_delegate loginSuccess];
}
}];
Now, you have an actual method which you can test. Not actually sure WHAT you are trying to test, but for example:
-(void) testLoginController {
// setup Network MOCK and/or loginController so that it fails:
...
[loginController loginWithCompletion:^(id result, NSError*error){
XCTAssertNotNil(error, #"");
XCTAssert(...);
<signal completion>
}];
<wait on the run loop until completion>
// Test possible side effects:
XCTAssert(loginController.isLoggedIn == NO, #""):
}
For any other further steps, this may help:
If you don't mind to utilize a third party framework, you can then implement the <signal completion> and <wait on the run loop until completion> tasks and other things as described here in this answer: Unit testing Parse framework iOS

Resources