it-swarm.com.de

UICollectionView in einer UITableViewCell - dynamische Höhe?

Auf einem unserer Anwendungsbildschirme müssen wir einen UICollectionView innerhalb eines UITableViewCell platzieren. Dieses UICollectionView hat eine dynamische Anzahl von Elementen, was zu einer Höhe führt, die ebenfalls dynamisch berechnet werden muss. Ich habe jedoch Probleme bei der Berechnung der Höhe des eingebetteten UICollectionView.

Unser übergeordnetes UIViewController wurde in Storyboards erstellt und verwendet das automatische Layout. Aber ich weiß nicht, wie ich die Höhe von UITableViewCell basierend auf der Höhe von UICollectionView dynamisch erhöhen kann.

Kann jemand ein paar Tipps oder Ratschläge geben, wie das zu erreichen ist?

80
Shadowman

Die richtige Antwort ist YES, Sie CAN tun dies.

Ich bin vor einigen Wochen auf dieses Problem gestoßen. Es ist tatsächlich einfacher als Sie vielleicht denken. Legen Sie Ihre Zellen in NIBs (oder Storyboards) ein und fixieren Sie sie, damit das automatische Layout alle Aufgaben erledigen kann

Gegeben der folgenden Struktur:

Tabellenansicht 

TableViewCell

CollectionView

CollectionViewCell

CollectionViewCell

CollectionViewCell

[... variable Anzahl von Zellen oder unterschiedliche Zellgrößen] 

Die Lösung besteht darin, dem automatischen Layout mitzuteilen, dass zunächst die collectionViewCell-Größen berechnet werden, dann die Auflistungsansicht contentSize und die Größe der Zelle. Dies ist die Methode UIView, die "die Magie ausübt":

-(void)systemLayoutSizeFittingSize:(CGSize)targetSize 
     withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority 
           verticalFittingPriority:(UILayoutPriority)verticalFittingPriority

Sie müssen hier die Größe der TableViewCell einstellen, die in Ihrem Fall die contentSize der CollectionView ist.

CollectionViewCell

Bei CollectionViewCell müssen Sie der Zelle bei jeder Änderung des Modells das Layout zuweisen (z. B .: Sie setzen ein UILabel mit einem Text, dann muss die Zelle erneut angeordnet werden).

- (void)bindWithModel:(id)model {
    // Do whatever you may need to bind with your data and 
    // tell the collection view cell's contentView to resize
    [self.contentView setNeedsLayout];
}
// Other stuff here...

TableViewCell

Die TableViewCell macht die Magie. Es verfügt über einen Auslass für Ihre collectionView. Ermöglicht das automatische Layout für collectionView-Zellen mithilfe von geschätzteItemSize der UICollectionViewFlowLayout.

Der Trick ist dann , um die Größe Ihrer tableView-Zelle in der Methode systemLayoutSizeFittingSize ... festzulegen. (HINWEIS: iOS8 oder höher)

HINWEIS: Ich habe versucht, die Height-Methode der Delegat-Zelle der tableView -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath. Zu verwenden, aber es ist zu spät, damit das Auto-Layout-System die CollectionView contentSize berechnet, und manchmal finden Sie falsche Zellen in der Größe.

@implementation TableCell

- (void)awakeFromNib {
    [super awakeFromNib];
    UICollectionViewFlowLayout *flow = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;

    // Configure the collectionView
    flow.minimumInteritemSpacing = ...;

    // This enables the magic of auto layout. 
    // Setting estimatedItemSize different to CGSizeZero 
    // on flow Layout enables auto layout for collectionView cells.
    // https://developer.Apple.com/videos/play/wwdc2014-226/
    flow.estimatedItemSize = CGSizeMake(1, 1);

    // Disable the scroll on your collection view
    // to avoid running into multiple scroll issues.
    [self.collectionView setScrollEnabled:NO];
}

- (void)bindWithModel:(id)model {
    // Do your stuff here to configure the tableViewCell
    // Tell the cell to redraw its contentView        
    [self.contentView layoutIfNeeded];
}

// THIS IS THE MOST IMPORTANT METHOD
//
// This method tells the auto layout
// You cannot calculate the collectionView content size in any other place, 
// because you run into race condition issues.
// NOTE: Works for iOS 8 or later
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority {

    // With autolayout enabled on collection view's cells we need to force a collection view relayout with the shown size (width)
    self.collectionView.frame = CGRectMake(0, 0, targetSize.width, MAXFLOAT);
    [self.collectionView layoutIfNeeded];

    // If the cell's size has to be exactly the content 
    // Size of the collection View, just return the
    // collectionViewLayout's collectionViewContentSize.

    return [self.collectionView.collectionViewLayout collectionViewContentSize];
}

// Other stuff here...

@end

