This question already has answers here:
Table View with Images, slow load and scroll
(4 answers)
Closed 7 years ago.
I'm creating a UITableViewController to display the roster of a hockey team. The tableViewController makes calls to the web to get the player's stats and a small picture to display in the tableViewCell. However, when I scroll through the TableView, it isn't smooth. It's incredibly jagged. How can I make it so (if this will decrease its work load) the player's pictures don't load until they're on-screen? Here is my current code (I've subclassed UITableViewCell):
EDIT: I've edited my code to follow a comment below. The property imagesCache is actually a UIMutableDictionary (confusing, sorry). However, now I get the error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** setObjectForKey: object cannot be nil (key: http://app-assets3.sportngin.com/app_images/noPhoto-square.jpg?1428933774)'
*** First throw call stack:
(0x1865f6530 0x1975cc0e4 0x1864e1348 0x1000496a8 0x185f87168 0x1874d3be8 0x187425374 0x187414ecc 0x1874d694c 0x1000acf94 0x1000b7db8 0x1000b02c4 0x1000ba5d4 0x1000bc248 0x197dfd22c 0x197dfcef0)
libc++abi.dylib: terminating with uncaught exception of type NSException
Here is my code:
#import "RosterTableTableViewController.h"
#import "TFHpple.h"
#import "RosterListing.h"
#import "RosterListingCellTableViewCell.h"
#interface RosterTableTableViewController ()
#property (nonatomic, strong) NSMutableArray *rosters;
#property (nonatomic, strong) NSMutableDictionary *imagesDictionary;
#property NSMutableDictionary *imageCache;
#end
#implementation RosterTableTableViewController
- (void) loadRoster
{
NSURL *RosterURL = [NSURL URLWithString:#"http://www.lancers.com/roster/show/1502650?subseason=197271"];
NSData *RosterHTMLData = [NSData dataWithContentsOfURL:RosterURL];
TFHpple *RosterParser = [TFHpple hppleWithHTMLData:RosterHTMLData];
// Get the data
NSString *RosterNumberPathQueryString = #"//tbody[#id='rosterListingTableBodyPlayer']/tr/td[#class='number']";
NSArray *RosterNumberNodes = [RosterParser searchWithXPathQuery:RosterNumberPathQueryString];
NSString *RosterNamePathQueryString = #"//tbody[#id='rosterListingTableBodyPlayer']/tr/td[#class='name']/a";
NSArray *RosterNameNodes = [RosterParser searchWithXPathQuery:RosterNamePathQueryString];
NSString *RosterImagePathQueryString = #"//tbody[#id='rosterListingTableBodyPlayer']/tr/td[#class='photo']/a/img";
NSArray *RosterImageNodes = [RosterParser searchWithXPathQuery:RosterImagePathQueryString];
NSMutableArray *rosterItems = [[NSMutableArray alloc] initWithCapacity:0];
for (int i = 0; i < RosterNumberNodes.count; ++i) {
RosterListing *thisRosterListing = [[RosterListing alloc] init];
thisRosterListing.playerNumber = [[[RosterNumberNodes objectAtIndex:i] firstChild] content];
thisRosterListing.playerName = [[[RosterNameNodes objectAtIndex:i] firstChild] content];
thisRosterListing.playerURL = [[RosterNameNodes objectAtIndex:i] objectForKey:#"href"];
#try {
thisRosterListing.playerImageURL = [[RosterImageNodes objectAtIndex:i] objectForKey:#"src"];
}
#catch (NSException *e) {}
/*
NSLog(#"%#", thisRosterListing.playerNumber);
NSLog(#"%#", thisRosterListing.playerName);
NSLog(#"%#", thisRosterListing.playerURL);
NSLog(#"%#", thisRosterListing.playerImageURL);
*/
[rosterItems addObject:thisRosterListing];
}
self.rosters = rosterItems;
}
- (instancetype) initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
self.navigationItem.title = #"Roster";
self.imageCache = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self loadRoster];
// Load the Cell NIB file
UINib *nib = [UINib nibWithNibName:#"RosterListingCellTableViewCell" bundle:nil];
// Register this NIB, which contains the cell
[self.tableView registerNib:nib forCellReuseIdentifier:#"RosterCell"];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 54;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// Return the number of rows in the section.
return self.rosters.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Get a new or recycled cell
RosterListingCellTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"RosterCell" forIndexPath:indexPath];
RosterListing *thisRosterListing = [self.rosters objectAtIndex:indexPath.row];
cell.playerNumberLabel.text = thisRosterListing.playerNumber;
cell.playerNameLabel.text = thisRosterListing.playerName;
__block UIImage *image = [self.imageCache objectForKey:thisRosterListing.playerImageURL];
cell.imageView.image = image;
if(image == nil) {
//If nil it's not downloaded, so we download it,
//We MUST download in a separate thread otherwise the scroll will be really slow cause the main queue will try to download each cell as they show up and every time they show up
NSURL *imageURL = [NSURL URLWithString: thisRosterListing.playerImageURL];
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:imageURL
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
//Completion Handler is executed in an async way
if([self.imageCache objectForKey:thisRosterListing.playerImageURL] == nil)
self.imageCache[thisRosterListing.playerImageURL] = image;
//We need to execute the image update in the main queue otherwise it won't work
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
RosterListingCellTableViewCell *aCell = (RosterListingCellTableViewCell *)[tableView cellForRowAtIndexPath:indexPath];
aCell.imageView.image = image;
}];
}];
[dataTask resume];
}
return cell;
}
Working with images in UITableViewCells can be a bit tricky at first, I do have a code that might help you, give me a second while I search it.
Basically what you want to do is check that the row you downloaded the image for is still been displayed (as the user can scroll faster than images are downloaded) and after download ended storage it locally so you won't have to download it again.
EDIT: Here is the code, sorry the lateness
#propery NSMutableDictionary *imagesDictionary;
...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
//...
//We load the model information for that cell
NSDictionary *cellInfo = self.dataModel[indexPath.row];
__block UIImage *image = [self.imagesDictionary objectForKey:[cellInfo objectForKey:#"avatar"]];
cell.avatarView.image = image;
if(image == nil) {
//If nil it's not downloaded, so we download it,
//We MUST download in a separate thread otherwise the scroll will be really slow cause the main queue will try to download each cell as they show up and every time they show up
NSURL *imageURL = [NSURL URLWithString:URL];
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:imageURL
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
//Completion Handler is executed in an async way
if([self.imagesDictionary objectForKey:[cellInfo objectForKey:#"avatar"]] == nil)
if(error == nil) {
// no error
image = [UIImage imageWithData:data];
if (image == nil) {
//nil image, in my case I use a default undefined image
image = [UIImage imageNamed:#"undefined_user"];
}
//Now we are sure image is never nil
[self.imagesDictionary setObject:image forKey:[cellInfo objectForKey:#"avatar"]];
//We need to execute the image update in the main queue otherwise it won't work
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
UITableviewCell *aCell = (UITableViewCell *)[tableView cellForRowAtIndexPath:indexPath];
aCell.avatarView.image = image;
}];
}];
[dataTask resume];
return cell;
Related
My Requirement is download all images in application memory and display it from local if its available.
Below is my code to access image from local and if its not available then it will download then display.
[cell.imgProfilePic processImageDataWithURLString:cData.PICTURE];
I have made custom UIImageView class
DImageView.h
#import <UIKit/UIKit.h>
#interface DImageView : UIImageView
#property (nonatomic, strong) UIActivityIndicatorView *activityView;
- (void)processImageDataWithURLString:(NSString *)urlString;
+ (UIImage *)getSavedImage :(NSString *)fileName;
#end
DImageView.m
#import "DImageView.h"
#define IMAGES_FOLDER_NAME #"DImages"
#implementation DImageView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{ }
return self;
}
- (void)dealloc
{
self.activityView = nil;
[super dealloc];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self)
{
[self initWithFrame:[self frame]];
}
return self;
}
- (void)processImageDataWithURLString:(NSString *)urlString
{
#autoreleasepool
{
UIImage * saveImg =[DImageView getSavedImage:urlString];
if (saveImg)
{
#autoreleasepool
{
dispatch_queue_t callerQueue = dispatch_get_main_queue();
dispatch_async(callerQueue, ^{
#autoreleasepool{
[self setImage:saveImg];
}
});
}
}
else
{
[self showActivityIndicator];
NSURL *url = [NSURL URLWithString:[urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
dispatch_queue_t callerQueue = dispatch_get_main_queue();
dispatch_queue_t downloadQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0);
__block NSError* error = nil;
dispatch_async(downloadQueue, ^{
#autoreleasepool
{
NSData * imageData = [NSData dataWithContentsOfURL:url options:NSDataReadingUncached error:&error];
if (!error)
{
dispatch_async(callerQueue, ^{
#autoreleasepool {
UIImage *image = [UIImage imageWithData:imageData];
[self setImage:image];
[self hideActivityIndicator];
[self saveImageWithFolderName:IMAGES_FOLDER_NAME AndFileName:urlString AndImage:imageData];
}
});
}
}
});
dispatch_release(downloadQueue);
}
}
}
- (void) showActivityIndicator
{
self.activityView = [[UIActivityIndicatorView alloc]initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
self.activityView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin;
self.activityView.hidesWhenStopped = TRUE;
self.activityView.backgroundColor = [UIColor clearColor];
self.activityView.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
[self addSubview:self.activityView];
[self.activityView startAnimating];
}
- (void) hideActivityIndicator
{
CAAnimation *animation = [NSClassFromString(#"CATransition") animation];
[animation setValue:#"kCATransitionFade" forKey:#"type"];
animation.duration = 0.4;;
[self.layer addAnimation:animation forKey:nil];
[self.activityView stopAnimating];
[self.activityView removeFromSuperview];
for (UIView * view in self.subviews)
{
if([view isKindOfClass:[UIActivityIndicatorView class]])
[view removeFromSuperview];
}
}
- (void)saveImageWithFolderName:(NSString *)folderName AndFileName:(NSString *)fileName AndImage:(NSData *) imageData
{
#autoreleasepool{
NSFileManager *fileManger = [[NSFileManager defaultManager] autorelease];
NSString *directoryPath = [[NSString stringWithFormat:#"%#/%#",[DImageView applicationDocumentsDirectory],folderName] autorelease];
if (![fileManger fileExistsAtPath:directoryPath])
{
NSError *error = nil;
[fileManger createDirectoryAtPath:directoryPath withIntermediateDirectories:YES attributes:nil error:&error];
}
fileName = [DImageView fileNameValidate:fileName];
NSString *filePath = [[NSString stringWithFormat:#"%#/%#",directoryPath,fileName] autorelease];
BOOL isSaved = [imageData writeToFile:filePath atomically:YES];
if (!isSaved)DLog(#" ** Img Not Saved");
}
}
+ (NSString *)applicationDocumentsDirectory
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : nil;
return basePath;
}
+ (UIImage *)getSavedImage :(NSString *)fileName
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
fileName = [DImageView fileNameValidate:fileName];
NSFileManager * fileManger = [[NSFileManager defaultManager] autorelease];
NSString * directoryPath = [[NSString stringWithFormat:#"%#/%#",[DImageView applicationDocumentsDirectory],IMAGES_FOLDER_NAME] autorelease];
NSString * filePath = [[NSString stringWithFormat:#"%#/%#",directoryPath,fileName] autorelease];
if ([fileManger fileExistsAtPath:directoryPath])
{
UIImage *image = [[[UIImage imageWithContentsOfFile:filePath] retain]autorelease];
if (image)
return image;
else
return nil;
}
[pool release];
return nil;
}
+ (NSString*) fileNameValidate : (NSString*) name
{
name = [name stringByReplacingOccurrencesOfString:#"://" withString:#"##"];
name = [name stringByReplacingOccurrencesOfString:#"/" withString:#"#"];
name = [name stringByReplacingOccurrencesOfString:#"%20" withString:#""];
return name;
}
#end
Everything is working fine with smooth scrolling as well as asyncImage download in background.
The issue is when i scroll UITableview application memory is continuously increase and after some time i got Receive memory waring 2/3 time then application crash.
When i use AsyncImageView class that time memory not increase and its working fine. But due to app requirement i saved all images to Document Directory and display from it if its available.
i have tried with #autoreleasepool and release some variable but not getting success.
I appreciated if any one have the solution to manage memory management.
**ARC is off in my application.**
It's possible that UIImagePNGRepresentation returns non-autoreleased object - you can try to release it and see if that results in a crash. Obviously you are not releasing something, but nothing other than the image representation appears obvious.
A few other comments:
run your app in Instruments, using the ObjectAlloc tool, and it should be immediately obvious what objects are not dealloced. If you don't know Instruments, well, its time now to learn it.
you can 'track' objects and get a message when they are dealloced using ObjectTracker - however it was designed for ARC so you may need to tweak it. If you use it you would see a message when each of your objects are dealloced
when the table view is done with a cell, there is a delegate method that you can receive that tells you so, and you can then nil (release) and objects the cell retains
your use of downloadQueue is bizarre - create it once in your instance as an ivar, use it as you need, and in dealloc release it
you hide the activity spinner on the main queue, but don't start it on the main queue
you command the activity view to remove itself from its superview, but then look for in in the subviews and try to remove it there:
[self.activityView removeFromSuperview];
for (UIView * view in self.subviews)
{
if([view isKindOfClass:[UIActivityIndicatorView class]])
[view removeFromSuperview];
}
In the end, Instruments is what you want. You can read up more about it here, or just google and you will surely find a slew of blogs to read.
Yes Finally i have resolved it.
The code which is in Question is working fine now. but Without release some objects and #autoreleasepool block which is in code, memory was increase continuously during scroll UITableView.
From the Instrument i found that memory increase in UILableView and UIImageView. I am using Custom UITableViewCell and in that file i havnt implement dealloc method. So When i have implement dealloc method in UITableViewCell .m file and release & nil all object.
After that memory not increase during scroll TableView and its Resolved the issue.
As per my Understanding there is an issue in your "getSavedImage" Method you have to manage memory Manually instead of 'autorelease' so as My suggestion is use
UIImage *image = [[UIImage alloc] initWithContentsOfFile:filePath]
and also release it after use of it. means after '[self setImage:saveImg];'
[saveImg release]
instead of this.
[[UIImage imageWithContentsOfFile:filePath] retain];
'Don't Use Autorelease because it has staying in memory until pool not drain' and just because of this you got an memory issue.
I have a UITableView that uses paging. All the delegates, and datasource are set.
My table view fetches a list of ten cars over the network and displays them by sending a page number (currentPage). During this fetch request I also get the pageCount which is the number of pages that contains cars on the server. Each page contains 10 cars.
I create a loading cell on the row that equals self.allCars.count which is my car array. This cell then fetches the next ten, and adds them to the self.allCars.count array. A loading cell is then created again for self.allCars.count + 1 etc. (I hope you get the picture, if not please ask).
On first launch the list contains All Cars which is the default request. However, the user can change it from a drop down. For example, they can select Blue Cars. This is passed into the fetchCars methods as the params parameter.
There is an unwanted behaviour in my code however: When I scroll down through the list, with the default paramter selected, and I scroll down three pages (three network calls to fetchCars...) and the array now contains 30 cars displayed in the tableView. However I now want to start a different search from scratch, so I go to the drop down, and select to filter by only blue cars (donePickerBlue). This method removes all the car objects, sets the currentPage back to 1, calls the network for the blue cars, and reloads the data. The unwanted behaviour occurs here. Because there had been 30 cells/indexPath.rows, the network call is called 3 times. This is because the indexPath.row < self.allCars.count is not true. This is where I am stuck, I can't seem to figure out how to fix it, so that if the search parameter is change (blue in this case) that it should treat it as new, I thought the [tableView reloadData] would handle this, but unfortunately it remembers how many index paths there are.
Its something i've been stuck on for a while. I've a feeling im missing something very simple to fix it.
Header file
#property (nonatomic) NSInteger currentPage;
#property (nonatomic) NSInteger pageCount;
Implementation
-(void)viewDidLoad{
...
self.currentPage = 1;
...
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
if (self.allCars.count ==0) {
return 0;
}
else{
if (self.currentPage<self.pageCount)
return self.allCars.count+1;
}
return 0;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell * cell = nil;
if (self.allCars.count!=0) {
if(indexPath.row <self.allCars.count){//here is where the problem occurs
cell=[self customCellForIndexPath:indexPath tableView:tableView];
}
else {
cell=[self loadingCell];
}
}
else{
// Disable user interaction for this cell.
cell = [[UITableViewCell alloc] init];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
return cell;
}
-(UITableViewCell *)loadingCell{
UITableViewCell * cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
UIActivityIndicatorView * activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
activityIndicator.center = cell.center;
cell.backgroundColor = [UIColor lightGrayColor];
[cell addSubview:activityIndicator];
cell.tag=kLoadingCellTag;
[activityIndicator startAnimating];
return cell;
}
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
if (cell.tag==kLoadingCellTag) {
self.currentPage++;
[self performSelector:#selector(getCars:withParams) withObject:nil afterDelay:1.5f];
}
}
-(void)getCars{
[self getCars:url withParams:params];
}
-(void)getCars: (NSURL *)url withParams: (NSString *)params{
NSMutableURLRequest * request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:0 timeoutInterval:80];
[request setHTTPBody:[params dataUsingEncoding:NSUTF8StringEncoding]];
[request setHTTPMethod:#"POST"];
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForResource=1;
NSURLSession * session = [NSURLSession sessionWithConfiguration:sessionConfig];
NSURLSessionDataTask * task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse * httpResp = (NSHTTPURLResponse *)response;
NSDictionary * dataDict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
if (data) {
switch (httpResp.statusCode) {
case 200:{
dispatch_async(dispatch_get_main_queue(), ^{
self.pageCount = [dataDict[#"message"][#"total_pages"] intValue];
NSArray * carsArray = dataDict[#"message"][#"results"];
for (NSDictionary *cDict in carsArray) {
Car *car = [Car carWithID:[cDict[#"car_id"] stringValue] ];
car.car_name=cDict[#"car_name"];
car.car_description = cDict[#"car_description"];
[self.allCars addObject:car];
}
[self.tableView reloadData];
});
break;
}
default:
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Error");
});
break;
}
}
else{
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Error");
});
}
}];
[task resume];
}
//reset list to start new search
-(void)donePickingBlue{
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:YES];
self.currentPage=1;
[self.allCars removeAllObjects];
[self getCars:url withParams:blue];
}
Edit
I seem to have resolved the the problem by doing the following;
//reset list to start new search
-(void)donePickingBlue{
self.currentPage=1;
[self.allCars removeAllObjects];
[self.tableView reloadData];//after removing all the cars, now we call reload, as there are no cars. I was calling reload in `[self getCars:....]` just below, and thought this was enough.
[self getCars:url withParams:blue];
}
I was able to answer my own problem. The answer can be seen in the Edit above incase anybody else has the same problem.
It should have been;
//reset list to start new search
-(void)donePickingBlue{
self.currentPage=1;
[self.allCars removeAllObjects];
[self.tableView reloadData];//after removing all the cars, now we call reload, as there are no cars. I was calling reload in `[self getCars:....]` just below, and thought this was enough.
[self getCars:url withParams:blue];
}
If you want to download cars page by page, willDisplayCell: is pretty good choice. But you must change the condition a little, to prevent downloading the same data multiple times. Also, I recommend you to change data model and provide ability to determine a page for particular cars. That's what I mean:
-(void)tableView:(UITableView *)tableView
willDisplayCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath{
// 10 cells on page
NSUInteger currentPage = indexPath.row / 10;
// Check, if cars for the current page are downloaded
if (carsOnPagesDict[#(currentPage)] != nil) {
// Add a stub to indicate that downloading started
// You can use this later to display correct cell
// Also it prevents getCars: from calling multiple times for the current page
carsOnPagesDict[#(currentPage)] = #"downloading";
// I removed delay for simplicity
[self getCars:url withParams:params forPage:currentPage];
}
}
Also, change getCars method:
-(void)getCars:(NSURL *)url withParams:(NSString *)params forPage:(NSUInteger)page{
// Creating request...
// ...
// Processing response...
// ...
// Array obtained:
NSArray *carsArray = dataDict[#"message"][#"results"];
// Storing required data to the array
NSMutableArray *cars = [NSMutableArray arrayWithCapacity:carsArray.count];
for (NSDictionary *cDict in carsArray) {
Car *car = [Car carWithID:[cDict[#"car_id"] stringValue] ];
car.car_name=cDict[#"car_name"];
car.car_description = cDict[#"car_description"];
[cars addObject:car];
}
// Save cars to the dictionary for the page given
carsOnPagesDict[#(page)] = cars;
// ...
// Resuming tasks...
}
You may consider using CoreData to store that cars.
I have a UItableView that is listing number of outlets respectively, each outlet has a logo image. I save those images locally on iPhone, so if any image is found on iPhone it will fetch from there if not then it will send a service call and fetch data. It is working accordingly but when I scroll down on tableview it gets hanged at certain point when the image is being downloaded, as it is downloaded it works fine again. Is there any solution where I can perform this multitasking of downloading images along with representing them either from service call or fetching it locally.
Here is my code..
// downloading images of outlets locally
NSLog(#"Downloading...");
NSString *imageLink = [NSString stringWithFormat:#"http://cf.abc.pk/outlets/l/%#",outs.logo];
UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageLink]]];
NSLog(#"%f,%f",image.size.width,image.size.height);
NSString *docDir =[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSLog(#"%#",docDir);
NSLog(#"saving png");
NSString *pngFilePath = [NSString stringWithFormat:#"%#/%d.png",docDir,[[arrOutletIds objectAtIndex:webserviceCall] intValue]];
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:pngFilePath];
if(!fileExists)
{
NSData *data1 = [NSData dataWithData:UIImagePNGRepresentation(image)];
[data1 writeToFile:pngFilePath atomically:YES];
}
Let's try:
//When you download something, execute in background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0),
^{
//download some thing
dispatch_async( dispatch_get_main_queue(), ^{
// when download finish execute in main thread to update data
// this function should be called in response of downloading
});
});
Hope this helps you.
Download image in background will solve your problem here
Try this.
if(!fileExists)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data1 = [NSData dataWithData:UIImagePNGRepresentation(image)];
[data1 writeToFile:pngFilePath atomically:YES];
});
}
You can use this code which integrates ImageCache to download async and cache images. This part of code shows downloading images in UITableView. Or you can check out this project on GitHub SDImageCache
if ([[ImageCache sharedImageCache] DoesExist:photoURL] == true)
{
image = [[ImageCache sharedImageCache] GetImage:photoURL];
cell.imageView.image = image;
}
else
{
cell.imageView.image = [UIImage imageNamed:#"PlaceHolder"];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^(void) {
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:photoURL]];
UIImage* image = [[UIImage alloc] initWithData:imageData];
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
if (cell.tag == indexPath.section) {
cell.imageView.image = image;
[cell setNeedsLayout];
[[ImageCache sharedImageCache] AddImage:image imageURL:photoURL];
}
});
}
});
}
Import these files to your project
ImageCache.h
#import <Foundation/Foundation.h>
#interface ImageCache : NSObject
#property (nonatomic, retain) NSCache *imgCache;
#pragma mark - Methods
+ (ImageCache*)sharedImageCache;
- (void) AddImage:(UIImage *)image imageURL:(NSString *)imageURL;
- (UIImage*) GetImage:(NSString *)imageURL;
- (BOOL) DoesExist:(NSString *)imageURL;
#end
ImageCache.m
#import "ImageCache.h"
#implementation ImageCache
#synthesize imgCache;
#pragma mark - Methods
static ImageCache* sharedImageCache = nil;
+(ImageCache*)sharedImageCache
{
#synchronized([ImageCache class])
{
if (!sharedImageCache)
sharedImageCache= [[self alloc] init];
return sharedImageCache;
}
return nil;
}
+(id)alloc
{
#synchronized([ImageCache class])
{
NSAssert(sharedImageCache == nil, #"Attempted to allocate a second instance of a singleton.");
sharedImageCache = [super alloc];
return sharedImageCache;
}
return nil;
}
-(id)init
{
self = [super init];
if (self != nil)
{
imgCache = [[NSCache alloc] init];
}
return self;
}
- (void) AddImage:(UIImage *)image imageURL:(NSString *)imageURL
{
if (image==nil) {
}
else
{
[imgCache setObject:image forKey:imageURL];
}
}
- (NSString*) GetImage:(NSString *)imageURL
{
return [imgCache objectForKey:imageURL];
}
- (BOOL) DoesExist:(NSString *)imageURL
{
if ([imgCache objectForKey:imageURL] == nil)
{
return false;
}
return true;
}
#end
I am a quite new to IOS development and keep having struggle with it. I would like to display phone list which an user has from my server but tableview does not display items. I have got data from server well and I think settings for UItableView is correct. Here is my code:
STKPhoneHolderViewController.h
#import <UIKit/UIKit.h>
#import "STKSimpleHttpClientDelegate.h"
#interface STKPhoneHolderViewController : UITableViewController <UITableViewDataSource, STKSimpleHttpClientDelegate>
#property (strong, nonatomic) IBOutlet UITableView *phoneTable;
#property (strong, nonatomic) NSMutableArray *phoneArray;
#end
STKPhoneHolderViewController.m
#implementation STKPhoneHolderViewController
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
self.phoneTable.dataSource = self;
self.phoneArray = [[NSMutableArray alloc]init];
[self loadPhoneList];
}
#pragma mark - Table view data source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return [self.phoneArray count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"PhoneCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
STKPhoneHolder *phoneHolder = [self.phoneArray objectAtIndex:indexPath.row];
[cell.textLabel setText:phoneHolder.userName];
return cell;
}
#pragma Custom method
- (void) loadPhoneList
{
self.phoneArray = [[NSMutableArray alloc]init];
STKSimpleHttpClient *client = [[STKSimpleHttpClient alloc]init];
client.delegate = self;
NSString *userId = #"your_id_h";
NSString *sUrl = [NSString stringWithFormat:#"%#%#?userid=%#",
MOBILE_API_URL,
PHONEHOLDER_URI,
userId];
[client send:sUrl data:#""];
}
#pragma STKSimpleHttpClientDelegate
-(void) complete:(STKHttpResult*) result
{
if (result.ok != YES){
[STKUtility alert:result.message];
return;
}
self.phoneArray = (NSMutableArray*)result.result;
for (STKPhoneHolder *holder in self.phoneArray) {
NSLog(#"%#", [holder description]);
}
[self.phoneTable reloadData];
NSLog(#" isMainThread(%d)", [NSThread isMainThread] );
}
#end
STKSimpleHttpClient.m
#import "STKSimpleHttpClient.h"
#import "STKSimpleHttpClientDelegate.h"
#implementation STKSimpleHttpClient
NSMutableData *responseData;
STKHttpResult *httpResult;
void (^completeFunction)(STKHttpResult *);
- (void) send:(NSString*)url
data:(NSString*)data
{
httpResult = [[STKHttpResult alloc]init];
dispatch_async(dispatch_get_main_queue(), ^{
if ( data == nil) return;
//Get request object and set properties
NSMutableURLRequest * urlRequest = [NSMutableURLRequest requestWithURL: [NSURL URLWithString: url]];
//set header for JSON request and response
[urlRequest setValue:#"application/json; charset=utf-8" forHTTPHeaderField:#"Content-Type"];
[urlRequest setValue:#"application/json" forHTTPHeaderField:#"Accept"];
//set http method to POST
[urlRequest setHTTPMethod:#"POST"];
//set time out
[urlRequest setTimeoutInterval:20];
NSData *body = [data dataUsingEncoding:NSUTF8StringEncoding];
//set request body
urlRequest.HTTPBody = body;
//connect to server
NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:urlRequest delegate:self];
if (conn==nil){
//Do something
}
});
}
#pragma mark - NSURLConnection Delegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// A response has been received, this is where we initialize the instance var you created
// so that we can append data to it in the didReceiveData method
// Furthermore, this method is called each time there is a redirect so reinitializing it
// also serves to clear it
responseData = [[NSMutableData alloc] init];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// Append the new data to the instance variable you declared
[responseData appendData:data];
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
willCacheResponse:(NSCachedURLResponse*)cachedResponse {
// Return nil to indicate not necessary to store a cached response for this connection
return nil;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// The request is complete and data has been received
// You can parse the stuff in your instance variable noow
NSError *error;
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:&error];
BOOL ok = [[json objectForKey:#"ok"] boolValue];
NSString *message = [json objectForKey:#"message"];
if (ok == NO) {
[httpResult setError:message];
} else {
[httpResult setSuccess:[json objectForKey:#"result"]];
}
if (self.delegate !=nil) {
[self.delegate complete:httpResult];
}
responseData = nil;
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
// The request has failed for some reason!
// Check the error var
if (self.delegate !=nil) {
[self.delegate complete:[httpResult setError:#"Connection failed."]];
}
}
STKPhoneHolder.m
#import <Foundation/Foundation.h>
#interface STKPhoneHolder : NSObject
#property NSString *deviceId;
#property NSString *userId;
#property NSString *userName;
#property NSString *msn;
- (id) initWithDeviceId:(NSString*)aDeviceId
userId:(NSString*)aUserId
userName:(NSString*)aUserName
msn:(NSString*)aMsn;
#end
Log:
2013-12-17 16:14:23.447 [5323:70b] {
deviceId = 11111;
email = "";
msn = 11111111;
role = "";
userId = aaaaaa;
userName = "Joshua Pak";
}
2013-12-17 16:14:23.448 [5323:70b] {
deviceId = 22222;
email = "";
msn = 2222222;
role = "";
userId = bbbbb;
userName = "Jasdf Pak";
}
2013-12-17 16:14:23.449 Stalker[5323:70b] isMainThread(1)
I can see the log printing phoneArray with two phones in 'complete' method but tableview just display "No record". Tableview does not render again even though I called reloadData method. I made sure that [self.phoneTable reloadData] is called in debugging mode.
What do I have to do more?
Try to call reloadData in main thread
#pragma STKSimpleHttpClientDelegate
-(void) complete:(STKHttpResult*) result
{
if (result.ok != YES){
[STKUtility alert:result.message];
return;
}
self.phoneArray = (NSMutableArray*)result.result;
for (STKPhoneHolder *holder in self.phoneArray) {
NSLog(#"%#", [holder description]);
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.phoneTable reloadData];
}
}
Or you can use performSelectorOnMainThread
[self.phoneTable performSelectorOnMainThread:#selector(reloadData)
withObject:nil
waitUntilDone:NO];
I am guessing STKSimpleHttpClient class is calling complete delegate function on different thread, all user interface interaction suppose to be called from main thread.
Try this code to see which thread you are in from the complete delegate function
NSLog(#" isMainThread(%d)", [NSThread isMainThread] );
check this. does the code load the tableview before you get information from web services. if so then write the statement [tableview Reload]; next to the web services information getting process. This will help
It's not necessary to specify the number of sections, but you might want to do it with this code:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
I see you're using a Table View Controller, which already has a tableView reference self.tableView.
Like #rdelmar said, you could use that reference instead of your phoneTable:
[[self tableView] setDataSource:self];
[[self tableView] setDelegate:self];
[[self tableView] reloadData];
I create stack class in my program that I want store NSString value in that.
this is stack class :
#interface Stack : NSObject
- (void)push:(id)obj;
- (id)pop;
- (BOOL)isEmpty;
#end
#implementation Stack
{
NSMutableArray *stack;
}
- (id)init
{
self = [super init];
if(self!= nil){
stack = [[NSMutableArray alloc] init];
}
return self;
}
- (void)push:(id)obj
{
[stack addObject:obj];
}
- (id)pop
{
id lastobj = [stack lastObject];
[stack removeLastObject];
return lastobj;
}
- (BOOL)isEmpty
{
return stack.count == 0;
}
#end
also I have another class with name : TableViewController
I want when to click on cell in TableViewController store cell's id that receive from URL
this is my code:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// I want that xCode value with xCode2 value push in stack
NSLog(#"Row in Tab : %d",indexPath.row);
if ([Folder containsObject:[All objectAtIndex:indexPath.row]]) {
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"http://192.168.1.%d/mamal/filemanager.php?dir=%#&folder=%d&id",IP,xCode,indexPath.row]];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLResponse *response = nil;
NSError *err = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&err];
NSString *responseString = [[NSString alloc] initWithBytes:[data bytes] length:[data length] encoding:NSUTF8StringEncoding];
xCode2 = responseString; //this is new cell's id.I want to push this value in stack
NSLog(#"xcode : %#", xCode2);
[self performSegueWithIdentifier:#"segue4" sender:self];
}
else
{
[self performSegueWithIdentifier:#"segue3" sender:self];
}
}
in top code I want when to click on cell push two value in stack (xCode & xCode2) but I dont know about to use of stack.
you need a variable that holds your stack.. i'd make it a member var:
#implementation TableViewController {
Stack *_stack;
}
...
then when the cell is clicked, push the value
...
if(!_stack)
_stack = [[Stack alloc] init];
[_stack push:xcode2];
...
In addition to what Daij-Djan has suggested, do the following:
#interface Stack : NSMutableArray
- (void)push:(id)obj;
- (id)pop;
- (BOOL)isEmpty;
#end
#implementation Stack
- (id)init
{
self = [super init];
if(self!= nil){
// Perform any initialization here. If you don't then there is no point in implementing init at all.
}
return self;
}
- (void)push:(id)obj
{
[self addObject:obj];
}
- (id)pop
{
id lastobj = [self lastObject];
[self removeLastObject];
return lastobj;
}
- (BOOL)isEmpty
{
return [self count] == 0;
}
#end