Why Are Image Views Sometimes Not Appearing in Collection View Cells

Why are image views sometimes not appearing in collection view cells?

Since Xcode 8 you have to call layoutIfNeeded() to calculate size (in your case you need to know cell.imageCell.frame.height) and position from auto layout rules or use a fixed value of cornerRadius.

cell.imageCell.layoutIfNeeded()
cell.imageCell.layer.masksToBounds = true
cell.imageCell.layer.cornerRadius = cell.imageCell.frame.height / 2

OR

cell.imageCell.layer.masksToBounds = true
cell.imageCell.layer.cornerRadius = 5

Collection View Cells not appearing

Did you set the CollectionViewController to the storyboard identity inspector? :)

And I would try to call the reloadData() after you change the data in the viewDidLoad method.

Hope that helps

UICollectionView displays wrong images in cells

Both UICollectionViewCell and UITableViewCell are reused. As one scrolls off the top of the screen, it is reinserted below the visible cells as the next cell that will appear on screen. The cells retain any data that they have during this dequeuing/requeuing process. prepareForReuse exists to give you a point to reset the view to default values and to clear any data from the last time it was displayed. This is especially important when working with asynchronous processes, such as network calls, as they can outlive the amount of time that a cell is displayed. Additionally, you're doing a lot of non-setup work in awakeFromNib. This method is not called every time a cell is displayed, it is only called the FIRST time a cell is displayed. If that cell goes off screen and is reused, awakeFromNib is not called. This is likely a big reason that your collection views have the wrong data, they're never making their network request when they appear on screen.

EcardsCategoriesTableViewCell:

prepareForReuse should be implemented. A few things need to occur in this method:

  • theseEcards should be nilled. When a table view scrolls off screen, you want to get rid of the collection view data or else the next time that cell is displayed, it will show the collection view data potentially for the wrong cell.
  • You should keep a reference to the dataTask that runs in awakeFromNib and then call cancel on this dataTask in prepareForReuse. Without doing this, the cell can display, disappear, then get reused before the dataTask completes. If that is the case, it may replace the intended values with the values from the previous dataTask (the one that was supposed to run on the cell that was scrolled off screen).

Additionally, the network call needs to be moved out of awakeFromNib:

  • You are only ever making the network call in awakeFromNib. This method only gets called the first time a cell is created. When you reuse a cell, it is not called. This method should be used to do any additional setup of views from the nib, but is not your main entry point in adding data to a cell. I would add a method on your cell that lets you set the category id. This will make the network request. It will look something like this:

    func setCategoryId(_ categoryId: String) {
    DispatchQueue.main.async {
    let jsonUrlString = "https://**********/*******/content?category=\(categoryId)"
    guard let url = URL(string: jsonUrlString) else { return }
    URLSession.shared.dataTask(with: url) { (data, response, err) in
    guard let data = data else { return }

    if err == nil {
    do {
    let decoder = JSONDecoder()
    let ecards = try decoder.decode(Ecards.self, from: data)
    self.theseEcards = ecards.content
    self.theseEcards = self.theseEcards.filter{$0.isActive}

    } catch let err {
    print("Err", err)
    }

    DispatchQueue.main.async {
    self.thisEcardCollection.reloadData()
    }
    }
    }.resume()
    }
    }

This will be called in the cellForRowAt dataSource method in EcardsViewController.

EcardCategoriesCollectionViewCell:

This cell has similar issues. You are setting images asynchronously, but are not clearing the images and cancelling the network requests when the cell is going to be reused. prepareForReuse should be implemented and the following should occur within it:

  • The image on the image view should be cleared or set to a default image.
  • The image request should be cancelled. This is going to take some refactoring to accomplish. You need to hold a reference to the dataTask in the collection view cell so that you can cancel it when appropriate.

After implementing these changes in the cells, you'll likely notice that the tableview and collection view feel slow. Data isn't instantly available. You'll want to cache the data or preload it some way. That is a bigger discussion than is right for this thread, but it will be your next step.

UICollectionView Cells Not Showing

Did you set the collection view's datasource and delegate? Your view controller doesn't seem to implement those protocols.

