Specificare una dimensione delle celle in UICollectionView utilizzando il layout automatico


113

In iOS 8 UICollectionViewFlowLayoutsupporta il ridimensionamento automatico delle celle in base alla dimensione del contenuto. Questo ridimensiona le celle sia in larghezza che in altezza in base al loro contenuto.

È possibile specificare un valore fisso per la larghezza (o altezza) di tutte le celle e consentire il ridimensionamento delle altre dimensioni?

Per un semplice esempio, si consideri un'etichetta su più righe in una cella con vincoli posizionandola ai lati della cella. L'etichetta multilinea può essere ridimensionata in modi diversi per adattare il testo. La cella dovrebbe riempire la larghezza della vista della raccolta e regolare l'altezza di conseguenza. Invece, le celle vengono ridimensionate in modo casuale e provoca anche un arresto anomalo quando la dimensione della cella è maggiore della dimensione non scorrevole della vista della raccolta.


iOS 8 introduce il metodo systemLayoutSizeFittingSize: withHorizontalFittingPriority: verticalFittingPriority:Per ogni cella nella vista raccolta il layout richiama questo metodo sulla cella, passando la dimensione stimata. Quello che avrebbe senso per me sarebbe sovrascrivere questo metodo sulla cella, passare la dimensione data e impostare il vincolo orizzontale su richiesto e una priorità bassa per il vincolo verticale. In questo modo la dimensione orizzontale è fissata al valore impostato nel layout e la dimensione verticale può essere flessibile.

Qualcosa come questo:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    UICollectionViewLayoutAttributes *attributes = [super preferredLayoutAttributesFittingAttributes:layoutAttributes];
    attributes.size = [self systemLayoutSizeFittingSize:layoutAttributes.size withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    return attributes;
}

Le dimensioni restituite con questo metodo, tuttavia, sono completamente strane. La documentazione su questo metodo è molto poco chiara per me e menziona l'uso delle costanti UILayoutFittingCompressedSize UILayoutFittingExpandedSizeche rappresentano solo una dimensione zero e una piuttosto grande.

Il sizeparametro di questo metodo è davvero solo un modo per passare due costanti? Non c'è modo di ottenere il comportamento che mi aspetto di ottenere l'altezza appropriata per una data taglia?


Soluzioni alternative

1) L'aggiunta di vincoli che verranno specificati per una larghezza specifica per la cella consente di ottenere il layout corretto. Questa è una soluzione scadente perché tale vincolo dovrebbe essere impostato sulla dimensione della vista della raccolta della cella a cui non ha alcun riferimento sicuro. Il valore per quel vincolo potrebbe essere passato quando la cella è configurata, ma sembra anche del tutto controintuitivo. Questo è anche scomodo perché l'aggiunta di vincoli direttamente a una cella o alla sua visualizzazione del contenuto sta causando molti problemi.

2) Usa una vista tabella. Le visualizzazioni tabella funzionano in questo modo fuori dagli schemi poiché le celle hanno una larghezza fissa, ma ciò non si adatterebbe ad altre situazioni come un layout iPad con celle a larghezza fissa in più colonne.

Risposte:


135

Sembra che quello che stai chiedendo sia un modo per usare UICollectionView per produrre un layout come UITableView. Se è davvero quello che vuoi, il modo giusto per farlo è con una sottoclasse UICollectionViewLayout personalizzata (forse qualcosa come SBTableLayout ).

D'altra parte, se stai davvero chiedendo se esiste un modo pulito per farlo con l'UICollectionViewFlowLayout predefinito, allora credo che non ci sia modo. Anche con le celle auto-dimensionanti di iOS8, non è semplice. Il problema fondamentale, come dici tu, è che il meccanismo del layout di flusso non fornisce alcun modo per fissare una dimensione e lasciare che un'altra risponda. (Inoltre, anche se fosse possibile, ci sarebbe ulteriore complessità nella necessità di due passaggi di layout per dimensionare le etichette multilinea. Ciò potrebbe non adattarsi al modo in cui le celle auto-dimensionanti vogliono calcolare tutto il dimensionamento tramite una chiamata a systemLayoutSizeFittingSize.)

