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
.)
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
vs. using NSLayoutConstraints.
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
How to Get Section of UItableview from Inside a Child UIcollectionview
Msmessagelivelayout Freeze/Crash in Transcript When Info.Plist Contains Privacy Request
Realm Data Insertion Took Over 7Mins for Big Size JSON
Firebase - Swift 4: Message Sent But Not Received
How to Get Data to Return from Nsurlsessiondatatask in Swift
Hit Fatal Error: Unexpectedly Found Nil While Unwrapping an Optional Value (Lldb)
Cocos2D Fcm Push Notification Not Working
How to Get User Nearby My Location in Geofire,Firebase
Animation Triggered Using a Button Stops a Repeatforever Animation Added Onappear
Swiftui Navigationview Starting Inside Itself
Macos Remote Push Notifications Not Showing Alert Banner
Spacing Between Sections in a Form
How to Create a UIprintpaper to Test UIprintinteractioncontrollerdelegate
Swift: How to Update UI When Listening to a Stream