How to cache images for a tableview? - ios

I have the following code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//P1
UITableViewCell *cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"Cell Identifier"] autorelease];
cell.textLabel.text = [photoNames objectAtIndex:indexPath.row];
//Check if object for key exists, load from cache, otherwise, load
id cachedObject = [_cache objectForKey:[photoURLs objectAtIndex:indexPath.row]];
if (cachedObject == nil) {
//IF OBJECT IS NIL, SET IT TO PLACEHOLDERS
cell.imageView.image = cachedObject;
[self setImage:[UIImage imageNamed:#"loading.png"] forKey:[photoURLs objectAtIndex:indexPath.row]];
[cell setNeedsLayout];
} else {
//fetch imageData
dispatch_async(kfetchQueue, ^{
//P1
NSData *imageData = [NSData dataWithContentsOfURL:[photoURLs objectAtIndex:indexPath.row]];
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = [UIImage imageWithData:imageData];
[self setImage:cell.imageView.image forKey:cell.textLabel.text];
[cell setNeedsLayout];
});
});
}
return cell;
}
Other than this, the viewDidLoad method fetches from the web, the json result from flickr, to populate a photoNames and photoURLs. Im trying to cache already downloaded images into a local NSDictionary. The problem is that the images arent loaded. Not even the loading.png placeholder image.

