Frage UICollectionView, Zellen mit voller Breite, ermöglichen die automatische Erstellung dynamischer Höhe?


Hier ist eine ziemlich grundlegende Frage zur aktuellen iOS-Entwicklung, auf die ich bisher keine gute Antwort gefunden habe.


In einer (sagen wir) vertikalen Richtung UICollectionView ,

Ist es möglich zu haben? Zellen voller Breite, aber erlauben Sie, dass die dynamische Höhe durch automatisches Layout kontrolliert wird?

(Wenn Sie mit "dynamischer Höhe" von iOS noch nicht vertraut sind, dh, die Zelle hat ein paar, sagen wir, Textansichten, die eine beliebige Länge haben können oder Bilder, die unterschiedliche Größen haben können, so dass jede Zelle eine völlig andere Höhe hat.)

Wenn das so ist, wie?

Dies erscheint mir als die "wichtigste Frage in iOS ohne wirklich gute Antwort".


40
2018-05-25 18:54


Ursprung


Antworten:


Problem

Sie suchen nach automatischer Höhe und möchten auch volle Breite haben, Es ist nicht möglich, beides zu verwenden UICollectionViewFlowLayoutAutomaticSize. warum nimmst du nicht UITableVIew Anstatt von UICollectionView in dem du einfach in die zurückkehren kannst heightForRow wie UITableViewAutomaticDimension Es wird automatisch verwaltet. wie auch immer du es machen willst UICollectionView So unten ist die Lösung für Sie.

Lösung

Schritt I: : Berechne die erwartete Höhe von Cell

1. : Wenn du nur hast UILabel  im ColllectionViewCell als setze die numberOfLines=0 und das berechnete die erwartete Höhe von UIlable   Überhole alle drei Parameter

 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. : wenn dein ColllectionViewCell enthält nur UIImageView und wenn es in der Höhe dynamisch sein soll, müssen Sie die Höhe von erreichen UIImage  (Ihre UIImageView haben müssen AspectRatio Einschränkungen)

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

3.  wenn es beides enthält, als ihre Höhe berechnet und addiert sie zusammen.

Schritt-II : Gebe die Größe von zurück CollectionViewCell

1. Hinzufügen UICollectionViewDelegateFlowLayout Delegieren Sie in Ihrer ViewController

2.  Implementieren Sie die Delegate-Methode

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)
}

Ich hoffe, dass dir das weiterhilft.


20
2018-06-04 08:12



Es gibt mehrere Möglichkeiten, wie Sie dieses Problem angehen können.

Eine Möglichkeit besteht darin, dem Sammlungsansichtsflusslayout eine geschätzte Größe zu geben und die Zellengröße zu berechnen.

Hinweis: Wie in den Kommentaren unten erwähnt, müssen Sie ab iOS 10 nicht mehr die geschätzte Größe angeben, um den Anruf an Zellen auszulösen  func preferredLayoutAttributesFitting(_ layoutAttributes:). Zuvor (iOS 9) müssten Sie eine geschätzte Größe angeben, wenn Sie eine Zelle mit prefferedLayoutAttributes abfragen möchten.

(Angenommen, Sie verwenden Storyboards und die Sammlungsansicht ist über IB verbunden)

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

Für einfache Zellen reicht das in der Regel aus. Wenn die Größe immer noch falsch ist, können Sie sie in der Sammlungsansichtszelle überschreiben func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes, die Ihnen eine feinere Kontrolle über die Zellgröße geben. Hinweis: Sie müssen dem Flusslayout weiterhin eine geschätzte Größe zuweisen.

Dann überschreiben func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes um die richtige Größe zurückzugeben.

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
}

Alternativ können Sie stattdessen eine Größenmesszelle verwenden, um die Größe der Zelle in der Zelle zu berechnen 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)
}

Diese sind zwar keine perfekten produktionsfertigen Beispiele, aber sie sollten Sie in die richtige Richtung bringen. Ich kann nicht sagen, dass dies die beste Vorgehensweise ist, aber das funktioniert für mich, sogar mit ziemlich komplexen Zellen, die mehrere Labels enthalten, die sich möglicherweise in mehrere Zeilen umwandeln.


10
2018-05-30 14:59



Ich fand eine ziemlich einfache Lösung für dieses Problem: Innerhalb meiner CollectionViewCell habe ich eine UIView (), die eigentlich nur ein Hintergrund ist. Um die volle Breite zu erhalten, setze ich einfach die folgenden Anker

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

Die "Magie" passiert in der ersten Zeile. Ich habe den widthAnchor dynamisch auf die Breite des Bildschirms eingestellt. Wichtig ist auch, dass Sie die Einfügungen von CollectionView subtrahieren. Andernfalls wird die Zelle nicht angezeigt. Wenn Sie eine solche Hintergrundansicht nicht haben wollen, machen Sie sie einfach unsichtbar.

Das FlowLayout verwendet die folgenden Einstellungen

layout.itemSize = UICollectionViewFlowLayoutAutomaticSize
layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize

Ergebnis ist eine Zelle mit voller Breite und dynamischer Höhe.

enter image description here