Tuttavia, se si desidera comunque creare un layout simile a una vista tabella con un layout di flusso, con celle che determinano la propria dimensione e rispondono naturalmente alla larghezza della vista della raccolta, ovviamente è possibile. C'è ancora il modo disordinato. L'ho fatto con una "cella di dimensionamento", ovvero un UICollectionViewCell non visualizzato che il controller conserva solo per il calcolo delle dimensioni delle celle.

Ci sono due parti in questo approccio. La prima parte è che il delegato della vista della raccolta calcoli la dimensione corretta della cella, prendendo la larghezza della vista della raccolta e utilizzando la cella di dimensionamento per calcolare l'altezza della cella.

Nel tuo UICollectionViewDelegateFlowLayout, implementi un metodo come questo:

func collectionView(collectionView: UICollectionView,
  layout collectionViewLayout: UICollectionViewLayout,
  sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
  // NOTE: here is where we say we want cells to use the width of the collection view
   let requiredWidth = collectionView.bounds.size.width

   // NOTE: here is where we ask our sizing cell to compute what height it needs
  let targetSize = CGSize(width: requiredWidth, height: 0)
  /// NOTE: populate the sizing cell's contents so it can compute accurately
  self.sizingCell.label.text = items[indexPath.row]
  let adequateSize = self.sizingCell.preferredLayoutSizeFittingSize(targetSize)
  return adequateSize
}

Ciò farà sì che la vista della raccolta imposti la larghezza della cella in base alla vista della raccolta che la racchiude, ma poi chiederà alla cella di dimensionamento di calcolare l'altezza.

La seconda parte consiste nel fare in modo che la cella di dimensionamento utilizzi i propri vincoli AL per calcolare l'altezza. Questo può essere più difficile di quanto dovrebbe essere, a causa del modo in cui le UILabel multi-linea richiedono effettivamente un processo di layout a due fasi. Il lavoro viene svolto nel metodo preferredLayoutSizeFittingSize, che è così:

 /*
 Computes the size the cell will need to be to fit within targetSize.

 targetSize should be used to pass in a width.

 the returned size will have the same width, and the height which is
 calculated by Auto Layout so that the contents of the cell (i.e., text in the label)
 can fit within that width.

 */
 func preferredLayoutSizeFittingSize(targetSize:CGSize) -> CGSize {

   // save original frame and preferredMaxLayoutWidth
   let originalFrame = self.frame
   let originalPreferredMaxLayoutWidth = self.label.preferredMaxLayoutWidth

   // assert: targetSize.width has the required width of the cell

   // step1: set the cell.frame to use that width
   var frame = self.frame
   frame.size = targetSize
   self.frame = frame

   // step2: layout the cell
   self.setNeedsLayout()
   self.layoutIfNeeded()
   self.label.preferredMaxLayoutWidth = self.label.bounds.size.width

   // assert: the label's bounds and preferredMaxLayoutWidth are set to the width required by the cell's width

   // step3: compute how tall the cell needs to be

   // this causes the cell to compute the height it needs, which it does by asking the 
   // label what height it needs to wrap within its current bounds (which we just set).
   let computedSize = self.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)

   // assert: computedSize has the needed height for the cell

   // Apple: "Only consider the height for cells, because the contentView isn't anchored correctly sometimes."
   let newSize = CGSize(width:targetSize.width,height:computedSize.height)

   // restore old frame and preferredMaxLayoutWidth
   self.frame = originalFrame
   self.label.preferredMaxLayoutWidth = originalPreferredMaxLayoutWidth

   return newSize
 }

(Questo codice è adattato dal codice di esempio Apple dal codice di esempio della sessione WWDC2014 in "Visualizzazione raccolta avanzata".)

Un paio di punti da notare. Utilizza layoutIfNeeded () per forzare il layout dell'intera cella, al fine di calcolare e impostare la larghezza dell'etichetta. Ma non è abbastanza. Credo che tu debba anche impostare in preferredMaxLayoutWidthmodo che l'etichetta utilizzi quella larghezza con il layout automatico. E solo allora puoi usarlo per fare systemLayoutSizeFittingSizein modo che la cella calcoli la sua altezza tenendo conto dell'etichetta.

Mi piace questo approccio? No!! Sembra troppo complesso e fa il layout due volte. Ma finché le prestazioni non diventano un problema, preferisco eseguire il layout due volte in fase di esecuzione piuttosto che definirlo due volte nel codice, che sembra essere l'unica altra alternativa.

