UICollectionView, celle a larghezza intera, consentire l'altezza dinamica del layout automatico?


120

In verticale UICollectionView,

È possibile avere celle a larghezza intera , ma consentire il controllo dell'altezza dinamica tramite il layout automatico ?

Questa mi sembra forse la "domanda più importante in iOS senza una risposta davvero buona".


Importante:

Si noti che nel 99% dei casi, per ottenere celle a larghezza intera + altezza dinamica del layout automatico, è sufficiente utilizzare una vista tabella. È così facile.


Allora qual è un esempio di dove hai bisogno di una visualizzazione della raccolta?

Le viste della raccolta sono incredibilmente più potenti delle viste della tabella.

Un esempio semplice dove effettivamente necessario utilizzare una visualizzazione di raccolta, per ottenere cellule larghezza + altezza autolayout dinamica:

Se animi tra due layout in una vista raccolta. Ad esempio, tra un layout di 1 e 2 colonne, quando il dispositivo ruota.

Questo è un idioma comune e normale in iOS. Purtroppo può essere raggiunto solo risolvendo il problema posto in questo QA. : - /


sì è possibile con la classe UICollectionViewDelegateFlowLayout
Sunil Prajapati

non significherebbe che potresti effettivamente utilizzare un tableView? quale funzionalità aggiuntiva sarebbe necessaria per te, per complicare troppo questo?
Catalina T.

1
Ciao @CatalinaT. , le viste di raccolta hanno molte, molte, molte funzionalità aggiuntive rispetto alle viste di tabella. Un semplice esempio è l'aggiunta della fisica. (Se non sai cosa intendo, è il modo in cui crei gli elenchi "rimbalzanti" in iOS.)
Fattie

puoi anche applicare in qualche modo la fisica (come SpringDampinge initialSpringVelocity) nella cella della tabella ..... dai un'occhiata a questa lista rimbalzante usando tableView ,,,, pastebin.com/pFRQhkrX puoi animare la TableViewCelltabella in willDisplayCell:(UITableViewCell *)cellcon il metodo delgate @Fattie puoi accettare la risposta che ha risolto il tuo problema per aiutare altri che stanno cercando lo stesso problema
Dhiru

ohk, hai provato l'elenco rimbalzante usando la visualizzazione elenco? @Fattie
Dhiru

Risposte:


124

1. Soluzione per iOS 13+

Con Swift 5.1 e iOS 13, puoi utilizzare gli oggetti del layout compositivo per risolvere il tuo problema.

Il seguente codice di esempio completo mostra come visualizzare la multilinea UILabelall'interno dell'intera larghezza UICollectionViewCell:

CollectionViewController.swift

import UIKit

class CollectionViewController: UICollectionViewController {

    let items = [
        [
            "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, quis nostrud exercitation ullamco laboris. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
            "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.",
        ],
        [
            "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.",
            "Lorem ipsum. 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.",
        ]
    ]

    override func viewDidLoad() {
        super.viewDidLoad()

        let size = NSCollectionLayoutSize(
            widthDimension: NSCollectionLayoutDimension.fractionalWidth(1),
            heightDimension: NSCollectionLayoutDimension.estimated(44)
        )
        let item = NSCollectionLayoutItem(layoutSize: size)
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitem: item, count: 1)

        let section = NSCollectionLayoutSection(group: group)
        section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
        section.interGroupSpacing = 10

        let headerFooterSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1.0),
            heightDimension: .absolute(40)
        )
        let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
            layoutSize: headerFooterSize,
            elementKind: "SectionHeaderElementKind",
            alignment: .top
        )
        section.boundarySupplementaryItems = [sectionHeader]

        let layout = UICollectionViewCompositionalLayout(section: section)
        collectionView.collectionViewLayout = layout
        collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "CustomCell")
        collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView")
    }

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return items.count
    }

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

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

    override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView", for: indexPath) as! HeaderView
        headerView.label.text = "Header"
        return headerView
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        coordinator.animate(alongsideTransition: { context in
            self.collectionView.collectionViewLayout.invalidateLayout()
        }, completion: nil)
    }

}

HeaderView.swift

import UIKit

