问题
Using Swift-5.0, Xcode-10.2, iOS-12.2,
There is an issue in my code while trying to achieve "autosizing" of cells in a UICollectionView (using UICollectionViewFlowLayout as its layout).
The height of the cell is content-based (and unknown upfront) - therefore I try to get the cell's height to autosize (the "width" I don't care for now and can, for example, be set to frame.width
).
Even tough, I only use one large UICollectionView-Cell for the example below, I would still like to keep "dequeueing" of the cells alive since later on, there will be many more cells that need to be filled with large content. Therefore, to make this example more simple, I keept the numberOfItemsInSection
at 1.
Moreover, for the below example, each custom CollectionViewCell is filled with a vertical StackView (vertically adding-up a couple of Labels and ImageViews). The StackView is not important here, but I made it as a quick example. Again, the content of the custom CollectionViewCell will change later on (especially it will change to a content-dependent height). The fixed-height cell-content in the code below is just for example reasons...
But what I would still like to get out of this, is the question on how to make the CollectionViewCell autosize its height according to the content (whether fixed-height like in the below example or dynamic content-based-height like in the future implementation) ???
I keep getting the following Constraint-error:
Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one
you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
Everyting works, if I give the Cell a very large fixed height using the method sizeForItemAt
inside my UICollectionViewController
. (i.e. with "very large" I mean much bigger than the cell's height turns out when dequeueing of the cell takes place).
But as explained above, I do not want to set the cell's height to a fixed value (using the UICollectionViewController's method sizeForItemAt
- but rather achieve the desired autosizing.
To autosize the cell, I tried the following:
Approach A)
Use collectionViewFlowLayout.estimatedItemSize = CGSize(...)
(and don't use sizeForItemAt
method)
--> Again same thing: If the estimated-size is set large enough, no error occurs. If I set it too small, then I get the same Constraint-error...
Approach B)
Use collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
(and use or don't use sizeForItemAt
method - does not make a difference)
--> Again same error: As soon as I start scrolling on the UICollectionView-cell, it throws the same Constraint-error...
Somewhat promising is the observation that using UICollectionViewFlowLayout.automaticSize
makes the App work as desired (except that the above error is still thrown - but strange-enough, the App continues somehow to run anyway). For me it is not acceptable that the App works and an error is thrown. The question is, how to get rid of the Constraint-error ??
Here is all the code that I use:
class TestViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
let cellId = "cellID"
override func viewDidLoad() {
super.viewDidLoad()
collectionView.backgroundColor = .yellow
collectionView.register(MyCollectionViewCell.self, forCellWithReuseIdentifier: cellId)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MyCollectionViewCell
return cell
}
// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// return .init(width: view.frame.width, height: 4000)
// }
init() {
let collectionViewFlowLayout = UICollectionViewFlowLayout()
// collectionViewFlowLayout.estimatedItemSize = CGSize(width: UIScreen.main.bounds.width, height: 4000)
collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
super.init(collectionViewLayout: collectionViewFlowLayout)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here is the CustomCollectionViewCell:
import UIKit
class MyCollectionViewCell: UICollectionViewCell {
let titleLabel1 = UILabel(text: "Title 123", font: .boldSystemFont(ofSize: 30))
let titleLabel2 = UILabel(text: "Testing...", font: .boldSystemFont(ofSize: 15))
let imgView1 = UIImageView(image: nil)
let imgView2 = UIImageView(image: nil)
let imgView3 = UIImageView(image: nil)
let imgView4 = UIImageView(image: nil)
let imgView5 = UIImageView(image: nil)
let imageView: UIImageView = {
let imgView = UIImageView()
imgView.backgroundColor = .green
return imgView
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .yellow
titleLabel1.constrainHeight(constant: 50)
titleLabel1.constrainWidth(constant: 250)
titleLabel2.constrainHeight(constant: 50)
titleLabel2.constrainWidth(constant: 250)
imgView1.constrainHeight(constant: 100)
imgView1.constrainWidth(constant: 200)
imgView1.backgroundColor = .green
imgView2.constrainHeight(constant: 100)
imgView2.constrainWidth(constant: 200)
imgView2.backgroundColor = .green
imgView3.constrainHeight(constant: 100)
imgView3.constrainWidth(constant: 200)
imgView3.backgroundColor = .green
imgView4.constrainHeight(constant: 100)
imgView4.constrainWidth(constant: 200)
imgView4.backgroundColor = .green
imgView5.constrainHeight(constant: 100)
imgView5.constrainWidth(constant: 200)
imgView5.backgroundColor = .green
let stackView = UIStackView(arrangedSubviews: [titleLabel1, imgView1, titleLabel2, imgView2, imgView3, imgView4, imgView5])
addSubview(stackView)
stackView.anchor(top: safeAreaLayoutGuide.topAnchor, leading: leadingAnchor, bottom: bottomAnchor, trailing: trailingAnchor)
stackView.spacing = 20
stackView.axis = .vertical
stackView.alignment = .center
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Needed extensions are defined as such:
extension UILabel {
convenience init(text: String, font: UIFont) {
self.init(frame: .zero)
self.text = text
self.font = font
self.backgroundColor = .red
}
}
extension UIImageView {
convenience init(cornerRadius: CGFloat) {
self.init(image: nil)
self.layer.cornerRadius = cornerRadius
self.clipsToBounds = true
self.contentMode = .scaleAspectFill
}
}
To abstract away the anchoring and height-definitions of the autolayout-constraints of the cell's components (i.e. a bunch of labels and imageViews that fill the cell), I used the following UIView extension...:
(The extension has been published by Brian Voong - see video link)
// Reference Video: https://youtu.be/iqpAP7s3b-8
extension UIView {
@discardableResult
func anchor(top: NSLayoutYAxisAnchor?, leading: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, trailing: NSLayoutXAxisAnchor?, padding: UIEdgeInsets = .zero, size: CGSize = .zero) -> AnchoredConstraints {
translatesAutoresizingMaskIntoConstraints = false
var anchoredConstraints = AnchoredConstraints()
if let top = top {
anchoredConstraints.top = topAnchor.constraint(equalTo: top, constant: padding.top)
}
if let leading = leading {
anchoredConstraints.leading = leadingAnchor.constraint(equalTo: leading, constant: padding.left)
}
if let bottom = bottom {
anchoredConstraints.bottom = bottomAnchor.constraint(equalTo: bottom, constant: -padding.bottom)
}
if let trailing = trailing {
anchoredConstraints.trailing = trailingAnchor.constraint(equalTo: trailing, constant: -padding.right)
}
if size.width != 0 {
anchoredConstraints.width = widthAnchor.constraint(equalToConstant: size.width)
}
if size.height != 0 {
anchoredConstraints.height = heightAnchor.constraint(equalToConstant: size.height)
}
[anchoredConstraints.top, anchoredConstraints.leading, anchoredConstraints.bottom, anchoredConstraints.trailing, anchoredConstraints.width, anchoredConstraints.height].forEach{ $0?.isActive = true }
return anchoredConstraints
}
func fillSuperview(padding: UIEdgeInsets = .zero) {
translatesAutoresizingMaskIntoConstraints = false
if let superviewTopAnchor = superview?.topAnchor {
topAnchor.constraint(equalTo: superviewTopAnchor, constant: padding.top).isActive = true
}
if let superviewBottomAnchor = superview?.bottomAnchor {
bottomAnchor.constraint(equalTo: superviewBottomAnchor, constant: -padding.bottom).isActive = true
}
if let superviewLeadingAnchor = superview?.leadingAnchor {
leadingAnchor.constraint(equalTo: superviewLeadingAnchor, constant: padding.left).isActive = true
}
if let superviewTrailingAnchor = superview?.trailingAnchor {
trailingAnchor.constraint(equalTo: superviewTrailingAnchor, constant: -padding.right).isActive = true
}
}
func centerInSuperview(size: CGSize = .zero) {
translatesAutoresizingMaskIntoConstraints = false
if let superviewCenterXAnchor = superview?.centerXAnchor {
centerXAnchor.constraint(equalTo: superviewCenterXAnchor).isActive = true
}
if let superviewCenterYAnchor = superview?.centerYAnchor {
centerYAnchor.constraint(equalTo: superviewCenterYAnchor).isActive = true
}
if size.width != 0 {
widthAnchor.constraint(equalToConstant: size.width).isActive = true
}
if size.height != 0 {
heightAnchor.constraint(equalToConstant: size.height).isActive = true
}
}
func centerXInSuperview() {
translatesAutoresizingMaskIntoConstraints = false
if let superViewCenterXAnchor = superview?.centerXAnchor {
centerXAnchor.constraint(equalTo: superViewCenterXAnchor).isActive = true
}
}
func centerYInSuperview() {
translatesAutoresizingMaskIntoConstraints = false
if let centerY = superview?.centerYAnchor {
centerYAnchor.constraint(equalTo: centerY).isActive = true
}
}
func constrainWidth(constant: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
widthAnchor.constraint(equalToConstant: constant).isActive = true
}
func constrainHeight(constant: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
heightAnchor.constraint(equalToConstant: constant).isActive = true
}
}
回答1:
Finally, after some more digging, I've found a solution:
Four things are important if you want to autosize your custom UICollectionViewCell:
- when creating your UICollectionViewController instance, make sure you pass a FlowLayout having set the
estimatedItemSize
property to some educated-guess value (in my case with a height = 1 [since unknown])
let collectionViewFlowLayout = UICollectionViewFlowLayout()
collectionViewFlowLayout.estimatedItemSize = CGSize(width: UIScreen.main.bounds.width, height: 1)
let vc = TestViewController(collectionViewLayout: collectionViewFlowLayout)
.
- create a property inside your custom UICollectionViewCell that keeps track of the cell's height
(i.e. inside your UICollectionViewCell class, you will always know what the cell's height is (even if dynamically changed later on). Again, I wanted to get rid of the fact that I needed to define this height already at the UICollectionViewController's sizeForItemAt
method before dequeueing the cell)
- Don't forget to add the
preferredLayoutAttributesFitting
method inside your custom UICollectionViewCell :
let myContentHeight = CGFloat(720)
// in my above code-example, the 720 are the sum of 2 x 50 (labels) plus 5 x 100 (imageViews) plus 6 x 20 (spacing)
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
setNeedsLayout()
layoutIfNeeded()
var newFrame = layoutAttributes.frame
// myContentHeight corresponds to the total height of your custom UICollectionViewCell
newFrame.size.height = ceil(myContentHeight)
layoutAttributes.frame = newFrame
return layoutAttributes
}
- Of course, whenever your custom CollectionViewCell changes its height dynamically, you also need to change the
myContentHeight
property again...
(the calling of preferredLayoutAttributesFitting
you don't need to care yourself in code, this method will be called automatically whenever the user enters the view, scrolls or does anything else. The OS takes more or less care of this...).
来源:https://stackoverflow.com/questions/55675885/autosizing-cells-in-uicollectionview-all-in-code