TableViewController

Denken Sie daran, das automatische Layoutsystem für die tableView-Zellen in Ihrem TableViewController zu aktivieren:

- (void)viewDidLoad {
    [super viewDidLoad];

    // Enable automatic row auto layout calculations        
    self.tableView.rowHeight = UITableViewAutomaticDimension;
    // Set the estimatedRowHeight to a non-0 value to enable auto layout.
    self.tableView.estimatedRowHeight = 10;

}

CREDIT: @rbarbera half das zu klären 

91
Pablo Romeu

Ich denke, mein Weg ist viel einfacher, als von @PabloRomeu vorgeschlagen.

Schritt 1. Erstellen Sie einen Auslass von UICollectionView nach UITableViewCell subclass, wo UICollectionView platziert wird. Der Name wird collectionView sein.

Schritt 2. Fügen Sie in IB die UICollectionView-Höhenbeschränkung hinzu und erstellen Sie auch UITableViewCell subclass einen Ausgang. Der Name wird collectionViewHeight sein.

Schritt 3. In tableView:cellForRowAtIndexPath: Code hinzufügen:

// deque a cell
cell.frame = tableView.bounds;
[cell layoutIfNeeded];
[cell.collectionView reloadData];
cell.collectionViewHeight.constant = cell.collectionView.collectionViewLayout.collectionViewContentSize.height;
61
Igor

Sowohl die Tabellensichten als auch die Auflistungsansichten sind UIScrollView-Unterklassen und möchten daher nicht in eine andere Bildlaufansicht eingebettet werden, da sie versuchen, Inhaltsgrößen zu berechnen, Zellen wiederverwenden usw.

Ich empfehle Ihnen, nur eine Sammlungsansicht für alle Ihre Zwecke zu verwenden.

Sie können es in Abschnitte unterteilen und das Layout einiger Abschnitte als Tabellenansicht und andere als Sammlungsansicht "behandeln". Schließlich können Sie mit einer Sammlungsansicht nichts erreichen, die Sie mit einer Tabellenansicht erreichen können.

Wenn Sie ein grundlegendes Rasterlayout für "Teile" der Sammlungsansicht haben, können Sie auch reguläre Tabellenzellen verwenden. Wenn Sie keine Unterstützung für iOS 5 benötigen, sollten Sie jedoch besser Sammlungsansichten verwenden.

16
Rivera

Die Antwort von Pablo Romeu oben ( https://stackoverflow.com/a/33364092/2704206 ) half mir sehr bei meinem Problem. Ich musste jedoch ein paar Dinge anders machen, damit dies für mein Problem funktioniert. Zuerst musste ich layoutIfNeeded() nicht so oft anrufen. Ich musste es nur für die Variable collectionView in der Funktion systemLayoutSizeFitting aufrufen. 

Zweitens hatte ich in meiner Sammlungsansicht in der Tabellenzellenzelle Einschränkungen für das automatische Layout, um etwas Auffüllung zu erhalten. Ich musste also die vorderen und nachlaufenden Ränder vom targetSize.width subtrahieren, wenn Sie die Breite des collectionView.frame einstellen. Ich musste auch die oberen und unteren Ränder zum Rückgabewert CGSize height hinzufügen. 

Um diese Constraint-Konstanten zu erhalten, hatte ich die Möglichkeit, entweder Auslässe für die Constraints zu erstellen, deren Konstanten hart zu codieren oder anhand eines Bezeichners nachzuschlagen. Ich entschied mich für die dritte Option, um meine benutzerdefinierte Tabellenzellenklasse leicht wiederverwendbar zu machen. Am Ende war dies alles, was ich brauchte, um es zum Laufen zu bringen:

class CollectionTableViewCell: UITableViewCell {

    // MARK: -
    // MARK: Properties
    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            collectionViewLayout?.estimatedItemSize = CGSize(width: 1, height: 1)
            selectionStyle = .none
        }
    }

    var collectionViewLayout: UICollectionViewFlowLayout? {
        return collectionView.collectionViewLayout as? UICollectionViewFlowLayout
    }

    // MARK: -
    // MARK: UIView functions
    override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        collectionView.layoutIfNeeded()

        let topConstraintConstant = contentView.constraint(byIdentifier: "topAnchor")?.constant ?? 0
        let bottomConstraintConstant = contentView.constraint(byIdentifier: "bottomAnchor")?.constant ?? 0
        let trailingConstraintConstant = contentView.constraint(byIdentifier: "trailingAnchor")?.constant ?? 0
        let leadingConstraintConstant = contentView.constraint(byIdentifier: "leadingAnchor")?.constant ?? 0

        collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width - trailingConstraintConstant - leadingConstraintConstant, height: 1)

        let size = collectionView.collectionViewLayout.collectionViewContentSize
        let newSize = CGSize(width: size.width, height: size.height + topConstraintConstant + bottomConstraintConstant)
        return newSize
    }
}