class HeaderView: UICollectionReusableView {

    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .magenta

        addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
    }

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

}

CustomCell.swift

import UIKit

class CustomCell: 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.topAnchor).isActive = true
        label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
        label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
        label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
    }

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

}

Visualizzazione prevista:

inserisci qui la descrizione dell'immagine


2. Soluzione per iOS 11+

Con Swift 5.1 e iOS 11, puoi sottoclassare UICollectionViewFlowLayoute impostare la sua estimatedItemSizeproprietà su UICollectionViewFlowLayout.automaticSize(questo dice al sistema che vuoi gestire il ridimensionamento automatico UICollectionViewCell). Dovrai quindi sovrascrivere layoutAttributesForElements(in:)e layoutAttributesForItem(at:)per impostare la larghezza delle celle. Infine, dovrai sovrascrivere il preferredLayoutAttributesFitting(_:)metodo della tua cella e calcolarne l'altezza.

Il codice completo seguente mostra come visualizzare la multilinea UILabelall'interno dell'intera larghezza UIcollectionViewCell(vincolata UICollectionViewdall'area sicura di e UICollectionViewFlowLayoutdagli inserti di s):

CollectionViewController.swift

import UIKit

class CollectionViewController: UICollectionViewController {

    let items = [
        [
            "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, quis nostrud exercitation ullamco laboris. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
            "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.",
        ],
        [
            "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.",
            "Lorem ipsum. 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.",
        ]
    ]
    let customFlowLayout = CustomFlowLayout()

    override func viewDidLoad() {
        super.viewDidLoad()

        customFlowLayout.sectionInsetReference = .fromContentInset // .fromContentInset is default
        customFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        customFlowLayout.minimumInteritemSpacing = 10
        customFlowLayout.minimumLineSpacing = 10
        customFlowLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        customFlowLayout.headerReferenceSize = CGSize(width: 0, height: 40)

        collectionView.collectionViewLayout = customFlowLayout
        collectionView.contentInsetAdjustmentBehavior = .always
        collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "CustomCell")
        collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView")
    }

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return items.count
    }

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

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

    override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView", for: indexPath) as! HeaderView
        headerView.label.text = "Header"
        return headerView
    }

}

CustomFlowLayout.swift

import UIKit

final class CustomFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributesObjects = super.layoutAttributesForElements(in: rect)?.map{ $0.copy() } as? [UICollectionViewLayoutAttributes]
        layoutAttributesObjects?.forEach({ layoutAttributes in
            if layoutAttributes.representedElementCategory == .cell {
                if let newFrame = layoutAttributesForItem(at: layoutAttributes.indexPath)?.frame {
                    layoutAttributes.frame = newFrame
                }
            }
        })
        return layoutAttributesObjects
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let collectionView = collectionView else {
            fatalError()
        }
        guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else {
            return nil
        }

        layoutAttributes.frame.origin.x = sectionInset.left
        layoutAttributes.frame.size.width = collectionView.safeAreaLayoutGuide.layoutFrame.width - sectionInset.left - sectionInset.right
        return layoutAttributes
    }

}

HeaderView.swift

import UIKit

class HeaderView: UICollectionReusableView {

    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .magenta

        addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
    }

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

}

CustomCell.swift

import UIKit

class CustomCell: 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.topAnchor).isActive = true
        label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
        label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
        label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
    }

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

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

}

Ecco alcune implementazioni alternative per preferredLayoutAttributesFitting(_:):

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)
    layoutAttributes.frame.size = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
    return layoutAttributes
}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    label.preferredMaxLayoutWidth = layoutAttributes.frame.width
    layoutAttributes.frame.size.height = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
    return layoutAttributes
}

Visualizzazione prevista:

inserisci qui la descrizione dell'immagine


Questo ha funzionato bene per me fino a quando non ho aggiunto le intestazioni di sezione. Le intestazioni di sezione sono fuori posto e coprono altre celle in iOS 11. Ma funziona bene in iOS 12. Hai riscontrato problemi con l'aggiunta di intestazioni di sezione?
Nakul

Grazie mille. Hai salvato la mia settimana. 🙇‍♂️
Gaetan

