iOS swift UICollectionView loses cell constraints after scrolling past first screen full - ios

I'm trying to learn how to deal with UICollectionViews in a Swift iOS app - just a simple app that displays an image and a label in the cell 25 times. Initially bad constraints were preventing the CollectionView from displaying anything, but once I worked those out, the CollectionView appears to display cells as desired and expected. Debug messages were displayed from my Layout method for all 25 items as expected.
Then I scrolled down once it got passed the 11th item, it seems to completely lose its layout as you can see in the following image.  Scrolling upward, it's even more wonky, all the cells turn into a full size image.
Example in simulator
There's nothing special about the code, especially the layout - here's that code:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        if indexPath.item > 10 {
// Limit number of debug messages
            print("view.bounds: \(view.bounds)")
            print("collectionView.bounds: \(collectionView.bounds)")
            print("collectionView.alignmentRectInsets: \(collectionView.alignmentRectInsets)")
            print("collectionView.contentInset: \(collectionView.contentInset)")
        }
        let width = view.bounds.width
        print("width[\(indexPath.item)]: \(width)")
        let cellDimension = (width / 2 ) - 15
        print("cellDimension[\(indexPath.item)]: \(cellDimension)")
        let returnSize = CGSize(width: cellDimension, height: cellDimension)
        print("returnSize[\(indexPath.item)]: \(returnSize)")
        return returnSize
    }
Once this goes past the 11th item, the debugger starts spewing lots and lots of messages:
2019-10-22 01:32:54.794032-0500 CollectionTest#1[13173:339022] The behavior of the UICollectionViewFlowLayout is not defined because:
2019-10-22 01:32:54.794203-0500 CollectionTest#1[13173:339022] the item width must be less than the width of the UICollectionView minus the section insets left and right values, minus the content insets left and right values.
2019-10-22 01:32:54.794360-0500 CollectionTest#1[13173:339022] Please check the values returned by the delegate.
2019-10-22 01:32:54.794856-0500 CollectionTest#1[13173:339022] The relevant UICollectionViewFlowLayout instance is <UICollectionViewFlowLayout: 0x7fe4c1407750>, and it is attached to <UICollectionView: 0x7fe492819400; frame = (0 0; 414 736); clipsToBounds = YES; autoresize = LM+W+RM+TM+H+BM; gestureRecognizers = <NSArray: 0x600000965050>; layer = <CALayer: 0x60000076efe0>; contentOffset: {0, 355}; contentSize: {414, 2512}; adjustedContentInset: {20, 0, 0, 0}> collection view layout: <UICollectionViewFlowLayout: 0x7fe4c1407750>.
2019-10-22 01:32:54.801289-0500 CollectionTest#1[13173:339022] Make a symbolic breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes to catch this in the debugger.
This just goes on and on ... abbreviated for brevity.
Thing is, the cells were all OK before.
Additional code:
CollectionViewCell.swift

import UIKit

class CollectionViewCell: UICollectionViewCell {
    #IBOutlet weak var cellImage: UIImageView!
    #IBOutlet weak var cellLabel: UILabel!

}

ViewController.swift

import UIKit

class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 25
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell
        cell.cellImage.image = UIImage(named: "file-35")
        cell.cellLabel.text = "cell# \(indexPath.item)"

        cell.layer.borderWidth = 0.5
        cell.layer.borderColor = UIColor.lightGray.cgColor
        cell.layer.cornerRadius = 10

        cell.backgroundColor = UIColor.black
        cell.cellImage.layer.cornerRadius = 10

        print("indexPath.item: \(indexPath.item)")
        print("view.bounds: \(view.bounds)")
        print("collectionView.bounds: \(collectionView.bounds)")
        print("collectionView.contentInset: \(collectionView.contentInset)")
        print("collectionView.alignmentRectInsets: \(collectionView.alignmentRectInsets)")

        print("layoutAttributesForItem: \(String(describing: collectionView.layoutAttributesForItem(at: indexPath)))")

        return cell
    }


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        collectionView.delegate = self
        collectionView.dataSource = self
//        view.translatesAutoresizingMaskIntoConstraints = true
    }
By the way, it doesn't seem to make any difference which version of the runtime simulator is used.
Any help, pointers, suggestions, etc., is greatly appreciated while I still have some hair left to pull out. Thank you.

Try setting the clipToBounds property of the cellImage inside CollectionViewCell to true, i.e.
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var cellImage: UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
self.cellImage.clipsToBounds = true //here..
}
//rest of the code....
}
You can also set the clipToBounds of cellImage in the UI itself, i.e.
Note: Move all the code for cell layout in the CollectionViewCell instead of writing everything in cellForItemAt. That'll make the code less bulky and readable.

