I am trying to create multiple collectionViewCells with 4 different types. And every cell has a different view of one of those 4 types. Every view of those types can have different contents based on user selection.
The problem I am having is the fact that some of the cards are overlapping/not loading correctly when multiple views/cells of the same type are on the screen.
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
Card *card = [[[usermanager getSelectedUser] getCards] objectAtIndex:indexPath.item];
NSLog(#"CARD LOADING: %#", card.title);
[card setupLayout];
UICollectionViewCell *cell;
if(card.type.intValue == 1){
cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"lifestyleCell" forIndexPath:indexPath];
}else if(card.type.intValue == 2){
cell= [collectionView dequeueReusableCellWithReuseIdentifier:#"sceneCell" forIndexPath:indexPath];
}else if(card.type.intValue == 3){
cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"energyCell" forIndexPath:indexPath];
}else if(card.type.intValue == 4){
cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"productCell" forIndexPath:indexPath];
}else{
cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cardCell" forIndexPath:indexPath];
}
[cell addSubview:card];
//Add dropshadow
cell.contentView.layer.borderWidth = 1.0f;
cell.contentView.layer.borderColor = [UIColor clearColor].CGColor;
cell.contentView.layer.masksToBounds = YES;
cell.layer.shadowColor = [UIColor blackColor].CGColor;
cell.layer.shadowOffset = CGSizeMake(0, 5.0f);
cell.layer.shadowRadius = 2.0f;
cell.layer.shadowOpacity = 0.5f;
cell.layer.masksToBounds = NO;
return cell;
}
The card is the view I add to the cell. As mentioned above there are multiple types of those cards.
try using:
cell.clipsToBounds = YES;
As you scroll a UICollectionView, cells that disappear offscreen are re-used for the new cells coming on-screen. That means if you add subviews in your collectionView:cellForItemAtIndexPath: method, they will still be part of the cell's view hierarchy when that cell is re-used. Every time the cell is re-used, it will add a new subview when you call [cell addSubview:card]. Your card subviews will simply stack on top of each other.
It seems that you're using a collection of Card objects, custom UIView subclasses, to store each user's deck of cards. I would suggest instead that you separate out the model from the view - store each card as a simple data model which represents the card independently of how it is displayed (see MVC). Then you can create a custom UICollectionViewCell subclass which can display any card. In your collectionView:cellForItemAtIndexPath: you would simply reconfigure the cell's view according to the corresponding card data. That way you do not need to call addSubview: within your collectionView:cellForItemAtIndexPath: method.
Related
I am creating a collectionView whose cells are of different sizes and have different content. I am using a cell prototype for these cells, however, when I am adding more than one cell I get weird UI bugs:
This is what it is supposed to look like
This is what it actually looks like
Code:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
Card *card = [[[usermanager getSelectedUser] getCards] objectAtIndex:indexPath.item];
[card setupLayout];
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
if(cell == nil){
cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cardCell" forIndexPath:indexPath];
}
[cell addSubview:card];
cell.clipsToBounds = YES;
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
cell.layer.shadowPath = [[UIBezierPath bezierPathWithRect:cell.bounds] CGPath];
//Add dropshadow
cell.contentView.layer.borderWidth = 1.0f;
cell.contentView.layer.borderColor = [UIColor clearColor].CGColor;
cell.contentView.layer.masksToBounds = YES;
cell.layer.shadowColor = [UIColor blackColor].CGColor;
cell.layer.shadowOffset = CGSizeMake(0, 5.0f);
cell.layer.shadowRadius = 2.0f;
cell.layer.shadowOpacity = 0.5f;
cell.layer.masksToBounds = NO;
cell.layer.borderColor = [UIColor yellowColor].CGColor;
cell.layer.borderWidth = 2.0f;
return cell;
}
It propably has something to do with the fact that I use the reusable cell. Because when I create 2 different prototypes in my storyboard for these cells they have no problems at all. Can anyone help me? Thanks
as you say: your cells will be reused, so if you change any layout or frame or colour these properties will be like you set when the cell will be used the next time. you should subclass UICollectionViewCell and implement the method prepareForReuse where you have to reset all views and properties of the cell to the original values and you have to remove the subview card:
-(void)prepareForReuse {
[super prepareForReuse];
// Reset all, for example backgroundView
self.backgroundView = nil;
}
one more point: why you call UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath]; Thats not correct. You need only UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cardCell" forIndexPath:indexPath];
I've got a UICollectionViewController which displays blocks of telephone numbers (see image). When the view loads they all appear fine however when i either begin scrolling, changing rotation, or execute a search function which alters the (mutable) array in which the data is sourced, i see these malformed labels. I did think it might be the iOS simulator however from looking at it, it appears to be an issue with the positioning of UICollectionViewCells.
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
static NSString *identifier = #"cell";
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
cell.backgroundColor = [UIColor grayColor];
cell.layer.cornerRadius = 5;
[cell setClipsToBounds: YES];
CGRect cellBound = CGRectMake(25, 12.5, 150, 12.5); // x, y, w, h
UILabel *title = [[UILabel alloc] initWithFrame:cellBound];
NSString *number = [[searchNumbers objectAtIndex:indexPath.row] valueForKey:#"number"];
number = [number stringByReplacingOccurrencesOfString:#"+44" withString: #"0"];
title.text = number;
[cell addSubview:title];
return cell;
}
It should be noted that i am using UICollectionViewFlowLayout
As #Woodstock mentioned, this is due to "over-adding" UILabel objects to your cell.
Rather than his solution, which still adds the UILabel to the cell in -collectionView:cellForRowAtIndexPath:, the better MVC solution is this:
// A UICollectionViewCell subclass
// Make sure to pick the correct "init" function for your use case
- (instancetype)init... {
self = [super init...];
if (self != nil) {
[self setupCell];
}
return self;
}
- (void)setupCell {
self.backgroundColor = [UIColor grayColor];
self.clipsToBounds = YES;
self.layer.cornerRadius = 5;
CGRect cellBound = CGRectMake(25, 12.5, 150, 12.5); // x, y, w, h
// Assumes you've set up a UILabel property
self.titleLabel = [[UILabel alloc] initWithFrame:cellBound];
[cell addSubview:self.titleLabel];
}
- (void)configureWithNumber:(NSString *)number {
number = [number stringByReplacingOccurrencesOfString:#"+44" withString: #"0"];
self.titleLabel.text = number;
}
// In your UICollectionViewDataSource/Delegate implementation
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
static NSString *identifier = #"cell";
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
NSString *number = [[searchNumbers objectAtIndex:indexPath.row] valueForKey:#"number"];
[cell configureWithNumber:number];
return cell;
}
Basically, you want to set up and add views only when setting up the cell initially. After that, you should pass in a data value/object and configure the cell. If you have cells that need different controls (2 labels vs. 1, etc.), then make multiple subclasses. This way, you encapsulate your classes for cleaner code and better reuse.
I believe this is happening because you are adding more and more UILabel subviews to your cell (over and over again as cellForItemAtIndexPath is called). You need to add a check and only add a label subview if the cell doesn't already have one. The dequeued cells already have the label subview if they're being reused, if this label already exists you simply need to set it's text from your datasource.
Pseudocode:
for subview in subviews {
if subview.isKindOfClass(UILabel) {
// assign the new text label.
}
else
{
// create and add the UILabel subView.
}
}
This is an easy mistake to make as dequeueReusableCellWithReuseIdentifier can either give you a previously used cell OR as you've seen give you a fresh one initially. Which is why the app works correctly when you start, but gets messy as you scroll.
I am using a CollectionView to display an image in. I am using horizontal scrolling with pagination enabled so the user is able to scroll from image to image right to left as they would on the apps screen on iOS. How might I be able to call didSelectItemAtIndexPath to detect which cell the collection view has stopped on without the user having to tap on the cell?
Though it is not relevant:
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
titleLabel.alpha = 1.0;
mainTextLabel.alpha = 1.0;
availableLabel.alpha = 1.0;
sizeLabel.alpha = 0.0;
sizeImage.alpha = 0.0;
moreInfoOnSize.alpha = 0.0;
UICollectionViewCell *datasetCell =[collectionView cellForItemAtIndexPath:indexPath];
datasetCell.backgroundColor = [UIColor lightGrayColor]; // highlight selection
PFObject *selectedObject = [labelFileArray objectAtIndex:indexPath.row];
titleLabel.text = selectedObject[#"labelText"];
PFObject *selectedObject2 = [mainTextArray objectAtIndex:indexPath.row];
mainTextLabel.text = selectedObject[#"mainTextLabel"];
PFObject *selectedObject3 = [availableLabelArray objectAtIndex:indexPath.row];
availableLabel.text = selectedObject[#"availableLabel"];
if ([availableLabel.text isEqual:#"Available"]) {
availableLabel.backgroundColor = [UIColor greenColor];
} else if ([availableLabel.text isEqual:#"Sorry We Are Out!"])
{
availableLabel.backgroundColor = [UIColor redColor];
}
NSLog(#"%#", indexPath);
}
And Image
http://cl.ly/image/3x1R41073W2z
http://cl.ly/image/203Q2E3T2X44
Thank you so much!
You can call visibleCells on your collection view to get the cells currently visible on the screen like this:
// All visible cells (1 or more depending on your layout and cell sizes)
NSArray *visibleCells = [myCollectionView visibleCells];
// Get the first cell from the array
UICollectionViewCell *firstCell = visibleCells.firstObject;
I am using collectionView in my App. I am setting image for the cell backgroundView in didSelect delegate. But When i select one cell indexPath the image is getting set for 3 cell indexPath. When i scroll the collectionView the images are getting changed randomly? Please Help me. thanks in advance.
- (void)viewDidLoad
{
[super viewDidLoad];
[collection registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:uio];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection: (NSInteger)section
{
return 50;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collection dequeueReusableCellWithReuseIdentifier:uio
forIndexPath:indexPath];
cell.backgroundColor = [UIColor whiteColor];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"index %#",indexPath);
UICollectionViewCell *cell = [collection cellForItemAtIndexPath:indexPath];
cell.backgroundView =[[UIImageView alloc]initWithImage:[UIImage imageNamed:#"download.jpg"]];
}
That's because you reuse your cell. An option would be to have an dictionary variable to say that your cell has been selected and reset the image if it has not been.
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"index %#",indexPath);
UICollectionViewCell *cell = [collection cellForItemAtIndexPath:indexPath];
cell.backgroundView =[[UIImageView alloc]initWithImage:[UIImage imageNamed:#"download.jpg"]];
[selectedDictionary setObject:[NSNumber numberWithBool:YES] forKey:[NSNumber numberWithInteger:indexPath.row]];
}
Then in your cellForItemAtIndexPath method you would check that value
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collection dequeueReusableCellWithReuseIdentifier:uio
forIndexPath:indexPath];
BOOL selected = [[selectedDictionary objectForKey:[NSNumber numberWithInteger:indexPath.row]] boolValue];
if(selected){
cell.backgroundView =[[UIImageView alloc]initWithImage:[UIImage imageNamed:#"download.jpg"]];
}else{
cell.backgroundView = nil;
}
cell.backgroundColor = [UIColor whiteColor];
return cell;
}
Of course if you use some kind of object as model, it would appropriate to have a selected variable in here, you won't need a nsdictionary any more.
The Problem is dequeueReusableCellWithReuseIdentifier.
When you scroll UICollectionview then cell are reused that is problem
add Collectionview inside scrollview.
Try this Inside:
Scroll_View is Your Scroll View
collection is Your Collectionview
-(UICollectionViewCell*)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
self.Scroll_View.contentSize = CGSizeMake(self.view.frame.size.width, collectionView.contentSize.height);
CGRect fram_For_Collection_View = self.collection_view.frame;
fram_For_Collection_View.size.height = collectionView.contentSize.height;
self.collection.view.frame = fram_For_Collection_View;
}
Your -collectionView:didSelectItemAtPath: is adding a new image view to the cell. Nothing is removing that image view when the cell is reused. So, when you say:
UICollectionViewCell *cell = [collection dequeueReusableCellWithReuseIdentifier:uio
forIndexPath:indexPath];
in your -collectionView:cellForItemAtIndexPath:, you're may get back some cell that already has one or more image views.
My suggestion would be to add the image view to the cell in the cell prototype, perhaps in your storyboard or in the cell's initializer. Have your -collectionView:cellForItemAtIndexPath: set the image for that image view to the correct image for the given path.
What's happening is that UICollectionView reuses cells. So in didSelectItemAtIndexPath: you set the cell background, but then the UICollectionView reuses that same cell as needed (and you're not resetting the cell.backgroundView in cellForItemAtIndexPath:).
The way to fix this is to maintain an NSIndexSet of selected cells. In didSelectItemAtIndexPath: you can add the index of the item that was selected, and then force a reload of that item by calling reloadItemsAtIndexPaths. Then, in your cellForItemAtIndexPath: check the index set to see if the selected index is included, and if so, set the backgroundView of the cell.
I had the same issue few days ago & I posted a question here. Here is the answer I got & it works for me.
Collection View Cell multiple item select Error
And also if you are using a custom cell you can add this code to the init method of that cell & it will work too.
CGFloat borderWidth = 6.0f;
UIView *bgView = [[UIView alloc] initWithFrame:frame];
bgView.layer.borderColor = [UIColor redColor].CGColor;
bgView.layer.borderWidth = borderWidth;
self.selectedBackgroundView = bgView;
I have an UICollectionView and Custom UICollectionViewCell, where i'm loading images, when i scroll the UICollectionView, i'm seeing all the cells are refreshing, here is the code for UICollectionView delegates,
In ViewDidLoad adding this first for adding CustomCell
-(void)ViewdidLoad{
UINib *nib = [UINib nibWithNibName:#"NMCFAIPadWishListCell" bundle:nil];
[self.accountDetailsCollectionView registerNib:nib forCellWithReuseIdentifier:#"Cell"];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return [[self wishListData] count];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
static NSString *identifier = #"Cell";
NMCFAIPadWishListCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
[cell setWishList:[[self wishListData] objectAtIndex:[indexPath row]] delegate:self];
return cell;
}
In setWishList method just assigning the values from the array to label and i have a button in Xib for each cell in my custom UICollectionViewCell, when user taps on that button i'm just changing the label BG color
- (void)setWishList:(NSString*)product delegate:(id)delegate
{
self.label.text = product;
}
Below is the button action
- (IBAction)editProduct:(id)sender
{
self.label.backgroundColor = [UIColor redColor];
}
Here my problem is when i scroll the Custom Cell and tap on Button in any Cell the label BG is not only changing in current cell but also in MANY CELLS.
You should not attempt to store any state in/on your cells as the cell objects themselves are reused at the discretion of the UICollectionView.
One solution to your problem could be:
In your editProduct: method (assuming your editProduct: method is in your custom UICollectionViewCell implementation), inform the collection view’s controller that the user has “selected” that product via a protocol method (or block or some other messaging mechanism).
In your view controller, when receiving the above message, identify the index of the cell for which the button has been tapped (indexPathForCell: might be useful here) and store the fact that the item at index n has been selected. An NSArray might be useful here.
In the same method, force a reload of the cell that has been tapped with reloadItemsAtIndexPaths: or a similar method. This will force the collectionView:cellForRowAtIndexPath: method to be called.
Implement something like the following in your collectionView:cellForRowAtIndexPath: method:
BOOL itemSelected = ((NSNumber *)isProductSelectedArray[indexPath.row]).boolValue; // You can't store `BOOL`s directly into NSArrays. So I've assumed an NSNumber here.
cell.backgroundColor = itemSelected ? [UIColor redColor] : [UIColor clearColor] // Or some other color to indicate non-selection.
As an aside, if you declare “ViewdidLoad” instead of “viewDidLoad”, you might find your code doesn’t behave the way you intend. Don’t forget to call [super viewDidLoad] somewhere in your implementation too.
Finally, I recommend getting a better handle on the concept of cell reuse by reading the “Collection View Basics” chapter of Apple’s “Collection View Programming Guide for iOS” - specifically the section titled “Reusable Views Improve Performance”.
Cells do not maintain a state. An array of objects that correspond to the cells should main the state since cells are recycled very often. For instance, inside you cellForItemAtIndexPath:
....
BOOL isWishListSet = self.isWishListSetArray[indexPath.row];
UIColor *cellColor = [UIColor redColor];
if (isWishListSet) {
cellColor = [UIColor blackColor];
}
cell.backgroundColor = cellColor;
....
EDIT 1:
As gavdotnet mentions in his answer, cell states should be held in a parallel array, not in the cell itself. So you would have one array that holds the data you want to show and another that holds the state of whether the cell has been selected to be on the wishlist:
#interface WishListViewController ()
#property (nonatomic, strong) NSArray *wishListData;
#property (nonatomic, strong) NSMutableArray *wishListStatus;
#end
#implementation WishListViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Initialize arrays
self.wishListData = [NSArray array];
self.wishListStatus = [NSMutableArray array];
}
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.wishListData.count;
}
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
NSNumber *isWishListSet = self.wishListStatus[indexPath.row];
UIColor *cellColor = [UIColor redColor];
if (isWishListSet.boolValue) {
cellColor = [UIColor blackColor];
}
cell.backgroundColor = cellColor;
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
NSNumber *isWishListSet = self.wishListStatus[indexPath.row];
if (isWishListSet.boolValue) {
isWishListSet = [NSNumber numberWithBool:NO];
} else {
isWishListSet = [NSNumber numberWithBool:YES];
}
[self.wishListStatus replaceObjectAtIndex:indexPath.row withObject:isWishListSet];
UIColor *cellColor = [UIColor redColor];
if (isWishListSet.boolValue) {
cellColor = [UIColor blackColor];
}
cell.backgroundColor = cellColor;
}
The section
UIColor *cellColor = [UIColor redColor];
if (isWishListSet.boolValue) {
cellColor = [UIColor blackColor];
}
cell.backgroundColor = cellColor;
is repeated, so it should be in its own method, but that is up to you decide really. The example shows your data array, which populates the cells, and your wishListStatus array which holds the status of the cell. If we were not going to dequeue cells, this would not be an issue. But since we are in this case, the status must be maintained outside of the cell.
The line you are using:
[cell setWishList:[[self wishListData] objectAtIndex:[indexPath row]] delegate:self];
should be changed to something like:
[cell setDelegate:self];
since the delegate is never toggled and is always set to 'self'.
Cells are being reused because of that they are refreshing.
CollectionView reuses cells so the multiple change of background color is the correct behavior.
To fix your problem, customize your UICollectionViewCell(NMCFAIPadWishListCell) instance as follows:
UIView *backgroundView = [[UIView alloc] initWithFrame:self.bounds];
backgroundView.backgroundColor = [UIColor clearColor];
self.backgroundView = backgroundView;
UIView *selectedBGView = [[UIView alloc] initWithFrame:self.bounds];
selectedBGView.backgroundColor = [UIColor redColor];
self.selectedBackgroundView = selectedBGView;
Use the delegate method for extra selection behavior:
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
// Instead of button actions, use this delegate method
}
Check out UICollectionViewCell Reference for more details. UICollectionViewCell has three properties backgroundView, selectedBackgroundView and selected which are sufficient for your needs.