1
CollectionView scorre verso l'alto ogni volta che le dimensioni cambiano
Sajith

1
notizie fantastiche, @ImanouPetit! Vorrei che la taglia fosse più grande! Informazioni fantastiche sugli oggetti Layout compositivo, grazie.
Fattie

1
Ciao, ci sto provando. Anche se sto impostando il layout nel viewdidload come hai fatto sopra, ottengo l'arresto anomalo che afferma che collectionview deve essere inizializzato con un parametro di layout non nullo. C'è qualcosa che mi manca? Questo è Xcode 11 per iOS 13.
geistmate

29

Problema

Stai cercando l'altezza automatica e vuoi anche avere tutta la larghezza, non è possibile ottenere entrambe in uso UICollectionViewFlowLayoutAutomaticSize.

Vuoi farlo usando UICollectionViewcosì di seguito è la soluzione per te.

Soluzione

Passaggio I : calcola l'altezza prevista della cella

1. Se è stato impostato solo UILabel in e che ha calcolato l'altezza prevista , passare tutti e tre i parametriCollectionViewCellnumberOfLines=0UIlable

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. Se il tuo CollectionViewCellcontiene solo UIImageViewe se dovrebbe essere dinamico in Altezza, allora devi ottenere l'altezza di UIImage ( UIImageViewdevi avere AspectRatiovincoli)

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

3. Se contiene entrambi, calcola la loro altezza e sommali.

FASE-II : restituire la dimensione diCollectionViewCell

1. Aggiungi UICollectionViewDelegateFlowLayoutdelegato nel tuo viewController

2. Implementare il metodo delegato

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

Spero che questo ti possa aiutare.


1
È possibile avere un UICollectionView a scorrimento orizzontale con l'altezza delle celle che aumenta verticalmente con il layout automatico?
nr5

Voglio ottenere lo stesso risultato ma metà larghezza e altezza dinamica
Mihir Mehta

return CGSize(width: view.frame.width/2, height: expectedHeight)tieni anche in considerazione il Padding piuttosto che restituire la larghezza altrimenti due celle non si adatterebbero in una singola riga.
Dhiru

@ nr5 Hai trovato la soluzione? Il mio requisito è uguale al tuo. Grazie.
Maulik Bhuptani

@MaulikBhuptani Non ho potuto farlo completamente con Auto Layout. Ho dovuto creare una classe Layout personalizzata e sovrascrivere alcuni metodi di classe.
nr5

18

Ci sono un paio di modi per affrontare questo problema.

Un modo è assegnare al layout del flusso della vista della raccolta una dimensione stimata e calcolare la dimensione della cella.

Nota: come menzionato nei commenti seguenti, a partire da iOS 10 non è più necessario fornire una dimensione stimata per attivare la chiamata a una cella func preferredLayoutAttributesFitting(_ layoutAttributes:). In precedenza (iOS 9) richiedeva di fornire una dimensione stimata se si desiderava interrogare una cella prefferedLayoutAttributes.

(supponendo che tu stia utilizzando gli storyboard e la vista della raccolta sia collegata tramite IB)

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

Per le celle semplici che di solito sarà sufficiente. Se la dimensione è ancora errata, nella cella di visualizzazione della raccolta è possibile eseguire l'override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes, il che ti darà un controllo più fine sulla dimensione della cella. Nota: sarà comunque necessario fornire al layout di flusso una dimensione stimata .

Quindi sovrascrivi func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributesper restituire la dimensione corretta.

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
}

In alternativa, puoi invece utilizzare una cella di dimensionamento per calcolare la dimensione della cella nel file 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)
}

Sebbene questi non siano esempi perfetti pronti per la produzione, dovrebbero farti iniziare nella giusta direzione. Non posso dire che questa sia la migliore pratica, ma per me funziona, anche con celle abbastanza complesse contenenti più etichette, che possono o meno andare a capo su più righe.


@Fattie Non è chiaro come "esitmatedItemSize" sia irrilevante per una cella di visualizzazione della raccolta di dimensioni dinamiche, quindi mi piacerebbe sentire i tuoi pensieri. (non sarcasmo)
Eric Murphey