Thanks for the suggestions. I found a Stack Overflow article that pushed me in a direction that seems to have helped solve the problems.
I could be wrong, but my observations tell me that it seems that not all Cells get their Size initialized from the Storyboard values. The first dozen or so do but when the collection view starts reusing / recycling cells, it loses track of the cell size. I tried setting the cell size in cellForItemAt() but that causes some really bad recursive problems.
Watching with the debugger, sizeForItemAt() only gets called initially, not when cells start being reused, and when the cell size has been lost, all sorts of undesirable things begin to happen.
Initializing the cell size in viewDidLoad() eliminated most of the problems, that is until the device is rotated, then sizeForItemAt() gets called in iPhone simulators for the new layout, but not in iPad simulators.
The solution to this is to check for the device type in viewDidLoad() and compute the desired number of cells per row. Simply setting the cell size of the layout doesn't completely solve the problem if you want a different number of cells per row when the device is rotated.
This seems to have tamed the problems, for now at least.
I'm just a bit concerned that I'm missing something because the sizeForItemAt() function doesn't get called as expected.

Related

Why is only half of a collection view getting displayed?

I have used MMPlayerView (GitHub link here) to add videos into a collection view, in the collection view the image gets replaced by a video in the code (from the demo). I took the MMPlayerView's demo project and changed stuff to fit my needs and then implemented it into my project to get something like this show in this video.
Now I'd like to obviously move the collection view up towards the buttons at the top of the screen, and later on I will use multiple collection views like that to get the style of the app store more like, just showing videos. Since I took the code from the demo I'm not too sure on some things, when I copied the project's demo the collection view was made for scrolling through it vertically, so it takes up the whole screen, so the collection view was constrained to the bottom of the view:
Which causes this to show in the app (I don't understand why it's so far away from the top when its constrained by 10 points):
Now I want to obviously want to remove the bottom constraint and constrain it by it's height instead so I can have multiple collection views, (probably through the use of table views), so I removed the constraint to the bottom of the view and added a height constraint for 250 points:
Which caused the app to look like this:
Why?? I'm guessing it has something to do with this part of the view controller code:
extension HomeViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let m = min(UIScreen.main.bounds.size.width, UIScreen.main.bounds.size.height)
return CGSize(width: m, height: m*0.75)
}
I want to keep the collection view's width the same width of the view, so the width is fine, it's just the height here. When I try and set the height to a constant value of 250 (the same as the height constraint) it does the same thing (only shows half of it) but shrinks the video a little bit. What can I change?
Taking a quick look at the Example app from MMPlayerView...
Change the height value in sizeForItemAt to the height of your collection view (250 per your question):
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let m = min(UIScreen.main.bounds.size.width, UIScreen.main.bounds.size.height)
return CGSize(width: m, height: 250.0)
//return CGSize(width: m, height: m*0.75)
}
I'm assuming you were getting the same error messages in debug console as I got:
The behavior of the UICollectionViewFlowLayout is not defined because:
the item height must be less than the height of the UICollectionView minus the section insets top and bottom values, minus the content insets top and bottom values.
and it should also have printed the Collection View properties, including:
adjustedContentInset: {0, 0, 200, 0};
So the collection view has a Bottom inset of 200.
If you search that example project for contentInset you'll find this line in the view controller's viewDidLoad() function:
playerCollect.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 200, right:0)
If you comment-out / remove that line, you should now see the full cell.
Might be a good idea to get a handle on collection views to begin with and then review that example project for the video implementations in your app.

CollectionView won't stop complaining about undefined behaviour