Als Hilfsfunktion zum Abrufen einer Einschränkung nach Bezeichner füge ich die folgende Erweiterung hinzu:

extension UIView {
    func constraint(byIdentifier identifier: String) -> NSLayoutConstraint? {
        return constraints.first(where: { $0.identifier == identifier })
    }
}

ANMERKUNG: Sie müssen den Bezeichner für diese Einschränkungen in Ihrem Storyboard oder an dem Ort festlegen, an dem sie erstellt werden. Wenn sie keine 0-Konstante haben, spielt es keine Rolle. Wie in Pablos Antwort müssen Sie auch UICollectionViewFlowLayout als Layout für Ihre Sammlungsansicht verwenden. Stellen Sie schließlich sicher, dass Sie das collectionView IBOutlet mit Ihrem Storyboard verknüpfen.

Mit der oben abgebildeten benutzerdefinierten Tabellensichtzelle kann ich sie jetzt in jede andere Tabellensichtzelle unterteilen, die eine Auflistungsansicht benötigt, und die Protokolle UICollectionViewDelegateFlowLayout und UICollectionViewDataSource implementieren. Hoffe, das ist für jemand anderen hilfreich!

3
jmad8

Eine Alternative zu Pablo Romeus Lösung besteht darin, UICollectionView selbst anzupassen, anstatt die Arbeit in der Tabellensichtzelle zu erledigen. 

Das zugrunde liegende Problem ist, dass eine Sammlungsansicht standardmäßig keine intrinsische Größe hat und daher das automatische Layout nicht über die zu verwendenden Dimensionen informieren kann. Sie können dies beheben, indem Sie eine benutzerdefinierte Unterklasse erstellen, die eine nützliche intrinsische Größe zurückgibt.

Erstellen Sie eine Unterklasse von UICollectionView und überschreiben Sie die folgenden Methoden

override func intrinsicContentSize() -> CGSize {
    self.layoutIfNeeded()

    var size = super.contentSize
    if size.width == 0 || size.height == 0 {
        // return a default size
        size = CGSize(width: 600, height:44)
    }

    return size
 }

override func reloadData() {
    super.reloadData()
    self.layoutIfNeeded()
    self.invalidateIntrinsicContentSize()
}

(Sie sollten auch die verwandten Methoden überschreiben: reloadSections, reloadItemsAtIndexPaths auf ähnliche Weise wie reloadData ()).

Der Aufruf von layoutIfNeeded zwingt die Sammlungsansicht zur Neuberechnung der Inhaltsgröße, die dann als neue intrinsische Größe verwendet werden kann.

Außerdem müssen Sie Änderungen an der Ansichtsgröße (z. B. bei Gerätedrehung) in der Tabellensichtsteuerung explizit behandeln

    override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
{
    super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
    dispatch_async(dispatch_get_main_queue()) {
        self.tableView.reloadData()
    }
}
3
Dale

Vielleicht ist meine Variante nützlich; Ich habe mich in den letzten zwei Stunden für diese Aufgabe entschieden. Ich behaupte nicht, dass es zu 100% korrekt oder optimal ist, aber meine Fähigkeiten sind noch sehr klein und ich würde gerne Kommentare von Experten hören. Vielen Dank. Ein wichtiger Hinweis: Dies funktioniert für statische Tabellen - es wird in meiner aktuellen Arbeit angegeben. Alles, was ich verwende, ist viewWillLayoutSubviews von tableView. Und noch ein bisschen mehr.

private var iconsCellHeight: CGFloat = 500 

func updateTable(table: UITableView, withDuration duration: NSTimeInterval) {
    UIView.animateWithDuration(duration, animations: { () -> Void in
        table.beginUpdates()
        table.endUpdates()
    })
}

override func viewWillLayoutSubviews() {
    if let iconsCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 1)) as? CategoryCardIconsCell {
        let collectionViewContentHeight = iconsCell.iconsCollectionView.contentSize.height
        if collectionViewContentHeight + 17 != iconsCellHeight {
            iconsCellHeight = collectionViewContentHeight + 17
            updateTable(tableView, withDuration: 0.2)
        }
    }
}

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    switch (indexPath.section, indexPath.row) {
        case ...
        case (1,0):
            return iconsCellHeight
        default:
            return tableView.rowHeight
    }
}
  1. Ich weiß, dass sich die collectionView in der ersten Zeile des zweiten Abschnitts befindet.
  2. Die Höhe der Reihe sei 17 p. größer als seine Inhaltshöhe;
  3. iconsCellHeight ist eine Zufallszahl, wenn das Programm startet (ich weiß, dass es im Hochformat genau 392 sein muss, aber es ist nicht wichtig). Wenn der Inhalt von collectionView + 17 nicht dieser Zahl entspricht, ändern Sie den Wert. Das nächste Mal in dieser Situation ergibt die Bedingung FALSCH;
  4. Nach allem Update die tableView. In meinem Fall ist dies die Kombination von zwei Operationen (für die Aktualisierung von Zeilen in Nice);
  5. Und natürlich fügen Sie in der heightForRowAtIndexPath-Zeile eine Zeile zum Code hinzu.