Inoltre, se a questa risposta manca qualcosa di specifico utile e / o ciò che consideri canonico, fammelo sapere. Non discuterò la taglia, vorrei solo essere d'aiuto a chiunque veda questa domanda.
Eric Murphey

1
(Includere o meno estimatoItemSize non fa differenza nell'attuale iOS e non è affatto d'aiuto, in ogni caso, nel problema "larghezza intera + altezza dinamica".) il codice alla fine, i pennini sono usati raramente in questi giorni e ha poca rilevanza per un'altezza dinamica (da, ad esempio, alcune visualizzazioni di testo con altezza dinamica). grazie comunque
Fattie

Sono arrivato anche a questa soluzione, ma nel mio caso, iOS 11, Xcode 9.2, il contenuto della cella sembrava scollegato dalla cella stessa: non riuscivo a far espandere un'immagine all'altezza o alla larghezza completa della dimensione fissa. Ho inoltre notato che il CV era vincolato alle impostazioni preferredLayoutAttributesFitting. Ho quindi aggiunto un mio vincolo in detta funzione, vincolante per la dimensione fissa da stimatoItemSize, FWIW, vedere la mia risposta di seguito.
Chris Conover

16

Ho trovato una soluzione abbastanza semplice per questo problema: all'interno della mia CollectionViewCell ho ottenuto un UIView () che in realtà è solo uno sfondo. Per ottenere l'intera larghezza ho impostato solo le seguenti ancore

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

La "magia" avviene nella prima riga. Ho impostato la larghezzaAnchor dinamicamente alla larghezza dello schermo. Altrettanto importante è sottrarre gli inserti di CollectionView. Altrimenti la cella non verrà visualizzata. Se non vuoi avere una tale visualizzazione in background, rendila invisibile.

Il FlowLayout utilizza le seguenti impostazioni

layout.itemSize = UICollectionViewFlowLayoutAutomaticSize
layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize

Il risultato è una cella a larghezza intera con altezza dinamica.

inserisci qui la descrizione dell'immagine


1
vacca sacra - è incredibile! così ovvio ! brillante!
Fattie

1
solo TBC @ inf1783, lo stai facendo in UICollectionView? (non vista tabella) - corretto?
Fattie

1
@Fattie Sì, è un UICollectionView;)
inf1783

fantastico @ inf1783, non vedo l'ora di provarlo. A proposito, un altro buon modo per farlo è quello di sottoclassare l'UIView e aggiungere semplicemente una larghezza intrinseca (immagino che avrebbe lo stesso effetto, e probabilmente lo farebbe funzionare anche al momento dello storyboard)
Fattie

1
Ogni volta che provo questo, finisco in un ciclo infinito con Il comportamento di UICollectionViewFlowLayout non è definito errore: /
Skodik.o

7

LAVORANDO!!! Testato su IOS: 12.1 Swift 4.1

Ho una soluzione molto semplice che funziona senza rompere i vincoli.

inserisci qui la descrizione dell'immagine

Il mio ViewControllerClass

class ViewController: UIViewController {

    @IBOutlet weak var collectionView: UICollectionView!

    let cellId = "CustomCell"

    var source = ["nomu", "when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ", "t is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by", "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia,","nomu", "when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ", "t is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by", "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia,","nomu", "when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ", "t is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by", "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia,"]

    override func viewDidLoad() {
        super.viewDidLoad()

        self.collectionView.delegate = self
        self.collectionView.dataSource = self
        self.collectionView.register(UINib.init(nibName: cellId, bundle: nil), forCellWithReuseIdentifier: cellId)

        if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
            flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        }

    }

}


extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {

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

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as? CustomCell else { return UICollectionViewCell() }
        cell.setData(data: source[indexPath.item])
        return cell
    }


}

Classe CustomCell:

class CustomCell: UICollectionViewCell {

    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var widthConstraint: NSLayoutConstraint!

    override func awakeFromNib() {
        super.awakeFromNib()
        self.widthConstraint.constant = UIScreen.main.bounds.width
    }

    func setData(data: String) {
        self.label.text = data
    }

    override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        return contentView.systemLayoutSizeFitting(CGSize(width: self.bounds.size.width, height: 1))
    }

}