I have a set of 4 different elements that can be shown in a small horizontal collectionView. There will only be one row, and the collectionView and the elements will always be the same height as each other - however, it needs to be dynamic, in case people increase their text size (accessibility larger text).
To make this work, I have a "spacer" label next to the CollectionView, and tell the collectionView to be the same height as the label, and I do the same in all the cells. They all have the same font.
When I try to run it, the CollectionView prints out this:
The behavior of the UICollectionViewFlowLayout is not defined because:
the item height must be less than the height of the UICollectionView
minus the section insets top and bottom values,
minus the content insets top and bottom values.
Please check the values returned by the delegate.
The relevant UICollectionViewFlowLayout instance is <...>,
and it is attached to <..; frame = (0 438.5; 414 29.5);
clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <..>;
layer = <CALayer:..>; contentOffset: {0, 0}; contentSize: {32, 29.5};
adjustedContentInset: {0, 0, 0, 0}> collection view layout: <...>.
Make a symbolic breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes
to catch this in the debugger.
But everything I can find returns 29.5 or less.
collectionView.contentInsetAdjustmentBehavior = .never
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
ContentInset is .zero, sectionInset is 0 for top and bottom, lineSpacing and interItem-spacing is 0, scrollDirection = .horizontal..
I have not overridden any sizeForItem or anything like that, because it should happen automatically, and I shouldn't have to create custom height logic.
I have tried this in each cell:
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
setNeedsLayout()
layoutIfNeeded()
layoutAttributes.frame.size = contentView.systemLayoutSizeFitting(UICollectionViewCell.layoutFittingCompressedSize)
print("Height: ", layoutAttributes.size)
return layoutAttributes
}
But it just prints out 29.5, which should be correct.
It does print it out after the warnings though, so something happens before this.
When I print out the height of the cell directly after its initialization like this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let item = items[indexPath.row]
if let someItem = item as? SomeCellViewModel, let cell = self.dequeueReusableCell(withReuseIdentifier: "SomeIdentifier", for: indexPath) as? SomeCell{
cell.bind(with: someItem)
print("height: ", cell.bounds.size.height)
return cell
}else{ ...
it prints out "50.0". And as I understand, that's the default "Item Size", but I've changed all that! In Storyboard/IB/Xib, the "Collection View Flow Layout" was 50x50, but I have changed it to different things (0x0, 1x1, 20x20, tested a lot). Still it prints out 50.0. The xibs for the cells aren't even 50 in their file. If I try to use the systemLayoutSize-thing directly here to set its frame.size, it warns me about an autoresizing constraint that wants to be 0. I assume I shouldn't be doing such logic here anyway, so I nevermind this.
Since the warning says items "must be less than the height minus X minus Y" (not less than or equal to), I tried making the CollectionView a point taller than the spacer-logic, making the CollectionView 30.5 and the cells 29.5, but I still get the warning.
I have tried setting the symbolic breakpoint, but there is nothing of value there..
The thing is - it looks and behaves correct - on iOS 12. It does not work on iOS 11. There might be a separat constraint-issue within the elements that makes it wrong on iOS 11, I'm looking into it. Either way;
Why am I receiving this warning?
Edit: if I implement sizeForItemAt:IndexPath and statically tell it to be
width:100, height: 29.5, there is no warning at all. Everything is good. But I don't want to do that. I shouldn't need to calculate the individual height like this. And I don't really want to deal with prototype-cells either.
This should work.

UICollectionView cellForItemAt: won't be called while layoutAttributesForElements are returning values

I have a bit complex UICollectionView with my custom UICollectionViewLayout.
I added a pinch gesture behavior onto it and adjust the layout according to that result.
It's basically working pretty well, the cells are resized and repositioned properly in normal cases.
But when it comes to a particular condition the cells disappear and never appear again.
So far I'm unable to clarify the condition, but it happens often when you pinched the view to a smaller size.
The issue is, layoutAttributesForElements in my collection view layout is called (of course this also implies that numberOfCells:inSection is called as well) and it's returning reasonable cell geometry, but actual cell generation (cellForItemAt:) won't be called.
Before jumping into the code(as it's a bit too long and complicated) I want to ask you guys if any of you have had the same kind of situation.
Below is the summary of what's happening and what I see so far.
It's not that always happening. It happens only after pinching and reached to a certain condition. So this is not a basic kind of how-to-use-UICollectionView issue.
Even when it happens layoutAttributesForElements keeps being called (as you keep pinching)
The layout attributes don't have crazy values like zero size or position of far out of view range. They all have the position attributes that fit into the collection view's content size.
Collection view claims the proper content view size (at least as reported on the debugger.)
When it happens you cannot see any cells in the view hierarchy in the View Debugger. Meaning, it's not zero sized or clear colored but cells themselves are gone. This corresponds to the fact that cellForItemAt: is not called.
After it happened you cannot see the cells anymore even you pinch the screen back to the original scale.
Any information is appreciated. Thanks!
EDIT
My collection view layout is like this. This project is a musical sequencer and the collection view is showing your musical notes in piano roll style.
//
// YMPianoRollLayout.swift
//
import UIKit
class YMPianoRollLayout: UICollectionViewLayout {
let notes : Array<CGPoint> = []
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!;
}
override var collectionViewContentSize : CGSize {
let cv = self.collectionView as! YMPianoRollCollectionView
let screenInfo = cv.pianoRollViewController.screenInfo
let totalSize = screenInfo.collectionViewContentsSize();
print("contentSize = \(totalSize)")
return totalSize;
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// "screenInfo" keeps the user specified view parameters including the scale ratio by the pinch gesture
let cv = self.collectionView as! YMPianoRollCollectionView;
let pianoRoll = cv.pianoRollViewController;
// Check which musical note can be included in the view rect
let indexArray: Array<Int> = pianoRoll!.getNoteIndexes(inRect:rect, useOnlyStartTime: false);
var retArray : [UICollectionViewLayoutAttributes] = []
for i in indexArray {
if let _ = pianoRoll?.pattern.eventSequence[i] as? YMPatternEventNoteOn {
retArray.append( self.layoutAttributesForPatternEventInfo(i) )
}
}
// This always reports non-zero count. Also checked the positions of each array members
// by breaking here and they all had proper size and positions
print("retArray count = \(retArray.count)");
return retArray
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let i = Int((indexPath as NSIndexPath).row)
return self.layoutAttributesForPatternEventInfo(i)
}
//
// This is my specific func to convert the musical time-pitch into the view geometory
//
func layoutAttributesForPatternEventInfo(_ index: Int) -> UICollectionViewLayoutAttributes!{
let cv = self.collectionView as! YMPianoRollCollectionView
// "screenInfo" keeps the user specified view parameters including the scale ratio by the pinch gesture
let screenInfo = cv.pianoRollViewController.screenInfo
// Retrieve musical event
let event = cv.pianoRollViewController.pattern.eventSequence[index]
let index = IndexPath(row:index, section: 0)
let newAttr = UICollectionViewLayoutAttributes(forCellWith:index)
var frame : CGRect!
if let e = event as? YMPatternEventNoteOn {
let noteNo = e.noteNo;
let origin = YMMusicalValuePoint(time:event.time, noteNumber:noteNo);
let size = YMMusicalValueSize(timeLength:e.duration, numberOfNotes: 1);
// Actual size calculation is done in my "screenInfo" class.
frame = screenInfo.getPhysicalRange(YMMusicalValueRange(origin:origin, size:size));
} else {
frame = CGRect(x: 0, y: 0, width: 0, height: 0);
}
newAttr.frame = frame;
newAttr.zIndex = 1;
return newAttr
}
//
// For checking the bounds
//
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
print("BBB newBounds = \(newBounds)")
return true
}
}
Sounds like an interesting problem...
More info on your custom UICollectionViewLayout subclass (i.e. the code) would be useful but I can offer an overview of how UICollectionView typically updates visible content in response to bounds changes (i.e. content offset, rotation) and invalidateLayout calls.
When invalidateLayout fires the internal attributes cache is cleared signaling the need to ask the layout on the next layoutSubviews pass what should be visible. Conversely, if the layout is not invalidated and the attributes are known in the cache the layout will not be asked.
The magic all happens when the current CA transaction commits and UICollectionView's layoutSubviews is invoked. At this point, a diff between what was last known to be on screen and what should now be on screen is computed according the current "visible bounds" (effectively the scroll view's bounds which includes the offset into the layout).
Cells no longer visible in the new visible bounds will be returned to the reuse queue and newly appearing cells (as per the layout) will be constructed and existing items (that have changed) will be updated with their new attributes.
Based on your description, it sounds like when the next layoutSubviews fires the queried attributes (possibly from the cache!) aren't returning anything for the new visible bounds therefore nothing "new" appears -- along with the existing items disappearing...
A few things to investigate might include:
Is your gesture causing your custom layout to invalidate? If not, it probably should so the UICollectionView knows to not trust it's internal attributes cache and always ask the layout for new attributes during the "diffing" process.
How is the bounds of the collection changing during the pinch gesture?
This directly affects the diff since it will use this visible bounds to determine what should be displayed next.
How does your layout respond to shouldInvalidateForBoundsChange:?
Most layouts only invalidate when the extents (e.g. rotation) change, so the UICollectionView will normally rely on it's cached attributes when performing the diff. In your case if you are tweaking the bounds during the gesture, you'll want to unconditionally return YES here.
When you get into this wonky state, you might try pausing into the debugger e.g.
po [[cv collectionViewLayout] invalidateLayout]
po [cv setNeedsLayout]
po [cv layoutIfNeeded]
...and resume. Has everything re-appeared?
If so, it sounds like the layout isn't being invalidated under certain circumstances and the attributes being returned are indeed reasonable.
If not, ping the layout and see what if it is reporting as visible is reasonable via:
po [[cv collectionViewLayout] layoutAttributesForElementsInRect:cv.bounds]