class Avatar: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {

// Collection View Within View Controller
@IBOutlet weak var CollectionView: UICollectionView!

// Avatar Images

var AvatarImages = ["Avacado", "Bear", "Cow", "Carrot", "Dolphin", "Dinosaur", "Elephant", "Flamingo", "Fox", "Hippo", "Jellyfish", "Moose", "Monkey", "Octopus", "Pig", "Panda", "Parrot", "Pumpkin", "Popcorn", "Penguin", "Platypus", "Sheep", "Sloth", "Shark", "Wolf"]

override func viewDidLoad() {
CollectionView.dataSource = self
CollectionView.delegate = self
// other setup stuff if you need it...
}

// ...
}

You can also do this from the storyboard by Ctrl-dragging from the collection view to the view controller. (You still have to declare the UICollectionViewDelegate and UICollectionViewDataSource protocols after your class name/UIViewController.)

Sample Image
Sample Image

Edit based on full code

The following compiles for me with no errors, and I think is at least the gist of what you want. (I did not uncomment anything except what was relevant to this answer, i.e. anything outside of viewDidLoad.)

import UIKit
import CoreData

//Things in the collection cell
class AvatarCollectiveCell: UICollectionViewCell {
@IBOutlet weak var AvatarImage: UIImageView!
}

class Avatar: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {

// Collection View Within View Controller
@IBOutlet weak var CollectionView: UICollectionView!

// Avatar Images

var AvatarImages = ["Avacado", "Bear", "Cow", "Carrot", "Dolphin", "Dinosaur", "Elephant", "Flamingo", "Fox", "Hippo", "Jellyfish", "Moose", "Monkey", "Octopus", "Pig", "Panda", "Parrot", "Pumpkin", "Popcorn", "Penguin", "Platypus", "Sheep", "Sloth", "Shark", "Wolf"]

// Create Cells
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return AvatarImages.count //Number of Images
}

// make a cell for each cell index path
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

// get a reference to our storyboard cell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "AvatarCell", for: indexPath as IndexPath) as! AvatarCollectiveCell
let Avatars = AvatarImages[indexPath.item]

// Use the outlet in our custom class to get a reference to the UILabel in the cell
//cell.BookCover.image = UIImage(named: arrayBookImages[indexPath.item])
cell.AvatarImage.image = UIImage(named: AvatarImages[indexPath.item-1])
cell.backgroundColor = UIColor.white // make cell more visible in our example project

return cell
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

// USERNAME DISPLAY BEGINS
/*
//reload the data for the collection view
//NameDisplay.reloadData()

guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}

//getting the managed context where the entity we need is
let managedContext = appDelegate.persistentContainer.viewContext

//make fetch request
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "UserInfo", attributeName: "name")

//try to fetch the entity we need, else print error
do {
Username = try managedContext.fetch(fetchRequest)
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}*/
//USERNAME DISPLAY ENDS

}

