iOS - AFNetworking crashing on line 577 of AFURLResponseSerialization.m - ios

I'm implementing a lazy loading strategy in my app where I use AFNetworking to asynchronously load news article images in a UITableView. This is the following code that does that in my cellForRowAtIndexPath method:
#implementation PocketTableViewController
- (AFHTTPRequestOperationManager *)operationManager
{
if (!_operationManager)
{
_operationManager = [[AFHTTPRequestOperationManager alloc] init];
_operationManager.responseSerializer = [AFImageResponseSerializer serializer];
};
return _operationManager;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
...
if (articleImageURL != nil)
{
//set article image
cell.ThumbImage.image = [UIImage imageNamed:#"greyImage.png"];
[cell.ThumbImage.associatedObject cancel];
cell.ThumbImage.associatedObject =
[self.operationManager GET:[articleImageURL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]
parameters:nil
success:^(AFHTTPRequestOperation *operation, id responseObject) {
cell.ThumbImage.image = responseObject;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Failed with error %#.", error);
}];
}
It works for most images when I scroll through the table view, but then it crashes here:
Any thoughts on why it's crashing?
UPDATE:
I found that it's crashing on this image url for some reason:
http://www.profitconfidential.com/wp-content/uploads/2015/12/Bill-Gates-Bought-Stock-in-Third-Quarter.jpg

Use UIImage+AFNetworking.
It lazy loading and supported image caching.
In your case, you can use this:
UIImage *image = [UIImage imageWithData:responseObject];
But the best way is use UIImage+AFNetworking.

Apparently I fixed the problem. There was nothing wrong with any of the images. I just had to disable all breakpoints in Xcode and it worked. Weird, but it fixed the problem.

Related

AFHTTPSessionManager load slow for first time request

I applied this code in my tableview. However, i realise that data will load slow for first time request. After that, if i access to the same page, the records will be loaded faster. Any idea to improve for first time loading? Thanks for your help.
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:#"http://api.domainname.com/api/abc" parameters:nil progress:nil success:^(NSURLSessionTask *task, id responseObject) {
_serverDataArr = responseObject;
self.dataArr=[NSMutableArray array];
for (NSDictionary *subDic in self.serverDataArr) {
Friend_Model *model=[[Friend_Model alloc]initWithDic:subDic];
[self.dataArr addObject:model];
}
_rowArr=[Friend_DataHelper getFriendListDataBy:self.dataArr];
_sectionArr=[Friend_DataHelper getFriendListSectionBy:[_rowArr mutableCopy]];
[self.tableView reloadData];
} failure:^(NSURLSessionTask *operation, NSError *error) {
UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:#"Error Retrieving Ninjas"
message:[error localizedDescription]
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:#"Ok"
style:UIAlertActionStyleCancel
handler:nil];
[alertVC addAction:okAction];
[self presentViewController:alertVC animated:YES completion:nil];
}];
And here is my cellForRowAtIndexPath code :-
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *cellIde=#"cellIde";
Friend_TableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:cellIde];
if (cell==nil) {
cell=[[Friend_TableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIde];
}
[cell setSelectionStyle:UITableViewCellSelectionStyleNone];
if (tableView==_searchDisplayController.searchResultsTableView){
NSString *url_Img_FULL = [_searchResultArr[indexPath.row] valueForKey:#"image"];
[cell.headImageView setImageWithURL:[NSURL URLWithString:url_Img_FULL] placeholderImage:[UIImage imageNamed:#"Placeholder.png"]];
[cell.nameLabel setText:[_searchResultArr[indexPath.row] valueForKey:#"name"]];
}else{
Friend_Model *model=_rowArr[indexPath.section][indexPath.row];
[cell.headImageView setImageWithURL:[NSURL URLWithString:model.image] placeholderImage:[UIImage imageNamed:#"Placeholder.png"]];
[cell.nameLabel setText:model.name];
}
return cell;
}
It looks like you're doing perfectly appropriate stuff to form the request and present the results. You might be seeing a big difference between the 1st and nth request because your back-end is awake, and data has been cached at various points on the stack. Hard to suggest much here besides moving the request earlier, maybe to app startup, where the user might not notice as much delay.
Another idea would be to request data for table rows more lazily, based on where the table is scrolled. When user gestures a pull at the bottom of the table, show a busy indicator and go get more data.

How to determine whether to perform segue based on JSON result from another thread

I have a login view. When user clicks "login" button, the app gonna authenticate the account then decide to segue or not. However, the POST request is asynchronous.
- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender
is executed before I can get something from the server. Here is my codeļ¼š
- (void)isAuthenticaionConfirmed
{
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
NSDictionary *parameters = #{#"account":self.studentsNumber.text,
#"password":self.passwordToJW.text};
[manager POST:checkAutenticationURL parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"JSON: %#", responseObject);
[self parseDictionary:responseObject];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
[self showAlert:#"3"];
}];
}
- (void)parseDictionary: (NSDictionary *)dictionary
{
NSString *errorInfo = dictionary[#"err"];
if ([errorInfo isEqualToString:#"subsequent request failed"]) {
[self showAlert:#"3"];
} else if ([errorInfo isEqualToString:#"login failed"]) {
[self showAlert:#"2"];
} else {
_name = dictionary[#"name"];
}
}
here is the code relating to segue
- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender
{
if (_name) {
return YES;
} else {
return NO;
}
}
The _name here is the JSON result when the user uses correct account and password.
I solve it by refusing any kinds of segue first
- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender
{
return NO;
}
then calling [self performSegueWithIdentifier:#"filterSegue" sender:self] method in paresDictionary
It looks weird. If anyone has a better solution, welcome to leave a comment! :-)

Objective-C: View displays empty Table since API takes time to fetch from Server

I have a TransactionViewController that during instantiation fetches data from API and looks like
- (id)init {
self = [super init];
if (self) {
TransactionsModel *transactionsModel = [TransactionsAPI getTransactionsForYear:2014 AndMonth:9];
self.transactionsModel = transactionsModel;
}
return self;
}
The API get data as following
+ (TransactionsModel *)getTransactionsForYear:(int)year AndMonth:(int)month {
NSDictionary *transactions = [self getTransactions];
NSLog(#"transactions: %#", transactions);
return [[TransactionsModel alloc] initWithJson:transactions];
}
and
+ (NSDictionary *)getTransactions {
AFHTTPRequestOperationManager *manager = [RestHelper getSecureManager];
__block NSDictionary* transactions = nil;
[manager GET:[appUrl stringByAppendingString:#"rest/transactions/2014/11"] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
transactions = responseObject;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
return transactions;
}
I am using AFNewtworking 2.0 in which every call is asynchronous, right?
Question
How do I wait to display my Table until data is fetched from server?
P.S I am new to iOS programming and looking for recommended ways to solve these issues. Thanks
You would need to turn your complete code into an asynchronous implementation. See my example edit below.
+ (void)getTransactionsWithCompletionHandler:^(NSDictionary *responseObject)completionHandler {
AFHTTPRequestOperationManager *manager = [RestHelper getSecureManager];
[manager GET:[appUrl stringByAppendingString:#"rest/transactions/2014/11"] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
completionHandler((NSDictionary *)responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
You would also need to call -reloadData in your block that you pass when using the method:
- (id)init {
self = [super init];
if (self) {
TransactionsModel *transactionsModel = [TransactionsAPI getTransactionsForYear:2014 AndMonth:9 completionHandler:^(NSDictionary *transactions) {
self.transactionsModel = transactions;
[self.tableView reloadData];
}];
}
}
And in your API:
+ (void)getTransactionsForYear:(int)year AndMonth:(int)month completionHandler:^(TransactionsModel *transactions)completionHandler {
[self getTransactionsWithCompletionHandler:^(NSDictionary *transactions) {
TransactionsModel *model = [[TransactionsModel alloc] initWithJson:transactions];
completionHandler(model);
}];
}
Use completion blocks. You have a useful one in your getTransactions method:
[manager GET:[appUrl stringByAppendingString:#"rest/transactions/2014/11"] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
transactions = responseObject;
--> RELOAD TABLE HERE: [tableView reloadData]; <--
.
.
.
Your table is probably in another class. You can use Notifications and Observers. Basically, you set an observer that listens to a specific notification and, at some point, you post a notification, that observers listens to it and do something. For example:
Post a notification when the response arrives (the same place where I suggested you put [tableView reloadData]; but don't put it if you choose this way):
[[NSNotificationCenter defaultCenter] postNotificationName:#"TransactionResponseArrived" object:self userInfo:nil];
Go to you table viewController and add an observer:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(updateData:) name:#"TransactionResponseArrived" object:nil];
Notice that you have to use the same notification name as above.
Also in your table viewController, create the method that is being called when the observer gets the notification (updateData:):
- (void)updateData:(NSNotification *)notification
{
[self.tableView reloadData];
}
Finally, remember to unload the observer:
[[NSNotificationCenter defaultCenter] removeObserver:self name:InAppPurchaseDownloadStatusUpdated object:nil];
You can do it in your dealloc method if you add the observer in an init method. If you use a the viewWillAppear or viewDidAppear to load the observer, use viewWillDisappear or viewDidDisappear to remove it.
Hope this helps!

memory crash when storing NSOperationQueues in a NSCache

I have a UITableView that loads thumbnails into cells aynchronously as follows:
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:
^{
ThumbnailButtonView *thumbnailButtonView = [tableViewCell.contentView.subviews objectAtIndex:i];
UIImage *image = [self imageAtIndex:startingThumbnailIndex + i];
[self.thumbnailsCache setObject: image forKey:[NSNumber numberWithInt:startingThumbnailIndex + i]];
[[NSOperationQueue mainQueue] addOperationWithBlock:
^{
UITableViewCell *tableViewCell = [self cellForRowAtIndexPath:indexPath];
if (tableViewCell)
{
[activityIndicatorView stopAnimating];
[self setThumbnailButtonView:thumbnailButtonView withImage:image];
}
}];
}];
[self.operationQueue addOperation:operation];
[self.operationQueues setObject:operation forKey:[NSNumber numberWithInt:startingThumbnailIndex + i]];
As per a technique I learned in a WWDC presentation, I store all of my operation queues in a NSCache called operationQueues so that later on I can cancel them if the cell scrolls off the screen (there are 3 thumbnails in a cell):
- (void) tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger startingThumbnailIndex = [indexPath row] * self.thumbnailsPerCell;
for (int i = 0; i < 3; i++)
{
NSNumber *key = [[NSNumber alloc] initWithInt:i + startingThumbnailIndex];
NSOperation *operation = [self.operationQueues objectForKey:key];
if (operation)
{
[operation cancel];
[self.operationQueues removeObjectForKey:key];
}
}
}
However, I notice if I repeatedly launch, load, then close my UITableView, I start recieving memory warnings, and then eventually the app crashes. When I remove this line:
[self.operationQueues setObject:operation forKey:[NSNumber numberWithInt:startingThumbnailIndex + i]];
The memory issues go away. Does anyone have any clue on why storing the operation queues in a cache or an array causes the app to crash?
Note: I learnt about NSCache and NSOperationQueue a couple of days ago so I might be wrong.
I don't think that this is a problem with NSOperationQueue, you are adding images to your thumbnailsCache but when the view scrolls off-screen they are still in memory. I am guessing that when the cells scroll back in, you re-create your images. This is probably clogs your memory.
Also, shouldn't you be caching your images instead of your operations?
EDIT
I did some detailed testing with NSCache by adding images and strings until my app crashed. It doesn't seem to be evicting any items so I wrote my custom cache, which seems to work:
#implementation MemoryManagedCache : NSCache
- (id)init
{
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(reduceMemoryFootprint) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
- (void)reduceMemoryFootprint
{
[self setCountLimit:self.countLimit/2];
}
#end

SDWebImage NSURLRequests failing intermittently

I'm loading images from a remote server using SDWebImage into a UICollectionView using the following code:
[myCell.imageView setImageWithURL:imgURL placeholderImage:nil options:SDWebImageRetryFailed success:^(UIImage *image)
{
[_imageCache storeImage:image forKey:[imgURL absoluteString] toDisk:YES];
} failure:^(NSError *error){
NSLog(#"ERROR: %#", error);
}];
For most cells, this code works fine - it loads the images and saves them to my local disk. However, after several (it seems random?) images, they stop loading. I then get the following error:
ERROR: Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo=0x1d33fdc0 {NSErrorFailingURLStringKey=http://path/to/image.jpg, NSErrorFailingURLKey=http://path/to/image.jpg, NSLocalizedDescription=The request timed out., NSUnderlyingError=0x1d34c0f0 "The request timed out."}
When this happens, my app seems to stop sending NSURLRequests altogether. After a period of time, probably about 20-30 seconds, I can refresh the table and the failed images will load in correctly and the app will resume responding to all NSURLRequests perfectly fine.
I find that this tends to happen more often if I scroll down my collection view fast. Could it be trying to download too many at once? Is there a way to limit the number of concurrent downloads? This method appears to be deprecated in the latest SDWebImage code.
Figured it out. I was using MWPhotoBrowser in another part of my app. MWPhotoBrowser comes with an older/modified version of SDWebImage. I downloaded the latest version of SDWebImage from Github, renamed/refactored all of the files, and imported my newly updated and modified SDWebImage alongside the one MWPhotoBrowser relies on.
The new version of SDWebImage has solved my problem completely!
It's better if use asynchronous download by share manager and display when finish download with SDWebImage. Now, you can scroll fast to test.
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
MyCell *myCell = (MyCell *)[cv dequeueReusableCellWithReuseIdentifier:#"MyCell" forIndexPath:indexPath];
//Set placeholder
myCell.imageView.image = [UIImage imageNamed:#"placeholder.png"];
NSURL *cellImageURL = [NSURL URLWithString:[NSString stringWithFormat:#"%#",[self.collectionData objectAtIndex:indexPath.row]]];
[myCell.cellIndicator startAnimating];
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadWithURL:cellImageURL
delegate:self
options:0
success:^(UIImage *image, BOOL cached)
{
//Display when finish download
myCell.imageView.image = image;
[myCell.cellIndicator stopAnimating];
} failure:nil];
return cell;
}
*EDIT:
If problem still not solve on device: try to separate download and display
#interface ViewController ()
#property (retain , nonatomic) NSMutableArray *linksArray;
#property (retain , nonatomic) NSMutableArray *imagesArray;
#end
- (void)viewDidLoad
{
[super viewDidLoad];
[self initLinksArray];
//Add placehoder.png to your imagesArray
[self initImagesArray];
//Download to NSMultableArray
[self performSelectorInBackground:#selector(downloadImages) withObject:nil];
}
- (void)initImagesArray{
self.imagesArray = [[[NSMutableArray alloc] init] autorelease];
for (NSInteger i = 0; i < self.linksArray.count; i++) {
[self.imagesArray addObject:[UIImage imageNamed:#"placeholder.png"]];
}
}
- (void)downloadImages{
for (NSInteger i = 0; i < self.linksArray.count; i++) {
NSURL *cellImageURL = [NSURL URLWithString:[self.linksArray objectAtIndex:i]];
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadWithURL:cellImageURL
delegate:self
options:0
success:^(UIImage *image, BOOL cached)
{
[self.imagesArray replaceObjectAtIndex:i withObject:image];
} failure:nil];
}
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [self.imagesArray count];;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
SDCell *cell = (SDCell *)[cv dequeueReusableCellWithReuseIdentifier:#"SDCell" forIndexPath:indexPath];
cell.cellImageView.image = [self.imagesArray objectAtIndex:indexPath.row];
return cell;
}

Resources