Auto layout calculating same height for each collectionViewCell

So I have a collection view that uses a custom layout that I found on Github called CHTCollectionViewWaterfallLayout I like it so far, however I am looking to make the custom collection view cell I made dynamic in height, and I am having trouble connecting Auto Layout to calculate this. I shared a simple sample project on Github that generates random string sizes and displays them, only problem is that Auto Layout generates the same cell height for each Collection View Cell. The project can be found here.
To give you a run down of my thought process, I calculate the cell height by using the method CHTCollectionViewDelegateWaterfallLayout sizeForItemAtIndexPath. Since I also use the delegates method columnCountforSection my thought is since I provide a finite number of columns based on the orientation, I take the collectionView frame width and I divide by the number of columns to get me my width for the cell.
func collectionView (collectionView: UICollectionView,layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
var cell = dict["heightCell"] as? CollectionViewCell
let randomString = RadomStrings[indexPath.item]
let float = CGFloat(columnCount)
let width = collectionView.bounds.size.width / float
if cell == nil {
cell = NSBundle.mainBundle().loadNibNamed("CollectionViewCell", owner: self, options: nil)[0] as? CollectionViewCell
dict["heightCell"] = cell
}
cell?.RandomStringLabel.text = randomString
cell!.frame = CGRectMake(0, 0, CGRectGetWidth(collectionView.bounds), CGRectGetHeight(cell!.frame))
cell!.setNeedsLayout()
cell!.layoutIfNeeded()
var size = cell?.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
size?.width = width
return CGSize(width: width, height: size!.height)
}
My constraints are pretty basic. I have a constraint on each axis. One on the top of the content view, leading, trailing and bottom. I also have a height on the label that is greater than or equal to 45.
Using Auto Layout to calculate TableViewCell heights is easy to me and I like it because I like the DRY principle behind this height calculation approach. So I would like to keep this height calculation process the same throughout my app. CollectionViews are a relatively new layout process for me, so I would love to learn what I am doing wrong here. Hopefully I am clear for everyone, thanks!
I figured it out! What I didn't do is put a width constraint on the custom cell I created! Duh!