Ingrediente principale è la systemLayoutSizeFitting funzione Customcell. Inoltre, dobbiamo impostare la larghezza della vista all'interno di Cell con vincoli.


6

Devi aggiungere un vincolo di larghezza a CollectionViewCell

class SelfSizingCell: UICollectionViewCell {

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

finalmente qualcuno lo ha reso perfetto in due righe
Omar N Shamali

Ciò limiterà la cella a una larghezza fissa, difficilmente auto-dimensionata. Se dovessi ruotare il dispositivo o regolare le dimensioni dello schermo su un iPad, rimarrà fisso.
Thomas Verbeek

4
  1. Imposta estimatedItemSizeil layout del flusso:

    collectionViewLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
  2. Definisci un vincolo di larghezza nella cella e impostalo in modo che sia uguale alla larghezza di superview:

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

Esempio completo

Testato su iOS 11.


3

Personalmente ho trovato i modi migliori per avere un UICollectionView in cui AutoLayout determina la dimensione mentre ogni cella può avere una dimensione diversa è implementare la funzione UICollectionViewDelegateFlowLayout sizeForItemAtIndexPath mentre si utilizza una cella effettiva per misurare la dimensione.

Ne ho parlato in uno dei miei post sul blog

Spero che questo ti aiuti a ottenere ciò che desideri. Non ne sono sicuro al 100%, ma credo che a differenza di UITableView in cui puoi effettivamente avere un'altezza delle celle completamente automatica utilizzando la disgiunzione del layout automatico con

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44

UICollectionView non dispone di un tale modo per consentire a AutoLayout di determinare la dimensione perché UICollectionViewCell non occupa necessariamente l'intera larghezza dello schermo.

Ma ecco una domanda per te : se hai bisogno di celle a schermo intero, perché ti preoccupi anche di usare UICollectionView su un buon vecchio UITableView che viene fornito con le celle di ridimensionamento automatico?


tienilo, Aprit sotto sta dicendo che UICollectionViewFlowLayoutAutomaticSize è ora disponibile nel layout di flusso, in iOS10 ...
Fattie

Ho visto anche questo. Apparentemente è una cosa ora in iOS 10. Ho appena visto il corrispondente discorso del WWDC su questo: developer.apple.com/videos/play/wwdc2016/219 Tuttavia, questo ridimensionerà completamente la cella per adattarla al suo contenuto. Non sono sicuro di come si possa dire alla cella (con AutoLayout) di riempire la larghezza dello schermo poiché non è possibile impostare un vincolo tra UICollectionViewCell e il suo genitore (almeno non in StoryBoards)
xxtesaxx

giusto. sembra che non siamo più vicini a una vera soluzione a questo problema incredibilmente ovvio. : /
Fattie

Secondo il discorso, potresti ancora sovrascrivere sizeThatFits () o preferredLayoutAttributesFitting () e calcolare la dimensione da solo, ma non è quello che l'OP ha chiesto, credo. Ad ogni modo, sarei comunque interessato al caso d'uso per quando ha bisogno di un UICollectionViewCell a larghezza intera e perché sarebbe un grosso problema non usare un UITableView in questo caso particolare (certo che ci possono essere alcuni casi ma poi immagino devi solo fare i calcoli da solo)
xxtesaxx

a pensarci bene, è abbastanza incredibile che non si possa semplicemente impostare "colonne == 1" (e poi forse colonne == 2 quando il dispositivo è di lato), con viste di raccolta.
Fattie

3

Secondo il mio commento sulla risposta di Eric, la mia soluzione è molto simile alla sua, ma ho dovuto aggiungere un vincolo in preferredSizeFor ... per vincolare alla dimensione fissa.

    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
    }

Questa domanda ha una serie di duplicati, ho risposto in dettaglio qui e ho fornito un'app di esempio funzionante qui.


2

Non sono sicuro che si qualifichi come una "risposta davvero buona", ma è quello che sto usando per ottenere questo risultato. Il mio layout di flusso è orizzontale e sto cercando di regolare la larghezza con il layout automatico, quindi è simile alla tua situazione.

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