La mia speranza è che alla fine le celle auto-dimensionate funzionino in modo diverso e tutto questo diventi molto più semplice.

Progetto di esempio che lo mostra al lavoro.

Ma perché non utilizzare solo celle auto-dimensionanti?

In teoria, le nuove funzionalità di iOS8 per le "celle auto-dimensionate" dovrebbero renderlo superfluo. Se hai definito una cella con layout automatico (AL), la visualizzazione della raccolta dovrebbe essere abbastanza intelligente da consentire la dimensione e la disposizione corretta. In pratica, non ho visto alcun esempio che lo abbia fatto funzionare con etichette multilinea. Penso che ciò sia in parte dovuto al fatto che il meccanismo cellulare auto-dimensionante è ancora difettoso.

Ma scommetto che è principalmente a causa della solita complessità del layout automatico e delle etichette, ovvero che le etichette UIL richiedono un processo di layout sostanzialmente in due passaggi. Non mi è chiaro come sia possibile eseguire entrambi i passaggi con celle auto-dimensionanti.

E come ho detto, questo è davvero un lavoro per un layout diverso. Fa parte dell'essenza del layout di flusso il fatto di posizionare le cose che hanno una dimensione, piuttosto che fissare una larghezza e consentire loro di scegliere la loro altezza.

E che dire di preferredLayoutAttributesFittingAttributes:?

Il preferredLayoutAttributesFittingAttributes:metodo è una falsa pista, credo. Questo è solo lì per essere utilizzato con il nuovo meccanismo cellulare auto-dimensionante. Quindi questa non è la risposta finché quel meccanismo è inaffidabile.

E che succede con systemlayoutSizeFittingSize :?

Hai ragione, i documenti creano confusione.

I documenti su systemLayoutSizeFittingSize:ed systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority:entrambi suggeriscono che dovresti solo passare UILayoutFittingCompressedSizee UILayoutFittingExpandedSizecome targetSize. Tuttavia, la firma del metodo stessa, i commenti di intestazione e il comportamento delle funzioni indicano che stanno rispondendo al valore esatto del targetSizeparametro.

In effetti, se si imposta il UICollectionViewFlowLayoutDelegate.estimatedItemSize, per abilitare il nuovo meccanismo di cella auto-dimensionante, quel valore sembra essere passato come targetSize. E UILabel.systemLayoutSizeFittingSizesembra restituire esattamente gli stessi valori di UILabel.sizeThatFits. Questo è sospetto, dato che l'argomento a systemLayoutSizeFittingSizedovrebbe essere un obiettivo approssimativo e l'argomento a sizeThatFits:dovrebbe essere una dimensione massima circoscrivente.

Più risorse

Sebbene sia triste pensare che un tale requisito di routine dovrebbe richiedere "risorse di ricerca", penso che lo sia. Buoni esempi e discussioni sono:


Perché reimposti il ​​frame al vecchio frame?
vrwim

Lo faccio in modo che la funzione possa essere utilizzata solo per determinare la dimensione del layout preferita della vista, senza influenzare lo stato del layout della vista in cui la chiami. In pratica, questo è irrilevante se tieni questa vista solo allo scopo di fare calcoli di layout. Inoltre, quello che faccio qui è insufficiente. Il solo ripristino del frame non lo ripristina al suo stato precedente, poiché l'esecuzione del layout potrebbe aver modificato i layout delle viste secondarie.
Algale

1
Questo è un tale casino per qualcosa che le persone hanno fatto da iOS6. Apple esce sempre in qualche modo, ma non è mai del tutto corretto.
GregP

1
È possibile creare manualmente un'istanza della cella di dimensionamento in viewDidLoad e tenerla per visualizzare una proprietà personalizzata. È solo una cella, quindi è economico. Dal momento che lo stai usando solo per il dimensionamento e gestisci tutte le interazioni con esso, non è necessario che partecipi al meccanismo di riutilizzo delle celle della vista raccolta, quindi non è necessario ottenerlo dalla vista raccolta stessa.
algale

1
Come diavolo si possono ancora spezzare le cellule auto-dimensionanti ?!
Reuben Scratton

50

