it-swarm.com.ru

UICollectionView, ячейки полной ширины, позволяют динамическую высоту авторазметки?

Вот довольно простой вопрос о текущей разработке iOS, на который я никогда не нашел хорошего ответа.


В (скажем) вертикальной UICollectionView,

Можно ли иметь ячейки полной ширины , но разрешить динамическую высоту для автоматического определения?

(Если вы знакомы с iOS «динамическая высота», то есть ячейка имеет несколько, скажем, текстовых представлений, которые могут быть любой длины, или изображения, которые могут иметь разные размеры, поэтому в конечном итоге каждая ячейка имеет совершенно другую высоту.)

Если так, как?

Мне кажется, что это, пожалуй, «самый важный вопрос в iOS без действительно хорошего ответа».

51
Fattie

В Swift 4.2 и iOS 12 вы можете создать подкласс UICollectionViewFlowLayout и установить для его свойства estimatedItemSize значение UICollectionViewFlowLayoutAutomaticSize (это говорит системе о том, что вы хотите иметь дело с автоматическим изменением размера UICollectionViewCells). Затем вам придется переопределить layoutAttributesForElements(in:) и layoutAttributesForItem(at:), чтобы установить ширину ячеек. Наконец, вам придется переопределить метод preferredLayoutAttributesFitting(_:) своих ячеек и вычислить их сжатую высоту фитинга.


Следующий полный код показывает, как отображать многострочное UILabel внутри полноразмерной UIcollectionViewCell (ограничено безопасной областью UICollectionView и вставками UICollectionViewFlowLayout):

CollectionViewController.Swift

import UIKit

class CollectionViewController: UICollectionViewController {

    let items = [
        "Lorem ipsum.",
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.",
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",
        "Lorem ipsum dolor sit amet.",
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam."
    ]
    let columnLayout = FlowLayout()

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.alwaysBounceVertical = true
        collectionView.collectionViewLayout = columnLayout
        collectionView.contentInsetAdjustmentBehavior = .always
        collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
    }

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

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
        cell.label.text = items[indexPath.row]
        return cell
    }

}

FlowLayout.Swift

import UIKit

class FlowLayout: UICollectionViewFlowLayout {

    override init() {
        super.init()

        self.minimumInteritemSpacing = 10
        self.minimumLineSpacing = 10
        self.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        estimatedItemSize = UICollectionViewFlowLayout.automaticSize
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath) else { return nil }
        guard let collectionView = collectionView else { return nil }
        layoutAttributes.bounds.size.width = collectionView.safeAreaLayoutGuide.layoutFrame.width - sectionInset.left - sectionInset.right
        return layoutAttributes
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let superLayoutAttributes = super.layoutAttributesForElements(in: rect) else { return nil }
        guard scrollDirection == .vertical else { return superLayoutAttributes }

        let computedAttributes = superLayoutAttributes.compactMap { layoutAttribute in
            return layoutAttribute.representedElementCategory == .cell ? layoutAttributesForItem(at: layoutAttribute.indexPath) : layoutAttribute
        }
        return computedAttributes
    }

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }

}

Cell.Swift

import UIKit

class Cell: UICollectionViewCell {

    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)

        label.numberOfLines = 0
        backgroundColor = .orange
        contentView.addSubview(label)

        label.translatesAutoresizingMaskIntoConstraints = false
        label.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor).isActive = true
        label.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor).isActive = true
        label.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor).isActive = true
        label.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        layoutIfNeeded()
        let layoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
        layoutAttributes.bounds.size = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
        return layoutAttributes
    }

}

Вот несколько альтернативных реализаций для preferredLayoutAttributesFitting(_:):

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    layoutIfNeeded()
    label.preferredMaxLayoutWidth = label.bounds.size.width
    layoutAttributes.bounds.size.height = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
    return layoutAttributes
}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    label.preferredMaxLayoutWidth = layoutAttributes.size.width - contentView.layoutMargins.left - contentView.layoutMargins.left
    layoutAttributes.bounds.size.height = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
    return layoutAttributes
}

Ожидаемое отображение:

 enter image description here

23
Imanou Petit

Проблема