override func viewDidLoad() {
super.viewDidLoad()
CollectionView.dataSource = self
CollectionView.delegate = self

// Do any additional setup after loading the view.
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

var Username:String = ""
@IBOutlet weak var NameDisplay: UILabel!
/*
// MARK: - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}

UICollectionView not showing cells properly

One major issue here is that you are adding a UIImageView as a subview in cellForItemAtIndexPath. This method runs every time a cell is dequeued, not every time a cell is initialized. This means that every time one of those cells is rendered like when the collection scrolls, a new subview is being added on top of the existing image view on the cell. This will very quickly cause major problems graphically and use way more memory then you intend to. The first thing I would do is subclass UICollectionViewCell and add your UIImageView to the subclass. You can then set the cell's image in that method but adding a subview in that particular spot is a really bad idea.

Here's my approach.

First, make a cell subclass and expose a method to set your image.

// MyCollectionViewCell.h

#import <UIKit/UIKit.h>

@interface MyCollectionViewCell : UICollectionViewCell

- (void)setImage:(UIImage *)image;

@end

// MyCollectionViewCell.m

#import "MyCollectionViewCell.h"

@interface MyCollectionViewCell ()

@property (strong, nonatomic) UIImageView *imageView;

@end

@implementation MyCollectionViewCell

-(instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setupImageView];
}
return self;
}

- (void)setupImageView
{
UIImageView *imageView = [[UIImageView alloc]init];
imageView.backgroundColor = [UIColor greenColor];
[self addSubview:imageView];
self.imageView = imageView;

[imageView setTranslatesAutoresizingMaskIntoConstraints:NO];
NSLayoutConstraint *leading = [NSLayoutConstraint constraintWithItem:self.imageView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0];
NSLayoutConstraint *trailing = [NSLayoutConstraint constraintWithItem:self.imageView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:0];
NSLayoutConstraint *top = [NSLayoutConstraint constraintWithItem:self.imageView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];
NSLayoutConstraint *bottom = [NSLayoutConstraint constraintWithItem:self.imageView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0];

[self addConstraints:@[leading, trailing, top, bottom]];

}

- (void)setImage:(UIImage *)image
{
[self.imageView setImage:image];
}

@end

The layout constraints here are key. UICollectionViewCells, in my experience often have a frame of 0 when they are initialized which is why you are seeing a ton of empty boxes (the UIImageView on a new cell is being initialized with CGRectZero). The constraints will make sure that when they do eventually lay out correctly, the imageView will also resize itself to fit.

After this, register your custom cell class and then you can implement this in your CollectionViewController

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MyCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
[cell setImage:[UIImage imageNamed:@"emoji.jpg"]];
// Configure the cell

return cell;
}

Here's the before and after shots of setting the UIImageViewFrame explicitly

layout with frame on initWithFrame:

vs. using NSLayoutConstraints.

layout with constraints

For the hell of it, here's how to do the same thing in cellForItemAtIndexPath.

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];

NSInteger viewTag = 1000;
UIImageView *imageView = (UIImageView *)[cell viewWithTag:viewTag];

if (!imageView) {
imageView = [[UIImageView alloc]init];
imageView.tag = viewTag;
[cell addSubview:imageView];

[imageView setTranslatesAutoresizingMaskIntoConstraints:NO];
NSLayoutConstraint *leading = [NSLayoutConstraint constraintWithItem:imageView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0];
NSLayoutConstraint *trailing = [NSLayoutConstraint constraintWithItem:imageView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:0];
NSLayoutConstraint *top = [NSLayoutConstraint constraintWithItem:imageView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];
NSLayoutConstraint *bottom = [NSLayoutConstraint constraintWithItem:imageView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0];
[cell addConstraints:@[leading, trailing, top, bottom]];
}

imageView.image = [UIImage imageNamed:@"emoji.jpg"];

return cell;
}

Asynchronous loading of image in UICollectionViewCell causes labels to disappear

Judging by your screenshots, it seems like UILabels are getting their heights compressed to 0 by expanded UIImageViews. Try increasing UILabel's vertical content compression resistance priority, say, to 1000, so that it becomes non-compressible (and, in turn, makes auto layout engine compress either UIImageView or spacings - depends on your constraints).

UICollectionView's cell disappearing

So, what did work?

1) Subclass UICollectionViewFlowLayout.

2) Set the flowLayout of my UICollectionView to my new subclass.

3) On the init method of the UICollectionViewFlowLayout subclass, set the orientation you want:

self.scrollDirection = UICollectionViewScrollDirectionHorizontal;

In my case it is Horizontal.

4) The important part:

-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}

At this moment, I should theorise a bit, but honestly I don't have a clue.

Why aren't my labels displaying in my collection view cells?

You have:

class DiceLabel: UILabel {
let diceLabel: UILabel = {
let label = UILabel()
// ... configure `label` here ...
return label
}()
}

That's pointless and nonsense. You are giving your label another label as a property, and it does nothing for the display of your label, the DiceLabel. Hence your use of DiceLabel is all wrong, because it's just a plain UILabel with no properties set. None of your configurations have any effect. You mean this:

class DiceLabel: UILabel {
override init(frame: CGRect) {
super.init(frame: frame)
// ... configure `self` here ...
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}


Related Topics



Leave a reply



Submit