2

Ich würde eine statische Methode für die Collection-Ansichtsklasse verwenden, die eine Größe basierend auf dem Inhalt zurückgibt, den sie haben wird. Verwenden Sie dann diese Methode in der Variablen heightForRowAtIndexPath, um die richtige Größe zurückzugeben.

Beachten Sie auch, dass Sie beim Einbetten dieser Art von viewControllers ein seltsames Verhalten bekommen können. Ich habe es einmal gemacht und hatte einige seltsame Speicherprobleme, die ich nie geklärt habe.

2
InkGolem

Die einfachste Herangehensweise, die ich mir bisher ausgedacht habe.

Fügen Sie dies einfach in Ihre tableviewcell-Klasse ein

override func layoutSubviews() {
    self.collectionViewOutlet.constant = self.postPoll.collectionViewLayout.collectionViewContentSize.height
}

und ändern Sie das collectionviewoutlet natürlich mit Ihrem Outlet in der Klasse der Zelle

2
Osa

Ich bekomme eine Idee von @Igor post und investiere meine Zeit dafür für mein Projekt mit Swift

Einfach vorbei in deinem

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //do dequeue stuff
    cell.frame = tableView.bounds
    cell.layoutIfNeeded()
    cell.collectionView.reloadData()
    cell.collectionView.heightAnchor.constraint(equalToConstant: cell.collectionView.collectionViewLayout.collectionViewContentSize.height)
    cell.layoutIfNeeded()
    return cell
}

Addition: Wenn Sie Ihre UICollectionView beim Laden von Zellen abgehackt sehen.

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {       
  //do dequeue stuff
  cell.layer.shouldRasterize = true
  cell.layer.rasterizationScale = UIScreen.main.scale
  return cell
}
0
Nazmul Hasan

Ich war vor kurzem mit dem gleichen Problem konfrontiert und habe fast jede Lösung in den Antworten ausprobiert. Einige von ihnen haben funktioniert und andere haben sich nicht mit dem @PabloRomeu - Ansatz befasst In der Sammlungsansicht müssen Sie die Höhe und die Höhe der Einschränkungen berechnen und das Ergebnis zurückgeben, damit das automatische Layout richtig angezeigt wird. Ich mag es nicht, die Dinge manuell in meinem Code zu berechnen. Hier ist also die Lösung, die für mich gut funktioniert hat, ohne manuelle Berechnungen in meinem Code vorzunehmen.

im cellForRow:atIndexPath der Tabellensicht mache ich Folgendes: 

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //do dequeue stuff
    //initialize the the collection view data source with the data
    cell.frame = CGRect.zero
    cell.layoutIfNeeded()
    return cell
}

Ich denke, was hier passiert, ist, dass ich die tableview-Zelle zwinge, ihre Höhe anzupassen, nachdem die Höhe der Sammlungsansicht berechnet wurde. (nach Angabe des collectionView-Datums für die Datenquelle)

Ich habe alle Antworten durchgelesen. Dies scheint allen Fällen zu dienen. 

override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        collectionView.layoutIfNeeded()
        collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width , height: 1)
        return collectionView.collectionViewLayout.collectionViewContentSize
}
0
SRP-Achiever

Die Lösung von Pablo funktionierte nicht sehr gut für mich, ich hatte merkwürdige visuelle Effekte (das collectionView wurde nicht korrekt angepasst).

Was funktionierte, war, die Höhenbeschränkung der collectionView (als NSLayoutConstraint) während layoutSubviews() an die collectionView contentSize anzupassen. Dies ist die Methode, die aufgerufen wird, wenn Autolayout auf die Zelle angewendet wird.

// Constraint on the collectionView height in the storyboard. Priority set to 999.
@IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!


// Method called by autolayout to layout the subviews (including the collectionView).
// This is triggered with 'layoutIfNeeded()', or by the viewController
// (happens between 'viewWillLayoutSubviews()' and 'viewDidLayoutSubviews()'.
override func layoutSubviews() {

    collectionViewHeightConstraint.constant = collectionView.contentSize.height
    super.layoutSubviews()
}

// Call `layoutIfNeeded()` when you update your UI from the model to trigger 'layoutSubviews()'
private func updateUI() {
    layoutIfNeeded()
}
0
Frédéric Adda