Am just starting with RAC and am wondering how to load images asynchronously in a cell within a TableView.
I was trying with the example in the doc, but to be honest, I didn't understand so well...
The thing is that the project is written with RAC, so I want to do the right things.
What have I tried?:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"tableCell";
CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.elementName.text = self.myListOfElements[indexPath.row].elementName;
RAC(cell.imageView, image) = [[finalImage map:^(NSURL *url) {
return [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self.myListOfElements[indexPath.row].url]]];
}] deliverOn:RACScheduler.mainThreadScheduler];
}
But this is not working...
Does anybody know the proper way to do this with RAC?
I have never used RAC, but based on the examples it looks you are referencing a URL that you do not seem have.
RAC(self.imageView, image) =
[
[
[
[
client fetchUserWithUsername:#"joshaber"
]
deliverOn:[RACScheduler scheduler]
]
map:^(User *user) {
// Download the avatar (this is done on a background queue).
return [[NSImage alloc] initWithContentsOfURL:user.avatarURL];
}
]
// Now the assignment will be done on the main thread.
deliverOn:RACScheduler.mainThreadScheduler
];
In the example this section:
[
[
client fetchUserWithUsername:#"joshaber"
]
deliverOn:[RACScheduler scheduler]
]
-fetchUserWithUsername is the function that returns the User object that contains the URL used to download the image.
This line assumes that there is already a UIImageView object created and set on the CustomTableViewCell's imageView property:
RAC(cell.imageView, image) = [[finalImage map:^(NSURL *url) {
Presumably this UIImageView object is being created in the CustomTableViewCell class's initializer or in -prepareForReuse method. If it's not, that could be part of your problem.
Be aware, though, that this code doesn't look that safe. If a CustomTableViewCell instance is reused, then you could end up calling RAC(cell.imageView, image) on the object a second time, which would be a problem. (On the other hand, if -prepareForReuse is creating a new UIImageView each time, then this shouldn't be a problem.)
Related
I am trying to display images in UITableViewCell, I'm using web service to get the image names. Here is what i'm getting in the response of web service.
"2514466130834f61a9g38d57h0e2cb-25-ProfilePic.PNG",
"26144482598634297156heb8c0dafg-26-ProfilePic.PNG",
"2714464392629h60253gd4ec8b1f7a-27-ProfilePic.PNG",
"2814449113316a7gh5db0e2139c84f-28-ProfilePic.PNG",
"2914454043856e3519ah0dbf27cg84-29-ProfilePic.PNG"
The above response is saved in the array format. These array objects are then append on a url, And I need to display images on UIImageView.
Please Help me.
Prepare your url like below.
// Consider this is your prefix url
NSString *urlPrefix = #"https://your_url_prefix/"; // like http://stackoverflow.com/
// Consider this is the array you parsed from the web service
NSArray *resourceArray = #[#"2514466130834f61a9g38d57h0e2cb-25-ProfilePic.PNG", #"26144482598634297156heb8c0dafg-26-ProfilePic.PNG", #"2714464392629h60253gd4ec8b1f7a-27-ProfilePic.PNG", #"2814449113316a7gh5db0e2139c84f-28-ProfilePic.PNG", #"2914454043856e3519ah0dbf27cg84-29-ProfilePic.PNG"];
Prepare the resource url in your tableView's data source method: cellForRowAtIndexPath
NSString *resourceURL = [urlPrefix stringByAppendingString:[resourceArray objectAtIndex:indexPath.row]];
and use SDWebImage library to load the image and set that in to your cell imageView like
[cell.imageView setImageWithURL:resourceURL];
Note: SDWebImage lib can be downloaded from SDWebImage Git
Add this library files in your project and import like below to use.
#import "UIImageView+WebCache.h"
First download Asynchronous Class Here
Then put your .m file
#import "AsyncImageView.h"
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpletableidentifier=#"simpletableidentifier";
UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:simpletableidentifier];
if(cell == nil)
{
cell.backgroundColor=[UIColor clearColor];
cell = [[UITableViewCell alloc ]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:simpletableidentifier];
}
AsyncImageView *Galleryimg = [[AsyncImageView alloc]initWithFrame:CGRectMake(0,0,40,40)];
Galleryimg.contentMode = UIViewContentModeScaleAspectFill;
Galleryimg.clipsToBounds=YES;
Galleryimg.tag=i;
Galleryimg.imageURL=[NSURL URLWithString:[Yourstring stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
[scroll addSubview:Galleryimg];
return cell;
}
I am using ReusableCell in my cellForRowAtIndexPath method like that;
NSDictionary *cellData = [_cellArray objectAtIndex:indexPath.row];
static NSString *kCellID = #"homePage";
_cell = (HomePageCell*) [tableView dequeueReusableCellWithIdentifier:kCellID];
if(_cell == nil)
{
_cell = [[HomePageCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:kCellID withDictionary:cellData];
}
__weak typeof(_cell) weakSelf = _cell;
[_cell.rootImageView setImageWithURLRequest:_cell.requestImage placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image)
{
weakSelf.rootImageView.image=image;
} failure:nil];
and my rootImageView object is configuring. My problem is start after scrolling like that;
In Cell-1 rootImageView.image should be Image-1
In Cell-2 rootImageView.image should be Image-2
In Cell-3 rootImageView.image should be Image-3
When i starting scroll and my code is coming out if(_cell == nil), my all images turned back same image as Image-1. However, when i logged [_cellArray description], data is totally true. What is the point i missed? Why all cell images turned back Image-1 after cell is not nil?
EDIT : This is really weird, if i don't use kCellID images are loading correct. I switched _cell property to HomePageCell *cell.
As others have already pointed out, it looks like you’re storing _cell as an instance variable. Never do that, as each cell instance must be dequeued/created ‘on the fly’ as they are requested to be displayed onscreen.
If your UITableViewCell instance is a custom subclass (which I assume it is), you should override its -prepareForReuse method, where you should set the imageView’s image property to nil. Otherwise you could assign it a nil value in your -tableView: cellForRowAtIndexPath: implementation.
Also, because of the way that cells are quickly queued/reused/discarded, you should check that you are assigning the requested image by the time it arrives from the network to the right cell. To do so, instead of keeping a weak reference to the cell (as you’re doing), inside the network request completion block get a new reference to the cell with the desired indexPath.
In all, your implementation should look like this:
static NSString *kCellID = #"homePage”; // defined somewhere else, probably at the top of your .m file
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
HomePageCell *cell = (HomePageCell *)[tableView dequeueReusableCellWithIdentifier:kCellID];
if(cell == nil) {
cell = [[HomePageCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:kCellID withDictionary:cellData];
}
// here we reset the cell’s imageView image property to nil
// you can/should also do this inside the subclass implementation by overriding -prepareForReuse
cell.rootImageView.image = nil;
// request the remote image and set it as the imageView image property
[cell.rootImageView setImageWithURLRequest:cell.requestImage placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image)
{
// since the response could come back some time after the request,
// we need to get a fresh, new reference to the cell (which might
// have been dequeued and it’s no longer the same one we got before)
HomePageCell *aCell = (HomePageCell *)[tableView cellForRowAtIndexPath:indexPath];
aCell.rootImageView.image = image;
} failure:nil]
return cell;
}
I assume that _cell is a property on your class which is just wrong. Never store a cell as a property, it's not meant to be stored! Just create a local variable and assign the image to that. It's also not such a good idea but it should work at least. The theory of UITableView is you provide the cell once, then you don't modify it! If you want, you reload the table view, it will ask you again for the cell and then you can modify that way. You should never store any kind of pointer to a UITableViewCell or any subclass of it.
I assume you get that URL from cellData, in which case the problem is that you do not reset this property, so when a cell is reused you are still using the same cellData from the initWithStyle:reuseIdentifier:withDictionary: method. You should do something like:
if(_cell == nil)
{
_cell = [[HomePageCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:kCellID withDictionary:cellData];
} else {
[_cell setCellData:cellData];
}
Currently I am loading all of my UITableViewControllers with images and text. I'm not sure if there is a way of shortening my loading times. I'm thinking that GCD might be the best route to go, however, I'm not too sure that I'm using this correctly:
dispatch_async(dispatch_get_main_queue(), ^{
[some method];
});
This is being loaded in the ViewDidLoad, and I'm unsure if this is the correct place to use GCD. Also, is this correct way of asynchronously loading information?
use this, it downloads multiple files in different threads.And its asynchronous.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//load image here
});
If you want to load all images ,even the images which might be very down in tableView (say image at 20th row).Then viewDidLoad is perfect approach, or if you want to only load images that are currently to be shown, then download images in cellForRow:atIndexPath method.
OR,you can use AFNetworking to download multiple files much efficiently.
Hope this helps
For loading images faster in table view cells you can use SDWebImage.
It is very simple to use. Just import the SDWebImage folder into your Xcode project. It contains a category on UIImageVIew. So on any imageView object just call the method setImageWithURL as illustrated below:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:MyIdentifier] autorelease];
}
// Here we use the new provided setImageWithURL: method to load the web image
[cell.imageView setImageWithURL:[NSURL URLWithString:#"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
cell.textLabel.text = #"My Text";
return cell;
}
I have an app where I load a lot of large images. When I lazy-load them, and even after the image has been loaded, the cell does not load them until I take my finger off the screen. I am calling my downloadImageForVisiblePaths function in the UIScrollViewDelegate methods scrollViewDidEndDragging and in scrollViewDidEndDecelerating apart from this, I am also setting the image in the UITableView's cellForRowAtIndexPath method like so:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
// Code to load reusable custom cell
CustomObject *object = (CustomObject*) [self.tableArray objectAtIndex: indexPath];
if(!object.mainImage){
[self downloadImageForIndexPath:indexPath];
cell.mainImageView.image = [UIImage imageNamed:#"placeholder"];
}else{
cell.mainImageView.image = object.mainImage;
}
return cell;
}
Where the downloadImageForIndexPath looks like this:
-(void) downloadImageForIndexPath:(NSIndexPath*) indexPath{
UIImage *loadedImage = [[UIImage alloc] init];
// take url and download image with dispatch_async
// Once the load is done, the following is done
CustomCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
cell.mainImageView.image = loadedImage;
CustomObject *object = (CustomObject*) [self.tableArray objectAtIndex: indexPath];
object.mainImage = loadedImage;
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableVIew reloadData];
});
}
I can't see where I am going wrong. I need the images to load even when the finger is on the screen. This behaviour is similar to how the images load on apps like Google+, Instagram or Facebook.
Any pointers will be much appreciated.
It's hard to tell since you didn't include all the code for downloadImageForIndexPath, but it looks like you are assigning an image to a cell from a background thread (you shouldn't touch UI controls from background threads). Also, if you'r updating cell directly, you don't need to call reloadData.
I would also suggest using SDWebImage for displaying remote images in a tableview.
I am using a tableview which loads images from the documents directory, creates a thumbnail and shows it in the tableview. However, I have a problem: it becomes slow and crashes as the pictures are large, taken using the camera.
I have explored several solution including GCD to do the work in a background thread but the result is the same thing. So, I thought to look into SDWebImage but I don't know if it will also work for local files, not web images in this case. Can someone advise me please? If not, how is this problem solved? Is there an API that can help to resolve this issue?
That question is not easy to answer as the Question is asked fairly broad but I will do my best.
First, I usually dispatch a Background Thread if I have expensive processing to do as to not block the Main Thread, which is fairly important.
I don't really know why you are not using the normal UIImageView for what you are doing but try to implement following :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"YourCell";
MyCellClass *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[MyCellClass alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
/*
Whatever Code you want
*/
NSArray* params =#[cell.myImageView, #"http://myfancyimages.com/image.png"];
[self performSelectorInBackground:#selector(loadEventImageWithParameters:) withObject:params];
return cell;
}
And now add the function :
- (void) loadEventImageWithParameters:(id) parameters {
NSArray* params = [[NSArray alloc] initWithArray:(NSArray*)parameters];
NSURL *url = [NSURL URLWithString:[params objectAtIndex:0]];
UIImage *image = [UIImage imageWithData: [NSData dataWithContentsOfURL:url]];
UIImageView* theImageView = (UIImageView*) [params objectAtIndex:0];
[theImageView setImage:image];
}
If you got a lot of Pictures to load you are well advised to queue your Processes so you don;t "steal" all resources with Grand Central Dispatch.
Have a read through this excellent post http://www.raywenderlich.com/4295/multithreading-and-grand-central-dispatch-on-ios-for-beginners-tutorial for further details.
Hope that helped