C'è un modo più pulito per farlo rispetto ad alcune delle altre risposte qui, e funziona bene. Dovrebbe essere performante (le viste della raccolta si caricano velocemente, nessun passaggio di layout automatico non necessario, ecc.) E non ha alcun "numero magico" come una larghezza di visualizzazione della raccolta fissa. Anche la modifica delle dimensioni della visualizzazione della raccolta, ad esempio durante la rotazione, e l'invalidazione del layout dovrebbero funzionare alla grande.

1. Creare la seguente sottoclasse di layout di flusso

class HorizontallyFlushCollectionViewFlowLayout: UICollectionViewFlowLayout {

    // Don't forget to use this class in your storyboard (or code, .xib etc)

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes
        guard let collectionView = collectionView else { return attributes }
        attributes?.bounds.size.width = collectionView.bounds.width - sectionInset.left - sectionInset.right
        return attributes
    }

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let allAttributes = super.layoutAttributesForElementsInRect(rect)
        return allAttributes?.flatMap { attributes in
            switch attributes.representedElementCategory {
            case .Cell: return layoutAttributesForItemAtIndexPath(attributes.indexPath)
            default: return attributes
            }
        }
    }
}

2. Registrare la visualizzazione della raccolta per il dimensionamento automatico

// The provided size should be a plausible estimate of the actual
// size. You can set your item size in your storyboard
// to a good estimate and use the code below. Otherwise,
// you can provide it manually too, e.g. CGSize(width: 100, height: 100)

flowLayout.estimatedItemSize = flowLayout.itemSize

3. Utilizzare la larghezza predefinita + l'altezza personalizzata nella sottoclasse di celle

override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    layoutAttributes.bounds.size.height = systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
    return layoutAttributes
}

8
Questa è la risposta migliore che ho trovato sull'argomento auto-dimensionamento. Grazie Jordan!
haik.ampardjian

1
Non riesco a farlo funzionare su iOS (9.3). Su 10 funziona bene. L'applicazione si arresta in modo anomalo su _updateVisibleCells (ricorsione infinita?). openradar.me/25303115
cristiano2lopes

@ cristiano2lopes funziona bene per me su iOS 9.3. Assicurati di fornire una stima ragionevole e assicurati che i vincoli delle celle siano impostati per risolversi in una dimensione corretta.
Jordan Smith

Funziona alla grande. Sto usando Xcode 8 e testato in iOS 9.3.3 (dispositivo fisico) e non si blocca! Funziona alla grande. Assicurati solo che la tua stima sia buona!
hahmed

1
@JordanSmith Questo non funziona in iOS 10. Hai qualche demo di esempio. Ho provato molte cose per fare la raccolta vista l'altezza della cella in base al suo contenuto ma niente. Sto usando il layout automatico in iOS 10 e utilizzo
Swift

6

Un modo semplice per farlo in iOS 9 in poche righe di codici: l'esempio del modo orizzontale (che fissa la sua altezza all'altezza della Vista raccolta):

Inizia il layout del flusso della vista raccolta con un estimatedItemSizeper abilitare la cella a ridimensionamento automatico:

self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.estimatedItemSize = CGSizeMake(1, 1);

Implementare la Collezione Vista layout delegato (nel controller Vista maggior parte del tempo), collectionView:layout:sizeForItemAtIndexPath:. L'obiettivo qui è impostare l'altezza fissa (o larghezza) sulla dimensione della vista raccolta. Il valore 10 può essere qualsiasi cosa, ma dovresti impostarlo su un valore che non infranga i vincoli:

- (CGSize)collectionView:(UICollectionView *)collectionView
                  layout:(UICollectionViewLayout *)collectionViewLayout
  sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return CGSizeMake(10, CGRectGetHeight(collectionView.bounds));
}

Sostituisci il tuo preferredLayoutAttributesFittingAttributes:metodo di cella personalizzato , questa parte calcola effettivamente la larghezza della cella dinamica in base ai vincoli del layout automatico e all'altezza che hai appena impostato:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
    UICollectionViewLayoutAttributes *attributes = [layoutAttributes copy];
    float desiredWidth = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].width;
    CGRect frame = attributes.frame;
    frame.size.width = desiredWidth;
    attributes.frame = frame;
    return attributes;
}