Вы ищете автоматическую высоту, а также хотите иметь полную ширину, Невозможно использовать оба параметра с помощью UICollectionViewFlowLayoutAutomaticSize. Почему вы не берете UITableVIew вместо UICollectionView, в котором вы можете просто вернуться в heightForRow as UITableViewAutomaticDimension будет управляться автоматически. В любом случае вы хотите использовать UICollectionView, поэтому ниже приведено решение для вас.

Решение

Шаг I: : Рассчитать ожидаемую высоту ячейки

1. : Если у вас есть только UILabel в ColllectionViewCell, чем установить numberOfLines=0 и который рассчитал ожидаемую высоту UIlable, передайте все три параметра

 func heightForLable(text:String, font:UIFont, width:CGFloat) -> CGFloat{
  // pass string ,font , LableWidth  
        let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
        label.numberOfLines = 0
        label.lineBreakMode = NSLineBreakMode.byWordWrapping
        label.font = font
        label.text = text
        label.sizeToFit()

        return label.frame.height
    }

2. : если ваша ColllectionViewCell содержит только UIImageView и если она должна быть динамической по высоте, вам нужно получить высоту UIImage(ваша UIImageView должна иметь ограничения AspectRatio)

// this will give you the height of your Image
let heightInPoints = image.size.height
let heightInPixels = heightInPoints * image.scale

3. , если он содержит как рассчитанные их высоты, так и сложить их вместе.

ШАГ-II : вернуть размер CollectionViewCell

1. Добавьте UICollectionViewDelegateFlowLayout делегата в ваш viewController 

2. Реализовать метод делегата

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

// This is Just for example , for the scenario Step-I -> 1 
let yourWidthOfLable=self.view.size.width
let font = UIFont(name: "Helvetica", size: 20.0)

var expectedHeight = heightForLable(array[indePath.row], font: font, width:yourWidthOfLable )


 return CGSize(width: view.frame.width, height: expectedHeight)
}

я надеюсь, что это поможет вам.

22
Dhiru

Есть несколько способов решить эту проблему.

Один из способов заключается в том, что вы можете задать макет потока представления коллекции для предполагаемого размера и расчета размера ячейки.

Примечание. Как упоминалось в комментариях ниже, начиная с iOS 10 вам больше не нужно указывать и оценивать размер для вызова вызова в ячейки func preferredLayoutAttributesFitting(_ layoutAttributes:). Ранее (iOS 9) требовал от вас указать приблизительный размер, если вы хотите запросить ячейки prefferedLayoutAttributes.

(при условии, что вы используете раскадровки и представление коллекции подключено через IB)

override func viewDidLoad() {
    super.viewDidLoad()
    let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout
    layout?.estimatedItemSize = CGSize(width: 375, height: 200) // your average cell size
}

Для простых клеток этого обычно будет достаточно. Если размер все еще неправильный, в ячейке представления коллекции вы можете переопределить func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes, что даст вам более точный контроль размера ячейки. Примечание: вам все равно нужно будет указать макет потока в предполагаемом размере .

Затем переопределите func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes, чтобы вернуть правильный размер.

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    let autoLayoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
    let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)
    let autoLayoutSize = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityDefaultLow)
    let autoLayoutFrame = CGRect(Origin: autoLayoutAttributes.frame.Origin, size: autoLayoutSize)
    autoLayoutAttributes.frame = autoLayoutFrame
    return autoLayoutAttributes
}

Кроме того, вместо этого вы можете использовать ячейку для определения размера ячейки в переменной UICollectionViewDelegateFlowLayout

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let width = collectionView.frame.width
    let size = CGSize(width: width, height: 0)
    // assuming your collection view cell is a nib
    // you may also instantiate a instance of our cell from a storyboard
    let sizingCell = UINib(nibName: "yourNibName", bundle: nil).instantiate(withOwner: nil, options: nil).first as! YourCollectionViewCell
    sizingCell.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    sizingCell.frame.size = size
    sizingCell.configure(with: object[indexPath.row]) // what ever method configures your cell
    return sizingCell.contentView.systemLayoutSizeFitting(size, withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityDefaultLow)
}

Хотя это не идеальные готовые примеры производства, они должны помочь вам начать работу в правильном направлении. Я не могу сказать, что это лучшая практика, но это работает для меня, даже с довольно сложными ячейками, содержащими несколько меток, которые могут или не могут переноситься на несколько строк.