Quindi nel controller di visualizzazione in cui viene modificato il vincolo di autolayout, lancio una NSNotification.

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

Nella mia sottoclasse UICollectionView, ascolto quella notifica:

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

e invalidare il layout:

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

Ciò fa sì sizeForItemAtche venga chiamato di nuovo utilizzando la nuova dimensione della vista raccolta. Nel tuo caso, dovrebbe essere in grado di aggiornarsi dati i nuovi vincoli disponibili nel layout.


solo TBC Mark vuoi dire che stai effettivamente cambiando la dimensione di una cella? (cioè, la cella appare ed è di dimensione X, poi succede qualcosa nella tua app e diventa di dimensione Y ..?) thx
Fattie

Sì. Quando l'utente trascina un elemento sullo schermo, viene generato un evento che aggiorna le dimensioni della cella.
Mark Suman

1

Sul tuo viewDidLayoutSubviews, imposta la estimatedItemSizelarghezza intera (il layout si riferisce all'oggetto UICollectionViewFlowLayout):

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

Sulla tua cella, assicurati che i tuoi vincoli tocchino sia la parte superiore che quella inferiore della cella (il codice seguente usa la cartografia per semplificare l'impostazione dei vincoli ma puoi farlo con NSLayoutConstraint o IB se lo desideri):

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
    }

Voilà, le tue celle ora aumenteranno automaticamente in altezza!


0

Nessuna delle soluzioni funzionava per me poiché ho bisogno di una larghezza dinamica per adattarsi alla larghezza di 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

Da iOS 10, abbiamo una nuova API sul layout di flusso per farlo.

Tutto quello che dovete fare è impostare il vostro flowLayout.estimatedItemSizeper una nuova costante, UICollectionViewFlowLayoutAutomaticSize.

fonte


Hmm, sei abbastanza sicuro che lo renda * a tutta larghezza ? Quindi, essenzialmente una colonna?
Fattie

Potrebbe accadere se imposti la larghezza di UICollectionViewCell su tutta la larghezza in modo esplicito.
Arpit Dongre

No, non fissa la larghezza a tutta altezza e ignora il valore della dimensione stimata a meno che tu non segua la risposta di Eric sopra, o la mia sotto / dopo.
Chris Conover

OK, sfortunatamente questo non ha assolutamente alcun collegamento con il problema! eh! : O
Fattie

0

AutoLayout può essere utilizzato per il dimensionamento automatico delle celle in CollectionView in 2 semplici passaggi:

  1. Abilitazione del dimensionamento dinamico delle celle

flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

  1. Avere una visualizzazione contenitore e impostare containerView.widthAnchor.constraint da collectionView(:cellForItemAt:)per limitare la larghezza di contentView alla larghezza di collectionView.
class ViewController: UIViewController, UICollectionViewDataSource {
    ...

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! MultiLineCell
        cell.textView.text = dummyTextMessages[indexPath.row]
        cell.maxWidth = collectionView.frame.width
        return cell
    }

    ...
}
class MultiLineCell: UICollectionViewCell{
    ....

    var maxWidth: CGFloat? {
        didSet {
            guard let maxWidth = maxWidth else {
                return
            }
            containerViewWidthAnchor.constant = maxWidth
            containerViewWidthAnchor.isActive = true
        }
    }

    ....
}

È così che otterrai il risultato desiderato. Fare riferimento ai seguenti riassunti per il codice completo:

Riferimento / Crediti:

Immagine dello schermo: inserisci qui la descrizione dell'immagine


-6

Devi ereditare la classe UICollectionViewDelegateFlowLayout sul tuo collectionViewController. Quindi aggiungi la funzione:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CGSize(width: view.frame.width, height: 100)
}

Usando quello, hai la larghezza della larghezza dello schermo.

E ora hai un collectionViewController con righe come tableViewController.

Se vuoi che la dimensione dell'altezza di ogni cella sia dinamica, forse dovresti creare celle personalizzate per ogni cella di cui hai bisogno.


7
Questa non è esattamente la risposta alla domanda :) Ciò darebbe un'altezza fissa di 100, cioè, è come quella programmata prima ancora che esistesse l'autolayout.
Fattie
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.