Sai se su iOS9 questo metodo più semplice funziona correttamente quando la cella include una multi-riga UILabel, la cui interruzione di riga influenza l'altezza intrinseca della cella? Questo è un caso comune che richiedeva tutti i backflip che descrivo. Mi chiedo se iOS lo abbia risolto dopo la mia risposta.
alghe

2
@algal Purtroppo, la risposta è no.
jamesk

4

Prova a correggere la larghezza negli attributi di layout preferiti:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    UICollectionViewLayoutAttributes *attributes = [[super preferredLayoutAttributesFittingAttributes:layoutAttributes] copy];
    CGSize newSize = [self systemLayoutSizeFittingSize:CGSizeMake(FIXED_WIDTH,layoutAttributes.size) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    CGRect newFrame = attr.frame;
    newFrame.size.height = size.height;
    attr.frame = newFrame;
    return attr;
}

Naturalmente vuoi anche assicurarti di impostare correttamente il tuo layout per:

UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *) self.collectionView.collectionViewLayout;
flowLayout.estimatedItemSize = CGSizeMake(FIXED_WIDTH, estimatedHeight)];

Ecco qualcosa che ho inserito su GitHub che utilizza celle a larghezza costante e supporta il tipo dinamico, quindi l'altezza delle celle si aggiorna al variare della dimensione del carattere del sistema.


In questo frammento si chiama systemLayoutSizeFittingSize :. Sei riuscito a farlo funzionare senza impostare anche preferredMaxLayoutWidth da qualche parte? Nella mia esperienza, se non stai impostando preferredMaxLayoutWidth, devi eseguire un layout manuale aggiuntivo, ad esempio sovrascrivendo sizeThatFits: con calcoli che riproducono la logica dei vincoli di layout automatico definiti altrove.
Algal

@algal preferredMaxLayoutWidth è una proprietà su UILabel? Non sei del tutto sicuro di quanto sia rilevante?
Daniel Galasko

Oops! Buon punto, non lo è! Non l'ho mai gestito con un UITextView, quindi non mi ero reso conto che le loro API differivano lì. Sembra che UITextView.systemLayoutSizeFittingSize rispetti il ​​suo frame poiché non ha intrinsicContentSize. Ma UILabel.systemLayoutSizeFittingSize ignora il frame, poiché ha intrinsicContentSize, quindi è necessario preferredMaxLayoutWidth per ottenere intrinsicContentSize per esporre la sua altezza di wrapping ad AL. Sembra ancora che UITextView abbia bisogno di due passaggi o di un layout manuale, ma non credo di aver capito tutto questo completamente.
alghe

@algal Posso essere d'accordo, ho anche riscontrato alcuni intoppi durante l'utilizzo di UILabel. Impostando la sua larghezza preferita risolve il problema, ma sarebbe bello avere un approccio più dinamico poiché questa larghezza deve essere scalata con la sua superview. La maggior parte dei miei progetti non utilizza la proprietà preferita, generalmente la uso come soluzione rapida poiché c'è sempre un problema più profondo
Daniel Galasko

0

SÌ, può essere fatto utilizzando il layout automatico a livello di codice e impostando vincoli in storyboard o xib. È necessario aggiungere un vincolo affinché la dimensione della larghezza rimanga costante e impostare l'altezza maggiore o uguale a.
http://www.thinkandbuild.it/learn-to-love-auto-layout-programmatical/

http://www.cocoanetics.com/2013/08/variable-sized-items-in-uicollectionview/
Spero che questo sia utile e risolvi il tuo problema.


3
Questo dovrebbe funzionare per un valore di larghezza assoluto, ma nel caso in cui desideri che la cella sia sempre l'intera larghezza della vista raccolta, non funzionerà.
Michael Waterfall

@MichaelWaterfall sono riuscito a farlo funzionare a tutta larghezza utilizzando AutoLayout. Ho aggiunto un vincolo di larghezza alla visualizzazione del contenuto all'interno della cella. Poi, nel cellForItemAtIndexPathaggiornare il vincolo: cell.constraintItemWidth.constant = UIScreen.main.bounds.width. La mia app è solo portait. Probabilmente vorrai reloadDatadopo un cambio di orientamento aggiornare il vincolo.
Flitskikker
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.