15
Eric Murphey

Я нашел довольно простое решение для этой проблемы: внутри моего CollectionViewCell я получил UIView (), который на самом деле просто фон. Чтобы получить полную ширину, я просто установил следующие якоря

bgView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.size.width - 30).isActive = true // 30 is my added up left and right Inset
bgView.topAnchor.constraint(equalTo: topAnchor).isActive = true
bgView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
bgView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
bgView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true

«Волшебство» происходит в первой строке. Я устанавливаю widthAnchor динамически к ширине экрана. Также важно вычесть вставки из вашего CollectionView. В противном случае ячейка не будет отображаться. Если вы не хотите иметь такой фоновый вид, просто сделайте его невидимым.

FlowLayout использует следующие настройки

layout.itemSize = UICollectionViewFlowLayoutAutomaticSize
layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize

Результатом является ячейка с полной шириной и динамической высотой.

 enter image description here

10
inf1783

Вы должны добавить ограничение ширины в CollectionViewCell

class SelfSizingCell: UICollectionViewCell {

  override func awakeFromNib() {
      super.awakeFromNib()
      contentView.translatesAutoresizingMaskIntoConstraints = false
      contentView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
  }
}
3
Mecid

Лично я нашел лучшие способы иметь UICollectionView, где AutoLayout определяет размер, в то время как каждая ячейка может иметь различный размер, - это реализовать функцию UICollectionViewDelegateFlowLayout sizeForItemAtIndexPath, используя фактическую ячейку для измерения размера. 

Я говорил об этом в одном из моих постов в блоге: https://janthielemann.de/ios-development/self-sizing-uicollectionviewcells-ios-10-Swift-3/

Надеюсь, этот поможет вам достичь того, что вы хотите. Я не уверен на 100%, но я верю, что в отличие от UITableView, где вы можете иметь полностью автоматическую высоту ячеек, используя AutoLayout в сочетании с

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44

UICollectionView не имеет такого способа позволить AutoLayout определять размер, потому что UICollectionViewCell не обязательно заполняет всю ширину экрана.

Но вот к вам вопрос: если вам нужны ячейки полной ширины экрана, зачем вам вообще использовать UICollectionView поверх старого доброго UITableView, который поставляется с ячейками авторазмера? 

3
xxtesaxx

Не уверен, что это квалифицируется как «действительно хороший ответ», но это то, что я использую для достижения этой цели. Мой макет потока горизонтальный, и я пытаюсь настроить ширину с помощью автоматического размещения, так что это похоже на вашу ситуацию.

extension PhotoAlbumVC: UICollectionViewDelegateFlowLayout {
  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    // My height is static, but it could use the screen size if you wanted
    return CGSize(width: collectionView.frame.width - sectionInsets.left - sectionInsets.right, height: 60) 
  }
}

Затем в контроллере представления, где ограничение autolayout изменяется, я запускаю NSNotification.

NotificationCenter.default.post(name: NSNotification.Name("constraintMoved"), object: self, userInfo: nil)

В моем подклассе UICollectionView я слушаю это уведомление:

