I don't know why it is so complicated to design cells that can adapt to its content. It shouldn't need that much code, I still don't understand why UIKit can't handle this properly.
Anyway, here is my issue (I have edited the whole post):
I have an UICollectionViewCell that contains an UITableView.
Here is my sizeForItem method :
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
var cellWidth: CGFloat = collectionView.bounds.size.width
var cellHeight: CGFloat = 0
let cellConfigurator = items[indexPath.item].cellConfigurator
if type(of: cellConfigurator).reuseId == "MoonCollectionViewCell" {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: type(of: cellConfigurator).reuseId, for: indexPath) as? MoonCollectionViewCell {
cell.contentView.layoutIfNeeded()
let size = cell.selfSizedTableView.intrinsicContentSize
cellHeight = size.height
}
}
return CGSize.init(width: cellWidth, height: cellHeight)
}
sizeForItem is called before cellForItem, that's the reason of the layoutIfNeeded, because I couldn't get the correct intrinsic content size.
I have removed the XIB as suggested, and designed my UICollectionViewCell within the Storyboard.
Here is my UICollectionViewCell designed within a Storyboard (only the UITableViewCell is designed in a XIB file)
I only added an UITableView within the UICollectionViewCell.
I want the UICollectionViewCell to adapt its size according to the height of the tableView.
Now here is my tableView :
I have created a subclass of UITableView (from this post)
class SelfSizedTableView: UITableView {
var maxHeight: CGFloat = UIScreen.main.bounds.size.height
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
self.layoutIfNeeded()
}
override var intrinsicContentSize: CGSize {
let height = min(contentSize.height, maxHeight)
return CGSize(width: contentSize.width, height: height)
}
}
Please note that I have disabled scrolling, I have dynamic prototype for the tableView cells, the style is grouped.
EDIT : Check the configure method, it comes from a protocol I used to configure in a generic way all my UICollectionViewCell
func configure(data: [MoonImages]) {
selfSizedTableView.register(UINib.init(nibName: "MoonTableViewCell", bundle: nil), forCellReuseIdentifier: "MoonTableViewCell")
selfSizedTableView.delegate = self
selfSizedTableView.dataSource = moonDataSource
var frame = CGRect.zero
frame.size.height = .leastNormalMagnitude
selfSizedTableView.tableHeaderView = UIView(frame: frame)
selfSizedTableView.tableFooterView = UIView(frame: frame)
selfSizedTableView.maxHeight = 240.0
selfSizedTableView.estimatedRowHeight = 40.0
selfSizedTableView.rowHeight = UITableView.automaticDimension
moonDataSource.data.addAndNotify(observer: self) { [weak self] in
self?.selfSizedTableView.reloadData()
}
moonDataSource.data.value = data
}
FYI the dataSource is a custom dataSource, with dynamic value (Generics) and the observer pattern, to reload the collection/tableView when the data is set.
I also have this warning when I launch the App.
[CollectionView] An attempt to update layout information was detected
while already in the process of computing the layout (i.e. reentrant
call). This will result in unexpected behaviour or a crash. This may
happen if a layout pass is triggered while calling out to a delegate.
Any hints or advice on how I should handle this ?
Because I am facing a strange behavior, it's like my sizeForItem use random values. The UICollectionViewCell height is not the same than my UITableView intrinsic content size height.
If I have 2 rows within my UITableView, the UICollectionView is not always equal at this size. I really don't know how to achieve this...
Should I invalideLayout?
Maybe it's not the answer you wanted, but here're my two cents. For your particular requirements, the better solution is moving away from UITableView, and use UIStackView or your custom container view.
Here's why:
UITableView is a subclass of UIScrollView, but since you've disabled its scrolling feature, you don't need a UIScrollView.
UITableView is mainly used to reuse cells, to improve performance and make code more structured. But since you're making it as large as its content size, none of your cells are reused, so features of UITableView is not taken any advantage of.
Thus, actually you don't need and you should not use either UITableView or UIScrollView inside the UICollectionViewCell for your requirements.
If you agree with above part, here're some learnings from our practices:
We always move most of the underlying views and code logics, mainly data assembling, into a UIView based custom view, instead of putting in UITableViewCell or UICollectionViewCell directly. Then add it to UITableViewCell or UICollectionViewCell's contentView and setup constraints. With this structure, we can reuse our custom view in more scenarios.
For requirements similar to yours, we'll create a factory class to create "rows" similar to how you create "cells" for your UITableView, add them into a vertical UIStackView, create constraints deciding UIStackView's width. Auto layout will take care of the rest things.
In your usage with UICollectionViewCell, to calculate the wanted height, inside preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) func of your cell, you can use contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) to calculate the height, do some check and return. Also, remember to invalidate layout when the width of the UICollectionView changes.
It is indeed very tricky, but I found a working way to solve this problem. As far as i know i got this from a chat app, where message bubble sizes are dynamic.
Here we go:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
// Minimum size
let frame = CGRect(x: 0, y: 0, width: view.frame.width - 30, height: 0)
let cell = MoonCollectionViewCell()
// Fill it with the content it will have in the actual cell,
// cell.content is just an example
let cell.content = items[indexPath.item]
cell.layoutIfNeeded()
// Define the maximum size it can be
let targetSize = CGSize(width: view.frame.width - 30, height: 240)
let estimatedSize = cell.systemLayoutSizeFittingSize(tagetSize)
return CGSize(width: view.frame.width - 30, height: estimatedSize.height)
}
What it basically do is, to define a minimum frame and the size that is targeted. Then by calling systemLayoutSizeFittingSize, it resizes the cell to the optimal size, but not larger than the targetSize.
Adjust the code to your needs, but this should work.
I tried to find the culprit in the posted code, but it seems that there are many moving parts. So, I will try to give some hints, that hopefully could help.
In theory (there is caveat for iOS 12), self sizing UICollectionViewCells should not be difficult. You essentially could set the collectionViewLayout.estimedItemSize to any value (preferred is the constant below), like this:
(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
Then you have to make sure the constraints in the cells are set in a way that it can self size; that is auto layout can calculate the width and the height of the cell. You are providing an intrinsicContentSize of the tableView and it is wrapped by its super view from all four ends, so this should be OK.
Once you set the estimatedItemSize as shown above, you should not implement the delegate method returning the size:
func collectionView(_: UICollectionView, layout: UICollectionViewLayout, sizeForItemAt: IndexPath) -> CGSize
A quick tutorial can be found here for further reference: https://medium.com/#wasinwiwongsak/uicollectionview-with-autosizing-cell-using-autolayout-in-ios-9-10-84ab5cdf35a2
As I said in theory it should not be difficult, but cell auto sizing seems broken on iOS 12 see here In iOS 12, when does the UICollectionView layout cells, use autolayout in nib
If I were in you position, I would start from afresh, adding complexity step by step:
try implement the self sizing cells, possibly with with a simple UIView and an override of intrinsicContentSize; possibly by using iOS 11.4 SDK to exclude issues relevant to iOS 12 (the easiest way is to download latest Xcode 9 and work from there); if not possible do the iOS 12 fixes at this step
replace the simple view with a table view (which may also have dynamic sizing per see)
do the tableview reload data flow, i.e. dynamic sizing feature
if everything OK, do the iOS 12 fixes and migrate to iOS 12
Hope this helps.
BTW, the warning in the console is probably due to call to layoutIfNeeded() in the delegate method. It triggers an immediate layout pass, whereas this is done for the UICollectionView once all sizes are collected.
I have a collectionView that scrolls vertically. Inside the cells of that collectionView is another collectionView that I have a custom layout for. The layout is currently a subclass of UICollectionViewFlowLayout and I want the scroll direction to be horizontal. I have tried a variety of ways to set the scrollDirection to horizontal but none of them are working. How do I set scroll direction on a subclass of FlowLayout?
You can easily change scroll direction by this code:
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
layout.scrollDirection = .horizontal
}
I'm trying to find an elegant way of doing auto sizing of UICollectionViewCell objects.
I understand that this topic has been covered a fair bit, in the form of blog posts and SO questions. In spite of that, I've yet come across an elegant solution.
For example, here are the steps to getting an auto resizing UITableViewCell to resize based on the vertical length of a single UILabel:
Make sure there are vertical spacing constraints from the contentView to the UILabel.
Set the rowHeight property to UITableViewAutomaticDimension
Set the estimatedRowHeight property
I am hoping I can follow through these steps for the UICollectionView. Here's what I've done so far:
class ViewController: UIViewController {
#IBOutlet private var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
let layout = collectionView.collectionViewLayout as! UICollectionViewLayout
layout.itemSize = UICollectionViewFlowLayoutAutomaticSize
Layout.estimatedItemSize = CGSize(width: collectionView.bounds.width, height: 100)
}
}
Currently, this automatically resizes horizontally. What I really want is to micmic the UITableView resizing and have it auto size vertically.
Does anyone have a solution to do this?
So far, I made my collection views which scroll horizontally in either left or right.I added UICollectionViewCells into one UICollectionView. My problem that I'm having is trying to find the right settings to make the cards stack on top of the first card, as demonstrated in the photo below.
Here are the settings for my collectionView and how it displays its cells.
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .Horizontal
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 78
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = UIColor.clearColor()
cv.showsHorizontalScrollIndicator = false
cv.translatesAutoresizingMaskIntoConstraints = false
return cv
}()
You're going to have to subclass UICollectionViewLayout, set the UICollectionViewLayoutAttributes for each cell, and make sure to set zIndex to the indexPath.row of the item in order to get overlapping. You can take a look at my sample project on GitHub that implements something very similar.
There are some bugs in UICollectionViewLayout related to animating insertions of new cells when the cells are overlapping (which is why I made the sample project in the first place).
I'm trying to create a UICollectionView whose width/height and coordinates are defined using AutoLayout (using SnapKit). When using the default UICollectionView constructor it fails with the following reason:
reason: 'UICollectionView must be initialized with a non-nil layout parameter'
The only constructor that allows a layout to be passed in, also requires a frame, so I tried using CGRectZero as the value for the frame like so:
collectionView = UICollectionView(frame: CGRectZero, collectionViewLayout: layout)
I then used SnapKit to setup the constraints like so:
collectionView?.snp_makeConstraints { make -> Void in
make.width.equalTo(view)
make.height.equalTo(300)
make.top.equalTo(someOtherView)
}
However, when doing so the UICollectionView is never rendered. In fact, I do not see the data source being called at all.
Any thoughts on how to use AutoLayout with a UICollectionView or what I might be doing wrong?
This following code works just fine for me and I got a red and empty collection view.
Xcode 7 beta 4 - Swift 2.0 - AutoLayout syntax from iOS 9
override func viewDidLoad() {
super.viewDidLoad()
let collectionView = UICollectionView(frame: CGRectZero, collectionViewLayout: UICollectionViewLayout())
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = UIColor.redColor()
self.view.addSubview(collectionView)
collectionView.widthAnchor.constraintEqualToAnchor(self.view.widthAnchor).active = true
collectionView.heightAnchor.constraintEqualToConstant(300).active = true
collectionView.topAnchor.constraintEqualToAnchor(self.view.topAnchor).active = true
}
I never used SnapKit but I do know it. The syntax provided in my code is almost the same as in your example. So it is only a clue that something is wrong with SnapKit or how you use it.
I hope this can help you somehow.