You want to save it in the app's Documents directory:
NSData *imageData = UIImagePNGRepresentation(newImage);
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *imagePath =[documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:#"%#.png",#"cached"]];
NSLog((#"pre writing to file"));
if (![imageData writeToFile:imagePath atomically:NO])
{
NSLog((#"Failed to cache image data to disk"));
}
else
{
NSLog((#"the cachedImagedPath is %#",imagePath));
}
Then just save the path in your NSMutableDictionary with:
[yourMutableDictionary setObject:theIMagePath forKey:#"CachedImagePath"];
Then retrieve it with something like:
NSString *theImagePath = [yourMutableDictionary objectForKey:#"cachedImagePath"];
UIImage *customImage = [UIImage imageWithContentsOfFile:theImagePath];
I recommend saving the dictionary inside NSUserDefaults.

Related

Asynchronously load images from document directory into UICollectionView

I am trying to load images from document directory into UICollectionView. It works: smooth, fast....(lol). But I get one issue: when I call [collectionView reloadData], images flicker(once time). Here is my code:
- (UICollectionViewCell*)collectionView:(UICollectionView *)collectionView_ cellForItemAtIndexPath:(NSIndexPath *)indexPath {
NSString *cellIdentifier = NSStringFromClass([AFMyRecentCollectionViewCell class]);
AFMyRecentCollectionViewCell *cell = (AFMyRecentCollectionViewCell*)[collectionView_ dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
AFMyRecentObject *recent = [[AFRecentManager sharedInstance].arrayRecent objectAtIndex:indexPath.row];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
UIImage *image;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *filePath = [self databasePathWithPathComponent:[NSString stringWithFormat:#"%#.jpg", recent.id_film]];
if ([fileManager fileExistsAtPath:filePath] == YES) {
image = [UIImage imageWithContentsOfFile:filePath];
}
dispatch_async(dispatch_get_main_queue(), ^{
if (image) {
cell.imageView.image = image;
} else {
cell.imageView.image = [UIImage imageNamed:#"loadding.png"];
}
});
});
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
return cell;
}
- (NSString *)databasePathWithPathComponent:(NSString*)pathComponent {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex: 0];
NSString *path = [documentsDirectory stringByAppendingPathComponent:pathComponent];
return path;
}
You have to place and default/placeholder image to prevent that..
// this prevents the flicker/blinks
cell.imageView.image = nil; // if you dont have any image as placeholder
//since you have
cell.imageView.image = [UIImage imageNamed:#"loadding.png"]; // this i assume your placeholder image?
dispatch_async(queue, ^{
//your codes..
dispatch_async(dispatch_get_main_queue(), ^{
// your codes..
});
});
I have faced same issue of flickring the images & solved it using below approach.
Apply same lazy loading concept. Like create one dictionary object to store all images before displaying, Before getting the image from the document directory check wether that images is there in the dictionary if it's there then then user that image to display it.
If image is not there in the dictionary then do database operation.
Hope this will resolve your issue.
The problem is that you're initiating the asynchronous update of the image view, but if the cell is reused, you're going to see the old image there. So, before you dispatch_async the process to retrieve the image, make sure you set cell.imageView.image to nil first. So make sure you never see the prior image in the cell before initiating the asynchronous retrieval of the correct image.

Loading images in collection view from documents folder

I have a list of images in my resources folder that I want to load into a collection view
I have my images file names in an NSAarray from the documents directory.
.h
{
NSArray *fileList;
NSString * searchedImage;
}
.m
NSFileManager *filesm = [NSFileManager defaultManager];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSArray *getFilelist= [filesm contentsOfDirectoryAtPath:[paths objectAtIndex:0] error:nil];
BOOL isDirectory;
for (searchedImage in getFilelist){
BOOL fileExistsAtPath = [[NSFileManager defaultManager] fileExistsAtPath:searchedImage isDirectory:&isDirectory];
if (fileExistsAtPath) {
if (isDirectory)
{
//Found Directory
}
}
if ([[searchedImage pathExtension] isEqualToString:#"png"]) {
//This is Image File with .png Extension
NSLog(#"directoryContents = %#",searchedImage);
}
}
//count files
NSFileManager *fm = [NSFileManager defaultManager];
NSArray *pathsAmount = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
fileList= [fm contentsOfDirectoryAtPath:[pathsAmount objectAtIndex:0] error:nil];
int filesCount = [fileList count];
NSLog(#"filesCount:%d", filesCount);
UINib *cellNib = [UINib nibWithNibName:#"NibCell" bundle:nil];
[self.appliancesCollectionView registerNib:cellNib forCellWithReuseIdentifier:#"cvCell"];
}
return self;
}
this reurns my image names correctly as 63293446.png, appliance63293447.png, appliance63293448.png, appliance63293449.png, etc in viewDidLoad
I want to load these into my collection view cells. Currently it returns a null value for my images file names when in the -(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath method
What ive tried so far in addtion to researching on here
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 1;
}
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return fileList.count;
}
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"cvCell";
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
cell.backgroundView = [[UIImageView alloc] initWithImage: [UIImage imageNamed: [NSString stringWithFormat:#"%#",searchedImage]]];
NSLog(#"cell Bg image %#",searchedImage); <-----------------//returns null
return cell;
}
try this code
cell.backgroundView = [[UIImageView alloc] initWithImage: [UIImage imageNamed: [NSString stringWithFormat:#"%#",[fileList objectAtIndex:indexPath.row]]]];
make sure searchedImage is not nil.
The file name of the image is in your fileList array, not in searchedImage, (this last variable is useless when creating the image).
NSString *fileName = [fileList objectAtIndex:indexPath.row];
cell.backgroundView = [[UIImageView alloc] initWithImage: [UIImage imageNamed: fileName]];
The problem here is that in your code, fileList can contain files that are not images, thus imageNamed: will fail, and return nil. You should filter your fileList array, and make sure it contains only images.
At first make fileList mutable via NSMutableArray *fileList;
next in block add image path to fileList
if ([[searchedImage pathExtension] isEqualToString:#"png"]) {
//This is Image File with .png Extension
NSLog(#"directoryContents = %#",searchedImage);
[fileList addObject:searchedImage]
}
then in cellForItemAtIndexPath: set image with
cell.backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageWithContentsOfFile:[fileList objectAtIndex:indexPath.row]]];

Retrieving core data object from cell

I am saving an object from core data to a cell as listed bellow. The URL is being saved to the cell correctly and working great. My problem is that, when a user taps on a cell, I would like the URL that is saved to that cell to be passed to my detailedViewController for use. I have some code that I have tried but the url is nil in the detailedViewController. If you have any better way of accomplishing the same thing, that would be fine. The code is listed bellow -
Here is where I save it to the cell:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"Cell";
PhotoCell *cell = (PhotoCell *)[self.tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
cell = [[PhotoCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
}
// Configure the cell...
FeedEntity *feed = [_fetchedResultsController objectAtIndexPath:indexPath];
NSData *data = feed.imageData;
self.feedImage = [UIImage imageWithData:data];
cell.thumbImage = self.feedImage;
NSData *stringData = feed.urlString;
self.stringForURL = [[NSString alloc] initWithData:stringData encoding:NSUTF8StringEncoding];
self.stringForURL = [self.stringForURL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
self.finalURL = [NSURL URLWithString:self.stringForURL];
cell.finalURL = self.finalURL;
return cell;
}
Here is where I retrieve the url from the cell and pass it to the detailedViewController:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSManagedObject *object = [[self fetchedResultsController] objectAtIndexPath:indexPath];
// Code to create detailed view and set properties
self.detailsViewController = [[DetailsViewController alloc] init];
NSIndexPath *path = [self.tableView indexPathForSelectedRow];
FeedEntity *feed = [_fetchedResultsController objectAtIndexPath:path];
NSData *stringData = feed.urlString;
NSString *stringURL = [[NSString alloc] initWithData:stringData encoding:NSUTF8StringEncoding];
NSLog(#"Here is the string before: %#", stringURL);
stringURL = [self.stringForURL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *urlForDetail = [NSURL URLWithString:self.stringForURL];
NSLog(#"Here is the url before it goes to the detailed: %#", urlForDetail);
self.detailsViewController.finalURL = urlForDetail;
[self.navigationController pushViewController:self.detailsViewController animated:YES];
}
Save the video (in didFinishPickingMediaWithInfo:):
self.videoURL = [info objectForKey:UIImagePickerControllerMediaURL];
NSData *videoData = [NSData dataWithContentsOfURL:self.videoURL];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *path = [documentsDirectory stringByAppendingFormat:#"/vid1.mp4"];
self.urlForSave = [NSURL fileURLWithPath:path];
//Look at YES
[videoData writeToFile:path atomically:YES];
[self saveImageAndVideo];
Here is SaveVideoAndPhoto:
- (void)saveImageAndVideo {
NSManagedObjectContext *context = [self managedObjectContext];
FeedEntity *feedEntity = [NSEntityDescription insertNewObjectForEntityForName:#"FeedEntity" inManagedObjectContext:context];
NSData *imageData = UIImageJPEGRepresentation(self.thumbImage, 0.8f);
self.photoData = imageData;
NSString *stringForSave = [self.urlForSave absoluteString];
NSLog(#"URL before save: %#", stringForSave);
//NSData * stringData = [stringForSave dataUsingEncoding:NSUTF8StringEncoding];
[feedEntity setValue:imageData forKey:#"imageData"];
[feedEntity setValue:[NSDate date] forKey:#"timeStamp"];
[feedEntity setValue: stringForSave forKey:#"urlString"];
NSError *error = nil;
if (![self.managedObjectContext save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
} else {
NSLog(#"URL's are being saved");
}
}
Your problem is in the code in cellForRowAtIndexPath:
self.finalURL = [NSURL URLWithString:self.stringForURL];
You are setting the URL as a property of SELF, which in this case is your viewController. You want to set it on the CELL. Change all that code when you create the cell from self.whatever to cell.whatever if you want to save them as properties of the cell. It might help you if you did some reading up on scope in objective-c.
Also, on a side note, there are a few things you are doing that are unnecessary. Firstly is this:
NSIndexPath *path = [self.tableView indexPathForSelectedRow];
You don't need to create an indexPath object inside this function, because you are already provided it by the function itself with the variable indexPath:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
Secondly, inside of didSelectRowAtIndexPath, if you want to get the url, you should be doing something like this:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// deselect the row if you want the cell to fade out automatically after tapping
[tableView deselectRowAtIndexPath:indexPath animated:YES];
// get a reference to the cell that the user tapped
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
// get the url from the cell, assuming your cell has a property called finalURL on it that you set some value
// to originally when it was created
NSURL *url = cell.finalURL;
// do something with that URL here...
}
Keep in mind this is slightly unorthodox. You really should get the information from your tableView's datasource. If you have an array of objects that you are using to populate your tableView, it's probably a better idea to simply get the object itself from our array with the given indexPath rather than save the information on the cell as a property and access it that way. I would highly suggest watching some tutorial videos or do some reading up, preferably in the iOS docs themselves, to try to learn best practices for UITableViews.

Loading images only when needed

When I load the images to show to the UICollectionView I load all the images from the array like this
- (void)viewDidLoad
{
allImagesArray = [[NSMutableArray alloc] init];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *location=#"Others";
NSString *fPath = [documentsDirectory stringByAppendingPathComponent:location];
NSArray *directoryContent = [[NSFileManager defaultManager] directoryContentsAtPath: fPath];
collectionOthers.delegate =self;
collectionOthers.dataSource=self;
for(NSString *str in directoryContent)
{
NSString *finalFilePath = [fPath stringByAppendingPathComponent:str];
NSData *data = [NSData dataWithContentsOfFile:finalFilePath];
if(data)
{
UIImage *image = [UIImage imageWithData:data];
[allImagesArray addObject:image];
NSLog(#"array:%#",[allImagesArray description]);
image = nil;
}
finalFilePath=nil;
data=nil;
}
paths= nil;
documentsDirectory= nil;
location= nil;
fPath= nil;
directoryContent = nil;
}
This is the biggest issue in my app since it uses so many memory. It is because number and size of the images, this could just take up memory. I would only want to load images when they are needed, and discard them when they are no longer needed.However I do not know where and how to change my code so that it will be that way. I am doing this for three month or so and I really need help.
Update
This is my code for the specific part
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *reuseID = #"ReuseID";
OthersCell *mycell = (OthersCell *) [collectionView dequeueReusableCellWithReuseIdentifier:reuseID forIndexPath:indexPath];
UIImageView *imageInCell = (UIImageView*)[mycell viewWithTag:1];
imageInCell.image = [allImagesArray objectAtIndex:indexPath.row];
NSLog(#"a");
return mycell;
}
Clearly, you should load the images just-in-time. One should never hold an array of images (because they take up a lot of memory), but rather just hold an array of filenames. So I'm suggesting you retire allImagesArray and instead define a NSMutableArray called filenames. You could then create the UIImage objects on the fly:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"Cell";
OthersCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
UIImageView *imageInCell = (UIImageView*)[cell viewWithTag:1];
imageInCell.image = [UIImage imageWithContentsOfFile:filenames[indexPath.item]];
return cell;
}
This, assumes, of course, that you populated this NSMutableArray of filenames in viewDidLoad:
- (void)viewDidLoad
{
filenames = [[NSMutableArray alloc] init];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *location=#"Others";
NSString *fPath = [documentsDirectory stringByAppendingPathComponent:location];
NSArray *directoryContent = [[NSFileManager defaultManager] directoryContentsAtPath: fPath];
collectionOthers.delegate =self;
collectionOthers.dataSource=self;
for(NSString *str in directoryContent)
{
NSString *finalFilePath = [fPath stringByAppendingPathComponent:str];
[filenames addObject:fileFilePath];
}
}
This has a problem, though, because imageWithContentsOfFile (as well as loading it into a NSData first and then doing imageWithData) is a bit slow if the images aren't tiny. On slower devices, this can result in a slight stuttering of a quick scroll of a collection view. So, a better approach would be to (a) load the images asynchronously; (b) use a NSCache to optimize performance for when you scroll backwards.
So, first, define a cache:
#property (nonatomic, strong) NSCache *imageCache;
And, instantiate this in viewDidLoad:
self.imageCache = [[NSCache alloc] init];
self.imageCache.name = #"com.company.app.imageCache";
And then, cellForItemAtIndexPath can (a) set the image from the cache; and (b) if not found, retrieve the image asynchronously updating cache and cell appropriately, e.g.:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"Cell";
OthersCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
UIImageView *imageInCell = (UIImageView*)[cell viewWithTag:1];
NSString *cacheKey = filenames[indexPath.item];
imageInCell.image = [self.imageCache objectForKey:cacheKey];
if (imageInCell.image == nil) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *image = [UIImage imageWithContentsOfFile:filenames[indexPath.item]];
if (image) {
[self.imageCache setObject:image forKey:cacheKey];
dispatch_async(dispatch_get_main_queue(), ^{
OthersCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath];
        UIImageView *imageInCell = (UIImageView*)[updateCell viewWithTag:1];
imageInCell.image = image;
});
}
});
}
return cell;
}
And, obviously, make sure you purge the cache if you receive memory warnings (in iOS 7, the cache doesn't always automatically purge itself under pressure like it used to do):
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
[self.imageCache removeAllObjects];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
This is the method in which you should be loading the images.
In viewDidLoad, I'd build the array of NSString file paths to each image, then I'd use the collectionView:cellForItemAtIndexPath: method to load the image from the specific file path for this particular cell.
In viewDidLoad You could just load a list of available images. So remove the for loop: for(NSString *str in directoryContent) { ... } loop there (EDIT: or make it a simple for loop, just to populate an array with filenames for the files having data).
When you update a specific collectionviewcell in collectionView:cellForItemAtIndexPath:, just load the image (only 1). The cell will now hold the image data instead of your view controller. So when the cell is released, so is the image data.

Load all the images that are saved in a plist into a tableview

In my project I am saving the images into the documents folder on my phone then i am loading them in a separate tableview. I am getting a bit of success, the very last image that is taken is loaded into the table, but is loaded into every row instead of just the last one. Here is the code i used to load the image in the tableview :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"List";
ListCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
//Load PLIST
NSArray *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [path objectAtIndex:0];
NSString *plistPath = [NSString stringWithFormat:#"%#/images.plist", documentsDirectory];
//Load PLIST into mutable array
NSMutableArray *imageArray = [NSMutableArray arrayWithContentsOfFile:plistPath];
for (NSDictionary *dict in imageArray) {
//Do whatever you want here, to load an image for example
NSString *imageFilePath = [NSHomeDirectory() stringByAppendingPathComponent:[dict objectForKey:#"Original Image"]];
UIImage *image = [UIImage imageWithContentsOfFile:imageFilePath];
[cell.imageofItem setImage:image];
}
}
Here is an example of what is happening
: Say i take 2 photos, one is called "10282012_13113138_image.jpg" and the other is called "10282012_13113468_image.jpg". Then i go to load the images in the cell, and the last photo is loaded in the two cells.
Any help would be much appreciated!
Instead of
for (NSDictionary *dict in imageArray) {
NSString *imageFilePath = [NSHomeDirectory() stringByAppendingPathComponent:[dict objectForKey:#"Original Image"]];
UIImage *image = [UIImage imageWithContentsOfFile:imageFilePath];
[cell.imageofItem setImage:image];
}
Try
NSDictionary *dict = [imageArray objectAtIndex:indexPath.row];
NSString *imageFilePath = [NSHomeDirectory() stringByAppendingPathComponent:[dict objectForKey:#"Original Image"]];
UIImage *image = [UIImage imageWithContentsOfFile:imageFilePath];
[cell.imageofItem setImage:image];
The problem was that for each indexPath.row, you were iterating till the last element in the array, constantly overwriting the cell image until you get to the last image. Then for the next indexPath.row, you do the same thing, and setting that to the last image in the array, and so on....

Resources