UIImageViews in UITableViewCells are recreating themselves in cached cells - ios

I've got a UITableViewCell and I need to randomly put interstitial images into some of them. This is working, but due to the caching of the cells, the images are repeating themselves further down the table. Let's say I've pre-decided to put images in cells 1, 12, and 25, those cells will have their images, but after cell 1, cells 4, 8, etc. might also contain that first image.
I'm guessing what I need to do is specifically tell cellForRowAtIndexPath clear the cache and remove any images if it doesn't meet the criteria to add an image. How would I go about doing that? I've already tried an else on the if statement that makes the decision and assigns the image. In that else, I've tried setting the UIImageView's image to nil, but that hasn't made any difference.
Is there any other way I can clear the cache to prevent these images from spamming their way down the page at regular intervals? Here's my cellForRowAtIndexPath method, and the lines currently in that last else aren't having an effect on the images, so they'll (I think) need replacing with something that works!
- (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath
{
// Make and allocate the cell if necessary.
CustomCell *cell = [tableView dequeueReusableCellWithIdentifier: #"Cell"];
if (cell == nil) {
cell = [[CustomCell alloc] initWithStyle: UITableViewCellStyleValue1 reuseIdentifier: #"Cell"];
}
cell.lineLabel.text = [[self.fetchedResultsController objectAtIndexPath: indexPath] line];
cell.actorLabel.text = [[self.fetchedResultsController objectAtIndexPath: indexPath] actor];
[cell.lineLabel sizeToFitMultipleLines];
// Get the size each label needs to be when constrained to set width and very long height (8000).
CGSize labelSize = [cell.lineLabel.text sizeWithFont: cell.lineLabel.font constrainedToSize: CGSizeMake(265, 8000)];
if ([[cellsToContainInterstitialImages allKeys] containsObject: [thisLine.lineID stringValue]]) {
UIImageView *interstitialImage = [[UIImageView alloc] init];
NSNumber *cellTypeNumber = [cellsToContainInterstitialImages valueForKey: [thisLine.lineID stringValue]];
kInterstitialCellType cellType = [cellTypeNumber intValue];
if (cellType == kInterstitialCellTypeFirst) {
interstitialImage = [[UIImageView alloc] initWithImage: [UIImage imageNamed: #"man.png"]];
}
else if (cellType == kInterstitialCellTypeSecond) {
interstitialImage = [[UIImageView alloc] initWithImage: [UIImage imageNamed: #"woman.png"]];
}
else {
interstitialImage = [[UIImageView alloc] initWithImage: [UIImage imageNamed: #"child.png"]];
}
[interstitialImage setFrame: CGRectMake(10, cell.lineLabel.frame.size.height + cell.actorLabel.frame.size.height + 10, 150, 150)];
[cell addSubview: interstitialImage];
} else {
UIImageView *interstitialImage = [[UIImageView alloc] init];
[interstitialImage setImage: nil];
}
return cell;
}

The else branch of your code does not add the interstitialImage image to the cell. You should change your code to add the UIImageView in the constructor of the cell, so that in the cellForRowAtIndexPath you'd have to set the image, rather than adding subviews.
Add the UIImageView to your CustomCell, make it private, and provide a property for the cellForRowAtIndexPath to use:
#interface CustomCell : UITableViewCell {
UIImageView *_interstitialImage;
}
#property (nonatomic,readonly) UIImageView *interstitialImage;
#end
#implementation CustomCell
-(id)initWithStyle:(UITableViewCellStyle) style reuseIdentifier:(NSString*)reuseId {
if (self = [super initWithStyle:style reuseIdentifier:reuseId]) {
_interstitialImage = [[UIImageView alloc] init];
[_interstitialImage setImage: nil];
[_interstitialImage setFrame: CGRectMake(10, cell.lineLabel.frame.size.height + cell.actorLabel.frame.size.height + 10, 150, 150)];
[cell addSubview: _interstitialImage];
}
return self;
}
-(UIImageView*)interstitialImage {
return _interstitialImage;
}
#end
Now your cellForRowAtIndexPath would use cell.interstitialImage instead of allocating the new UIImageView all the time. This would avoid adding the image multiple times:
if ([[cellsToContainInterstitialImages allKeys] containsObject: [thisLine.lineID stringValue]]) {
NSNumber *cellTypeNumber = [cellsToContainInterstitialImages valueForKey: [thisLine.lineID stringValue]];
kInterstitialCellType cellType = [cellTypeNumber intValue];
if (cellType == kInterstitialCellTypeFirst) {
[cell.interstitialImage setImage:[UIImage imageNamed: #"man.png"]];
} else if (cellType == kInterstitialCellTypeSecond) {
[cell.interstitialImage setImage:[UIImage imageNamed: #"woman.png"]];
} else {
[cell.interstitialImage setImage: [UIImage imageNamed: #"child.png"]];
}
} else {
[cell.interstitialImage setImage: nil];
}

The easiest solution should be to use – tableView:willDisplayCell:forRowAtIndexPath: method of UITableViewDelegate Protocol. Clean your cells in this method.

The problem is that in the if part you add an image view. This means as cells are reused you keep adding more and more images views.
In the else part you create an dummy image view and set its image to nil. This code does not do what you intended - to clear any existing image.
What you need to do is to ensure you never add a second image view to a cell. And when you need to clear the image, you get a reference to the existing image view, not create a new one.

Related

Custom Cell init in 'cellForItemAtIndexPath' causing stacked cell to stack views

I create a cell using this func:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
BOOL isFocusOn = [_userDefault boolForKey:#"mixFocusOn"];
CDCChannelStrip *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cell" forIndexPath:indexPath];
if (isFocusOn == TRUE) {
NSNumber *setChan = [self.focusChannels objectAtIndex:indexPath.section];
NSInteger chanInt = [setChan intValue];
[cell initData:(chanInt)];
[self getParameters:(setChan)];
} else {
NSInteger chanInt = indexPath.section;
NSNumber *chanNum = [NSNumber numberWithInt:chanInt]; // doesnt matter
[cell initData:(chanInt)]; // init
[self getParameters:(chanNum)]; // params
}
[self.mixMonitorView setChannelsStripToType:(cell)];
cell.clipsToBounds = YES;
return cell;
}
The func in question is initData which looks like this:
- (void)initData:(NSInteger)channel {
self.isBus = false;
self.isSendChan = false;
self.recallSafeFade = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"recallSafe"]];
self.recallSafeFade.y = 80;
[self.recallSafeFade setUserInteractionEnabled:YES];
self.recallSafeHead = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"recallSafeHeadamp"]];
[self.recallSafeHead setUserInteractionEnabled:NO];
self.recallSafePan = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"recallSafePan"]];
self.recallSafePan.y = 768 - 141;
[self.recallSafePan setUserInteractionEnabled:YES];
self.channelNumber = channel;
[self setClipsToBounds:NO];
_background = [[UIView alloc] initWithFrame:CGRectMake(0, 80, 122, 547)];
[_background setBackgroundColor:[UIColor colorWithPatternImage:[UIImage imageNamed:#"singleFader"]]];
[self addSubview:_background];
[self addChannelName];
[self addInput];
[self addPan];
[self addOn];
[self addMeter];
[self addFader];
}
So the issue im having is, i need to call this func to setup my custom cell to look and function how i need it to. However, every time i call reload on the collectionview due to state changes, it layers the new views over the top of the old views (due to reocurring imageview alloc's)
So how can i apply this func to each cell automatically, yet not reapply it over the top every time i reload the cell?
Image of the issue ina view debugger here:
UICollectionViewCells are reused. So, avoid doing addSubView: (directly or indirectly as in your case: collectionView:cellForRowAtIndexPath: is calling initData: which is calling addSubView:), each time in collectionView:cellForItemAtIndexPath:.
You are using a UICollectionView and UICollectionViewCell only by code.
So there is no XIB/Storyboard were is the cell, and no IBOutlet.
According to the doc of dequeueReusableCellWithReuseIdentifier:forIndexPath:, that's the part you fall into:
If you registered a class for the specified identifier and a new cell
must be created, this method initializes the cell by calling its
initWithFrame: method.
So, in CDCChannelStrip.m, override initWithFrame: and initialize its properties (that are "always" visible/used).
-(id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
_myImageViewProperty = [[UIImageView alloc] initWithFrame:someFrame];
[self addSubview:_myImageViewProperty];
etc.
}
return self;
}
-(void)updateWithChannel:(NSInteger)channel
{
self.channelNumber = channel;
self.channelLabelName = #"Something";
//etc.
}
You can also use prepareForReuse to "reinit" the cell as "virgin" (like it was just after initWithFrame: for instance.
-(void)prepareForReuse
{
[super prepareForReuse];
self.channelLabelName = #"";
self.backgroundColor = [UIColor clearColor];
//etc.
[self.sometimesThatImageIsShown setHidden:YES];
}
Simplest way is to create a separate xib for cell or prototype cell. Design you cell there and in cellForItemAtIndexPath you just manipulate your views and set values to your labels, images and other controllers.

Cell after scroll show image over image

I have tableview and I want to show all images from one folder inside directory and I have path to that directory. I am loading image to cell like:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
for (UIView * view in cell.contentView.subviews){
if ([view isKindOfClass:[UIButton class]]) {
[view removeFromSuperview];
}
}
cell.imageView.image = nil;
cell.accessoryType = UITableViewCellAccessoryNone;
NSData *imgData = [NSData dataWithContentsOfFile:absoluteImagePath];
UIImage *thumbNail = [[UIImage alloc] initWithData:imgData];
UIImageView *imageView = [[UIImageView alloc] initWithImage:thumbNail];
[cell insertSubview:imageView atIndex:0];
}
but when I scroll up and down I get image over image inside cell (old image is visible below new, when I scroll on for example first position I see image for index 0 below other image ).
Does anyone know what is a problem, I am new to this iOS programming ?
You should
subclass your cell,
create an outlet for the image, and
design the layout in storyboard.
The benefits:
No need to check for (cell==nil)
You will have to write less code
Cleaner solution without redundant view creation errors
When the cell is reused, [cell insertSubview:imageView atIndex:0]; adds an additional UIImageView to the cell. Suggestion is to create a custom UITableViewCell and in the prepareForReuse method, remove the UIImageView. An easy way to identify UIImageView to remove is to add a tag to it. E.g.,
imageView.tag = 1000;
Then you can find the image view with
UIImageView *imageView = [self.contentView viewWithTag:1000];
if (imageView) {
[imageView removeFromSuperview];
}
Declare the UIImageView where you are initializing the cell.
For example
if (cell==nil)
{
UIImageView *image = [[UIImageView alloc]initWithFrame:yourFrame];
[cell addSubView:image];
}
image.image = yourImage;
Alternatively, you can remove all subviews from the cell before adding new UIImageView as subview.

UICollectionView shows only the first item

I'm working on a project similar to a video album. In that I'm using UICollectionView to display the thumb images of those videos. The worst part is that I should not use storyboard or xib files. I have tried to do this programatically. I'm currently working on this code:
- (void)viewDidLoad
{
i = 0;
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
[layout setItemSize:CGSizeMake(self.view.frame.size.width/2.5,self.view.frame.size.width/2.5)];
collectionView = [[UICollectionView alloc] initWithFrame:self.view.frame collectionViewLayout:layout];
[collectionView setDataSource:self];
[collectionView setDelegate:self];
collectionView.backgroundColor = [UIColor whiteColor];
[collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:#"MyCell"];
[self.view addSubview:collectionView];
[super viewDidLoad];
}
I have given 1 for return in numberOfSectionsInCollectionView and [myArray count] for return in numberOfItemsInSection.
-(UICollectionViewCell *) collectionView:(UICollectionView *)cV cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [cV dequeueReusableCellWithReuseIdentifier:#"MyCell" forIndexPath:indexPath];
cell.backgroundColor = [UIColor blackColor];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(cell.frame.origin.x , cell.frame.origin.y, cell.frame.size.width, (cell.frame.size.height - cell.frame.size.height/3))];
cell.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(cell.frame.origin.x , cell.frame.origin.y, cell.frame.size.width, (cell.frame.size.height - cell.frame.size.height/3))];
imageView.image = [UIImage imageNamed:[storeData objectAtIndex:i]];
[cell addSubview:imageView];
i++;
return cell;
}
I have rechecked the images in myArray. When the view loads, the collection view shows only the first image. Other 4 cells are empty. What is wrong with my code?
For those who is experiencing this problem, frame is the key. I've encountered this and changed:
cell.imageView.frame = cell.frame;
into
cell.imageView.frame = cell.bounds;
The op is using:
cell.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(cell.frame.origin.x , cell.frame.origin.y, cell.frame.size.width, (cell.frame.size.height - cell.frame.size.height/3))];
That's why this happened.
You shouldn't be using i as a counter. The whole point of the delegate method sending you an indexPath is that it tells you what information to get from your array of source data. So, remove i and use the indexPath.row instead.
You also don't need 2 image views. But you should probably keep your special subview and not use the cells built in image view.
You do not need a counter. As indicated by Wain, use indexPath.row.
Importantly, you should not create new subviews in cellForItemAtIndexPath, but rather use this method to fill them appropriately with content. You could put the image views into your storyboard prototype cells and identify them with tags. Each cell returned from dequeueReusableCellWithReuseIdentifier will already contain the image view.
you should never create ui elements in cellForRowAtIndexPath:. Subclass a collection view cell like so:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
NSLog(#"INIT WITH FRAME FOR CELL");
//we create the UIImageView here
imageView = [[UIImageView alloc] init];
imageView.contentMode = UIViewContentModeScaleAspectFill;
imageView.frame = CGRectMake(cell.frame.origin.x , cell.frame.origin.y, cell.frame.size.width, (cell.frame.size.height - cell.frame.size.height/3));
[self.contentView addSubview:imageView]; //the only place we want to do this addSubview: is here!
}
return self;
}
Then add that subclassed cell as a property and alter this code:]
[collectionView registerClass:[customCellClass class] forCellWithReuseIdentifier:#"MyCell"];
The perform these changes:
-(customCellClass *) collectionView:(UICollectionView *)cV cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = (customCellClass *)[cV dequeueReusableCellWithReuseIdentifier:#"MyCell" forIndexPath:indexPath];
cell.backgroundColor = [UIColor blackColor];
imageView.image = [UIImage imageNamed:[storeData objectAtIndex:indexPath.row]];
return cell;
}
A final adjustment would be to move the the [super viewDidLoad] to this:
- (void)viewDidLoad
{
[super viewDidLoad];
//insert the rest of the code here rather than before viewDidLoad
}

UITableViewCell view after being dequeued from UITableView not empty

I have a strange problem using the dequeueReusableCellWithIdentifier: method of UITableView. Not sure if I don't understand the method well enough or is it just plain weird. Here goes:
Im using a UITableView which presents some data to users, and inside my
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath I use the dequeue method like so:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell"];
if (!cell)
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"cell"];
Afterwards I add some subviews to the contentView property of the cell. When scrolling a bit further down on my table I see those previously added subviews i.e. the cell is not empty but filled with "old" data. If I don't dequeue, and just alloc-init a new one each time, the cells are empty but I do see a bit more memory consumption which is precisely what Im trying to bring down a little. I'm using ARC if that means anything here.
What or how should I tackle the problem? I have tried running a for loop through the subviews of the content view and [view removeFromSuperview] which does remove the previous views and brings down memory consumption a little. But is that really necessary? Or is there a better way?
EDIT here is some more code how I add subviews
cell.backgroundView = [[UIView alloc] initWithFrame:cell.frame];
cell.backgroundColor = kClearColor; //defined to [UIColor clearColor]
cell.selectionStyle = UITableViewCellSelectionStyleNone;
if (indexPath.row == 0)
{
UIImageView *shine = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 320, 50)];
shine.image = [UIImage imageNamed:#"top_shine_1"];
[cell.backgroundView addSubview:shine]; //its a gradient thats why its added to background
UILabel *appLabel = [[UILabel alloc] initWithFrame:CGRectMake(55, winSize.height * 0.027, 250, 33)];
appLabel.backgroundColor = kClearColor; //defined to clear color
appLabel.textColor = kWhiteColor; //defined to white color
appLabel.text = [viewOrder objectAtIndex:tableView.tag]; //just an array from where I get the required text
appLabel.font = kStandardFontOfSize(30); //defined to a specific font
[cell.contentView addSubview:appLabel];
UIButton *settingsButton = [UIButton buttonWithType:UIButtonTypeCustom];
settingsButton.frame = CGRectMake(10, winSize.height * 0.0377, 31, 21);
[settingsButton setImage:[UIImage imageNamed:#"settings_button"] forState:UIControlStateNormal];
[settingsButton addTarget:self action:#selector(settings:) forControlEvents:UIControlEventTouchUpInside];
[cell.contentView addSubview:settingsButton];
return cell; //here I just return it since this is all the config the first cell needs
}
NSString *app = [viewOrder objectAtIndex:tableView.tag];
NSArray *boxes = [[plist secondObjectForKey:#"order" parent:app] componentsSeparatedByString:#";"];
//Add necessary shines or create the last logotype cell - just some details and stuff, all are just images
if (indexPath.row == 1)
{
UIImageView *shine = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 320, 282.5)];
shine.image = [UIImage imageNamed:#"top_shine_2"];
[cell.backgroundView addSubview:shine];
}
else if (indexPath.row == 2)
{
UIImageView *shine = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, winSize.width, 150)];
shine.image = [UIImage imageNamed:#"main_shine"];
[cell.backgroundView addSubview:shine];
}
else if (indexPath.row == boxes.count + 1)
{
UIImageView *logo = [[UIImageView alloc] initWithFrame:CGRectMake(111.5, 25, 97, 20)];
logo.image = [UIImage imageNamed:#"cell_logo"];
[cell.backgroundView addSubview:logo];
return cell;
}
NSString *databox = [boxes objectAtIndex:indexPath.row - 1];
UIView *view; //Main subview to be added to the cell
/*
here I have a class that creates a view with a bunch of subviews added to that view, the view is then assigned to 'view'; kinda like
view = [someAssembler assembleViewWith:options.....]. all are basically UILabels or ImageViews added to the main view
*/
[cell.contentView addSubview:view]; //and here this 'main view' is added as a subview, this view is still visible after the cell has been dequeued and the shines are as well
return cell;
Before you start criticising why im not using a single UIColor for background and text color let me remind you that this is still in testing stage, it will be taken care of later.
[cell.backgroundView addSubview:shine]; these lines of code are the problem in your case.
You should create a complete reusable cell within the if (!cell) block and repopulate them each time cellForRow is being called. For every unique cell a unique reuse identifier should be used. For example, if you have multiple cells with differently laid out subviews, you should use different identifiers for them.
In your specific example cells must be created in the if (indexPath.row == 1) blocks.
static NSString *cellIdentifier = #"cell";
UITableViewCell *cell = nil;
if (indexPath.row == 0) {
cellIdentifier = #"topCell";
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
if (!cell) {
// create the cell and add the necessary subviews for indexPath row 0
}
return cell;
}
else if (indexPath.row == 1) {
}
//etc.
}
You'll have to create the "main subview" for each cell in the !cell block with this approach though, so you should probably look into subclassing a cell.

UIImageView added to cell contentView displaying randomly when scrolling through UITableView

My UITableView is pulling data from an array. Items in this array have a property called IsSelected. I am trying to put a UIImageView in the cell contentView for each item that is selected.
The UITableView however when reusing the cell is causing my image to be reused on cells that it shouldn't. I cannot figure out for the life of me how I should be doing it differently. I have attached a screen shot showing the issue. If I keep scrolling up and down the image goes all over the place:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *SchoolCellIdentifer = #"SchoolCellIdentifier";
SchoolInfoItem *item = [self.schoolsArray objectAtIndex:indexPath.row];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SchoolCellIdentifer];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:SchoolCellIdentifer];
cell.contentView.backgroundColor = [BVColors WebDarkBlue];
}
cell.textLabel.text = item.Name;
if ([item.selected isEqualToString:#"1"])
{
cell.contentView.backgroundColor = [BVColors WebBlue];
UIImageView *selectedItemCheckMarkIcon = [[UIImageView alloc] initWithFrame:CGRectMake(300, 13, 17, 17.5)];
[selectedItemCheckMarkIcon setImage:[UIImage imageNamed:#"check-mark.png"]];
[cell.contentView addSubview:selectedItemCheckMarkIcon];
}
else
{
cell.contentView.backgroundColor = [BVColors WebDarkBlue];
}
return cell;
}
You need to make sure the UIImageView is getting removed from the cell content view. It looks like in your code when a cell gets dequeued that imageview is still in the cells view hierarchy.
Best solution is to have your cell hold onto a reference to the image view and remove it when necessary.
Take the following:
if ([item.selected isEqualToString:#"1"])
{
cell.contentView.backgroundColor = [BVColors WebBlue];
cell.myImageView = [[UIImageView alloc] initWithFrame:CGRectMake(300, 13, 17, 17.5)];
[selectedItemCheckMarkIcon setImage:[UIImage imageNamed:#"check-mark.png"]];
[cell.contentView addSubview:cell.myImageView];
}
else
{
[cell.myImageView removeFromSuperview];
cell.myImageView = nil;
cell.contentView.backgroundColor = [BVColors WebDarkBlue];
}
Note in the else case the removal of the imageview.
You keep adding a the UIImageView as a subview on the cell's contentView. This isn't removed when the table view is reusing the cell. You'll need to remove the subview if it shouldn't appear.
You should make selectedItemCheckMarkIcon a property on your UITableViewCell subclass. And then have a method in your subclass where you set the image or visibility of the image accordingly.
You could also use the accessoryView property on the UITableView and set the imageView as that:
if ([item.selected isEqualToString:#"1"]) {
cell.contentView.backgroundColor = [BVColors WebBlue];
UIImageView *selectedItemCheckMarkIcon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"check-mark.png"]];
cell.accessoryView = selectedItemCheckMarkIcon;
} else {
cell.accessoryView = nil;
cell.contentView.backgroundColor = [BVColors WebDarkBlue];
}
Note that you don't need to set a frame in this case because the system will automatically set the frame correctly for the accessoryView.

Resources