How to adjust UICollectionView contentSize.height based on number of rows in swift

I'd really appreciate if someone can light up some ideas here. I've been trying to fix this for weeks now (not kidding).
I have a "to do list" using a collectionView where I hide the rows that were completed and move them to the end of the list. I then unhide the items if needed with a button. The collectionView looks exactly as a tableView with one item(cell) per row.
When the items are hidden the collectionView has a lot of empty scrolling space at the bottom instead of automatically deleting the space used by the hidden rows since they technically are still there.
I'm trying to cut that empty space so the collectionView height would be equal to the amount of cells/rows left visible.
override func viewDidAppear(animated: Bool) {
if isCellHidden { //checking if cells are hidden
var heightOfSection0 = 95 + (42 * self.collectionView.numberOfItemsInSection(0))
println(self.collectionView.contentSize.heigh) //returns 2350
self.collectionView.contentSize.height = CGFloat(heightOfSection0)
println(heightOfSection0) //returns 1019
println(self.collectionView.contentSize.heigh) //returns 1019.0 which is correct but as soon as I scroll down it resets to it's original size (2350) and let's me scroll through that empty space...
}}
If I try to read the collectionView height immediately after setting this, it displays the correct value but as soon as I try to scroll down it resets back to it's original height. I also tried disabling the auto layout and it doesn't make a difference
You should not manage contentSize directly - return appropriate number of items to be displayed from collectionView(_:numberOfItemsInSection:) instead (i.e. do not count your "hidden" cells).
You can use sizeForItemAtIndexPath: to change the size of collection view cell.
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
var numberOfCellInRow : Int = 3
var padding : Int = 5
var collectionCellWidth : CGFloat = (self.view.frame.size.width/CGFloat(numberOfCellInRow)) - CGFloat(padding)
return CGSize(width: collectionCellWidth , height: collectionCellWidth)
}
Changing UI stuff in viewDidAppear() in theory is a good place to do so. However, at this point, you can't be sure that auto layout has operated on the subview that you're manipulating.
You can check when it has and when it's safe to manipulate the frame, by subclassing it and overriding layoutSubviews()
I had a similar issue here: https://stackoverflow.com/a/30446125/4396258

Resources