8
2018-02-08 08:58



Mit Swift 4 und iOS 11 können Sie Unterklassen erstellen UICollectionViewFlowLayout und setze es estimatedItemSize Eigentum zu UICollectionViewFlowLayoutAutomaticSize (Dies teilt dem System mit, dass Sie mit der Autoresierung arbeiten möchten UICollectionViewCells). Sie müssen dann überschreiben layoutAttributesForElements(in:) und layoutAttributesForItem(at:) um die Zellenbreite einzustellen. Zuletzt müssen Sie Ihre Zellen überschreiben preferredLayoutAttributesFitting(_:) Methode und berechnen ihre komprimierte Einbauhöhe.


Der folgende vollständige Code zeigt, wie Multiline angezeigt wird UILabel innen in voller Breite UIcollectionViewCell (eingeschränkt durch UICollectionView's sicherer Bereich und UICollectionViewFlowLayout's Einsätze):

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?.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 = UICollectionViewFlowLayoutAutomaticSize
    }

    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
    }

}

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.height = systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
        return layoutAttributes
    }

}

Hier sind einige alternative Implementierungen für preferredLayoutAttributesFitting(_:):

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    layoutIfNeeded()
    let layoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
    layoutAttributes.bounds.size = systemLayoutSizeFitting(UILayoutFittingCompressedSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
    return layoutAttributes
}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    layoutIfNeeded()
    label.preferredMaxLayoutWidth = label.bounds.size.width
    layoutAttributes.bounds.size.height = systemLayoutSizeFitting(UILayoutFittingCompressedSize).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(UILayoutFittingCompressedSize).height
    return layoutAttributes
}

Erwartete Anzeige:

enter image description here


5
2017-07-08 12:11



Persönlich fand ich die besten Möglichkeiten, eine UICollectionView zu haben, wo AutoLayout die Größe bestimmt, während jede Zelle eine andere Größe haben kann, um die UICollectionViewDelegateFlowLayout sizeForItemAtIndexPath Funktion zu implementieren, während eine tatsächliche Zelle verwendet wird, um die Größe zu messen.

Ich habe darüber in einem meiner Blogposts gesprochen: https://janthielemann.de/ios-development/self-sizing-uicollectionviewcells-ios-10-swift-3/

Hoffentlich wird dieser Ihnen helfen zu erreichen, was Sie wollen. Ich bin nicht 100% sicher, aber ich glaube im Gegensatz zu UITableView, wo Sie tatsächlich eine vollautomatische Höhe von Zellen haben können, indem Sie AutoLayout inconjunction mit verwenden

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44

UICollectionView bietet keine Möglichkeit, die Größe von AutoLayout zu bestimmen, da UICollectionViewCell nicht unbedingt die gesamte Breite des Bildschirms ausfüllt.

Aber hier ist eine Frage an Sie: Wenn Sie Zellen mit voller Bildschirmbreite benötigen, warum verwenden Sie überhaupt die UICollectionView über eine gute alte UITableView, die mit den Auto-Sizing-Zellen geliefert wird?


3
2018-06-03 17:12



Ab iOS 10 haben wir dafür eine neue API im Flow-Layout.

Alles, was Sie tun müssen, ist Ihre flowLayout.estimatedItemSize zu einer neuen Konstante, UICollectionViewFlowLayoutAutomaticSize.

Quelle: http://asciiwwdc.com/2016/sessions/219


3
2018-06-03 17:25



Ich bin mir nicht sicher, ob dies eine "wirklich gute Antwort" ist, aber ich verwende das, um dies zu erreichen. Mein Flow-Layout ist horizontal, und ich versuche, die Breite mit Auto-Layout anzupassen, also ist es ähnlich wie in Ihrer Situation.

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) 
  }
}

Dann wird im View-Controller, in dem die Autolayout-Bedingung geändert wird, eine NSNotification ausgelöst.

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

In meiner Unterklasse UICollectionView höre ich auf diese Benachrichtigung:

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

und das Layout ungültig machen:

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

Dies bewirkt sizeForItemAtmit der neuen Größe der Sammlungsansicht erneut aufgerufen werden. In Ihrem Fall sollte es in der Lage sein, die neuen Einschränkungen, die im Layout verfügbar sind, zu aktualisieren.


2
2018-05-31 20:57



Sie müssen der CollectionViewCell eine Breiteneinschränkung hinzufügen

class SelfSizingCell: UICollectionViewCell {

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

2
2018-04-14 21:34



Auf Ihrem viewDidLayoutSubviewsStellen Sie die estimatedItemSize auf volle Breite (Layout bezieht sich auf das Objekt UICollectionViewFlowLayout):

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

Stellen Sie in Ihrer Zelle sicher, dass Ihre Beschränkungen sowohl den oberen als auch den unteren Rand der Zelle berühren (der folgende Code verwendet Cartography, um das Festlegen der Einschränkungen zu vereinfachen, aber Sie können dies mit NSLayoutConstraint oder IB tun, wenn Sie möchten):

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
    }

Voila, deine Zellen werden jetzt automatisch in der Höhe wachsen!


1
2017-09-22 20:58