// viewDidLoad
NotificationCenter.default.addObserver(self, selector: #selector(handleConstraintNotification(notification:)), name: NSNotification.Name("constraintMoved"), object: nil)

и сделать недействительным макет:

func handleConstraintNotification(notification: Notification) {
    self.collectionView?.collectionViewLayout.invalidateLayout()
}

Это приводит к повторному вызову sizeForItemAt с использованием нового размера представления коллекции. В вашем случае он должен иметь возможность обновляться с учетом новых ограничений, доступных в макете.

2
Mark Suman

Согласно моему комментарию к ответу Эрика, мое решение очень похоже на его, но мне пришлось добавить ограничение в Предпочитаемый размер для ..., чтобы ограничиться фиксированным измерением. 

    override func systemLayoutSizeFitting(
        _ targetSize: CGSize, withHorizontalFittingPriority
        horizontalFittingPriority: UILayoutPriority,
        verticalFittingPriority: UILayoutPriority) -> CGSize {

        width.constant = targetSize.width

        let size = contentView.systemLayoutSizeFitting(
            CGSize(width: targetSize.width, height: 1),
            withHorizontalFittingPriority: .required,
            verticalFittingPriority: verticalFittingPriority)

        print("\(#function) \(#line) \(targetSize) -> \(size)")
        return size
    }

Этот вопрос имеет ряд дубликатов, я подробно ответил на него здесь: https://stackoverflow.com/a/47424930/2171044 , и предоставили рабочий пример приложения здесь.

1
Chris Conover
  1. Установите estimatedItemSize вашего макета потока:

    collectionViewLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
    
  2. Определите ограничение ширины в ячейке и установите его равным ширине суперпредставления:

    class CollectionViewCell: UICollectionViewCell {
        private var widthConstraint: NSLayoutConstraint?
    
        ...
    
        override init(frame: CGRect) {
            ...
            // Create width constraint to set it later.
            widthConstraint = contentView.widthAnchor.constraint(equalToConstant: 0)
        }
    
        override func updateConstraints() {
            // Set width constraint to superview's width.
            widthConstraint?.constant = superview?.bounds.width ?? 0
            widthConstraint?.isActive = true
            super.updateConstraints()
        }
    
        ...
    }
    

Полный пример: https://Gist.github.com/madyanov/246217ad2628ba6a870114131a27c55c

Протестировано на iOS 11.

1
Roman

Начиная с iOS 10, у нас появился новый API для макета потока.

Все, что вам нужно сделать, это установить свой flowLayout.estimatedItemSize в новую константу, UICollectionViewFlowLayoutAutomaticSize.

Источник: http://asciiwwdc.com/2016/sessions/219

1
Arpit Dongre

На вашей viewDidLayoutSubviews установите estimatedItemSize на полную ширину (макет ссылается на объект UICollectionViewFlowLayout):

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
    return CGSize(width: collectionView.bounds.size.width, height: 120)
}

В своей ячейке убедитесь, что ваши ограничения касаются как верхней, так и нижней части ячейки (следующий код использует картографию для упрощения установки ограничений, но вы можете сделать это с помощью NSLayoutConstraint или IB, если хотите):

constrain(self, nameLabel, valueLabel) { view, name, value in
        name.top == view.top + 10
        name.left == view.left
        name.bottom == view.bottom - 10
        value.right == view.right
        value.centerY == view.centerY
    }

Вуаля, ваши клетки теперь будут расти автоматически!

1
Raphael

Ни одно из решений не сработало для меня, так как мне нужна динамическая ширина для адаптации к ширине iPhone.

    class CustomLayoutFlow: UICollectionViewFlowLayout {
        override init() {
            super.init()
            minimumInteritemSpacing = 1 ; minimumLineSpacing = 1 ; scrollDirection = .horizontal
        }

        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            minimumInteritemSpacing = 1 ; minimumLineSpacing = 1 ; scrollDirection = .horizontal
        }

        override var itemSize: CGSize {
            set { }
            get {
                let width = (self.collectionView?.frame.width)!
                let height = (self.collectionView?.frame.height)!
                return CGSize(width: width, height: height)
            }
        }
    }

    class TextCollectionViewCell: UICollectionViewCell {
        @IBOutlet weak var textView: UITextView!

        override func prepareForReuse() {
            super.prepareForReuse()
        }
    }




    class IntroViewController: UIViewController, UITextViewDelegate, UICollectionViewDataSource, UICollectionViewDelegate, UINavigationControllerDelegate {
        @IBOutlet weak var collectionViewTopDistanceConstraint: NSLayoutConstraint!
        @IBOutlet weak var collectionViewTopDistanceConstraint: NSLayoutConstraint!
        @IBOutlet weak var collectionView: UICollectionView!
        var collectionViewLayout: CustomLayoutFlow!

        override func viewDidLoad() {
            super.viewDidLoad()

            self.collectionViewLayout = CustomLayoutFlow()
            self.collectionView.collectionViewLayout = self.collectionViewLayout
        }

        override func viewWillLayoutSubviews() {
            self.collectionViewTopDistanceConstraint.constant = UIScreen.main.bounds.height > 736 ? 94 : 70

            self.view.layoutIfNeeded()
        }
    }
0
iOS Flow