UICollectionView Self-Sizing Cells met automatische lay-out

Ik probeer self-sizing UICollectionViewCellste laten werken met automatische lay-out, maar het lukt me niet om de cellen de grootte aan te passen aan de inhoud. Ik heb moeite om te begrijpen hoe de grootte van de cel wordt bijgewerkt op basis van de inhoud van de inhoud van de cel.

Dit is de configuratie die ik heb geprobeerd:

  • Aangepaste UICollectionViewCellmet een UITextViewin de contentView.
  • Scrollen voor de UITextViewis uitgeschakeld.
  • De horizontale beperking van contentView is: “H:|[_textView(320)]”, d.w.z. de UITextViewwordt links van de cel vastgezet met een expliciete breedte van 320.
  • De verticale beperking van contentView is: “V:|-0-[_textView]”, d.w.z. de UITextViewvastgemaakt aan de bovenkant van de cel.
  • De UITextViewheeft een hoogtebeperking die is ingesteld op een constante waarvan de UITextViewrapporten in de tekst passen.

Zo ziet het eruit met de celachtergrond ingesteld op rood en de UITextViewachtergrond ingesteld op Blauw:
cel achtergrond rood, UITextView achtergrond blauw

Ik heb het project waarmee ik heb gespeeld op GitHub hiergezet.


Antwoord 1, autoriteit 100%

Dit antwoord is verouderd vanaf iOS 14 met de toevoeging van compositorische lay-outs. Overweeg om de nieuwe API

bij te werken

Bijgewerkt voor Swift 5

preferredLayoutAttributesFittingAttributeshernoemd naar preferredLayoutAttributesFittingen gebruik automatisch aanpassen


Bijgewerkt voor Swift 4

systemLayoutSizeFittingSizehernoemd naar systemLayoutSizeFitting


Bijgewerkt voor iOS 9

Nadat ik zag dat mijn GitHub-oplossing onder iOS 9 kapot ging, kreeg ik eindelijk de tijd om het probleem volledig te onderzoeken. Ik heb de repo nu bijgewerkt met verschillende voorbeelden van verschillende configuraties voor zelfgrootte cellen. Mijn conclusie is dat self-sizing cellen in theorie geweldig zijn, maar in de praktijk rommelig. Een woord van waarschuwing wanneer u doorgaat met zelfgrootte cellen.

TL;DR

Bekijk mijn GitHub-project


Zelfgrootte cellen worden alleen ondersteund met stroomlay-out, dus zorg ervoor dat u dat gebruikt.

Er zijn twee dingen die je moet instellen om de zelfgrootte cellen te laten werken.

#1. Stel estimatedItemSizein op UICollectionViewFlowLayout

De stroomlay-out wordt dynamisch van aard zodra u de eigenschap estimatedItemSizeinstelt.

self.flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

#2. Ondersteuning toevoegen voor dimensionering op uw celsubklasse

Dit is er in 2 smaken; Automatische lay-out ofaangepaste overschrijving van preferredLayoutAttributesFittingAttributes.

Cellen maken en configureren met automatische lay-out

Ik zal hier niet in detail op ingaan, want er is een briljante SO berichtover het configureren van beperkingen voor een cel. Wees op uw hoede dat Xcode 6 brakeen heleboel dingen met iOS 7 dus, als je iOS 7 ondersteunt, moet je dingen doen zoals ervoor zorgen dat het masker voor automatisch aanpassen is ingesteld op de contentView van de cel en dat de grenzen van de contentView worden ingesteld als de grenzen van de cel wanneer de cel wordt geladen (dwz awakeFromNib).

Waar u zich wel van bewust moet zijn, is dat uw cel ernstiger moet worden beperkt dan een cel met tabelweergave. Als u bijvoorbeeld wilt dat uw breedte dynamisch is, heeft uw cel een hoogtebeperking nodig. Evenzo, als u wilt dat de hoogte dynamisch is, heeft u een breedtebeperking voor uw cel nodig.

Implementeer preferredLayoutAttributesFittingAttributesin uw aangepaste cel

Als deze functie wordt aangeroepen, is uw weergave al geconfigureerd met inhoud (d.w.z. cellForItemis aangeroepen). Ervan uitgaande dat uw beperkingen correct zijn ingesteld, zou u een implementatie als deze kunnen hebben:

//forces the system to do one layout pass
var isHeightCalculated: Bool = false
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    //Exhibit A - We need to cache our calculation to prevent a crash.
    if !isHeightCalculated {
        setNeedsLayout()
        layoutIfNeeded()
        let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)
        var newFrame = layoutAttributes.frame
        newFrame.size.width = CGFloat(ceilf(Float(size.width)))
        layoutAttributes.frame = newFrame
        isHeightCalculated = true
    }
    return layoutAttributes
}

OPMERKINGOp iOS 9 veranderde het gedrag een beetje dat crashes in uw implementatie zou kunnen veroorzaken als u niet voorzichtig bent (Bekijk meer hier). Wanneer u preferredLayoutAttributesFittingAttributesimplementeert, moet u ervoor zorgen dat u het frame van uw lay-outkenmerken slechts één keer wijzigt. Als u dit niet doet, roept de lay-out uw implementatie voor onbepaalde tijd op en crasht uiteindelijk. Een oplossing is om de berekende grootte in uw cel te cachen en deze ongeldig te maken wanneer u de cel opnieuw gebruikt of de inhoud ervan wijzigt, zoals ik heb gedaan met de eigenschap isHeightCalculated.

Ervaar uw lay-out

Op dit punt zou je ‘functionerende’ dynamische cellen in je collectionView moeten hebben. Ik heb de kant-en-klare oplossing nog niet voldoende gevonden tijdens mijn tests, dus voel je vrij om commentaar te geven als je dat hebt gedaan. Het voelt nog steeds alsof UITableViewde strijd om dynamische dimensionering IMHO wint.

##Voorbehoud
Houd er rekening mee dat als u prototypecellen gebruikt om de geschatteItemSize te berekenen, dit niet werkt als uw XIB gebruikt maatklassen. De reden hiervoor is dat wanneer u uw cel laadt vanuit een XIB, de grootteklasse wordt geconfigureerd met Undefined. Dit wordt alleen verbroken op iOS 8 en hoger, aangezien op iOS 7 de grootteklasse wordt geladen op basis van het apparaat (iPad = Regular-Any, iPhone = Compact-Any). U kunt de geschatteItemSize instellen zonder de XIB te laden, of u kunt de cel laden vanuit de XIB, deze toevoegen aan de collectionView (hiermee wordt de traitCollection ingesteld), de lay-out uitvoeren en deze vervolgens uit de superview verwijderen. Je kunt ook je cel de getter traitCollectionlaten overschrijven en de juiste eigenschappen teruggeven. Het is aan jou.

Laat het me weten als ik iets heb gemist, ik hoop dat ik heb geholpen en veel succes met coderen



Antwoord 2, autoriteit 19%

In iOS10 is er een nieuwe constante genaamd UICollectionViewFlowLayout.automaticSize(voorheen UICollectionViewFlowLayoutAutomaticSize), dus in plaats daarvan:

self.flowLayout.estimatedItemSize = CGSize(width: 100, height: 100)

u kunt dit gebruiken:

self.flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

Het presteert beter, vooral wanneer cellen in je collectieweergave een constante breedte hebben.

Toegang tot stroomindeling:

override func viewDidLoad() {
   super.viewDidLoad()
   if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
      flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
   }
}

Swift 5 bijgewerkt:

override func viewDidLoad() {
   super.viewDidLoad()
   if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
      flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
    }
}

Antwoord 3, autoriteit 13%

Een paar belangrijke wijzigingen in Daniel Galasko’s antwoordlosten al mijn problemen op. Helaas heb ik niet genoeg reputatie om (nog) rechtstreeks commentaar te geven.

In stap 1 voegt u bij het gebruik van Automatische lay-out eenvoudig een enkele bovenliggende UIView toe aan de cel. ALLES in de cel moet een subweergave van de ouder zijn. Dat beantwoordde al mijn problemen. Hoewel Xcode dit automatisch toevoegt voor UITableViewCells, doet het dat niet (maar zou het moeten) voor UICollectionViewCells. Volgens de docs:

Als u het uiterlijk van uw cel wilt configureren, voegt u de weergaven toe die nodig zijn om de inhoud van het gegevensitem als subweergaven te presenteren aan de weergave in de eigenschap contentView. Voeg subviews niet rechtstreeks toe aan de cel zelf.

Sla stap 3 dan helemaal over. Het is niet nodig.


Antwoord 4, autoriteit 10%

In iOS 10+ is dit een heel eenvoudig proces van twee stappen.

  1. Zorg ervoor dat al uw celinhoud binnen een enkele UIView wordt geplaatst (of binnen een afstammeling van UIView zoals UIStackView, wat de automatische lay-out aanzienlijk vereenvoudigt). Net als bij het dynamisch wijzigen van de grootte van UITableViewCells, moeten er voor de hele weergavehiërarchie beperkingen zijn geconfigureerd, van de buitenste container tot de binnenste weergave. Dat omvat beperkingen tussen de UICollectionViewCell en de directe onderliggende weergave

  2. Instrueer de stroomlay-out van uw UICollectionView automatisch op maat

    yourFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
    

Antwoord 5, autoriteit 4%

  1. FlowLayout toevoegen aan viewDidLoad()

    override func viewDidLoad() {
        super.viewDidLoad() 
        if let flowLayout = infoCollection.collectionViewLayout as? UICollectionViewFlowLayout {
            flowLayout.estimatedItemSize = CGSize(width: 1, height:1)
        }
    }
    
  2. Stel ook een UIView in als mainContainer voor uw cel en voeg alle vereiste weergaven erin toe.

  3. Raadpleeg deze geweldige, verbluffende tutorial voor meer informatie:
    UICollectionView met autosizing cel met autolayout in iOS 9 & amp; 10


Antwoord 6, autoriteit 4%

BEWERK 19-11-19: Gebruik voor iOS 13 UICollectionViewCompositionalLayout met geschatte hoogtes. Verspil geen tijd aan het omgaan met deze kapotte API.

Na hier enige tijd mee te hebben geworsteld, merkte ik dat het formaat wijzigen niet werkt voor UITextViews als je scrollen niet uitschakelt:

let textView = UITextView()
textView.scrollEnabled = false

Antwoord 7, autoriteit 2%

contentBekijk ankermysterie:

In een bizar geval dit

   contentView.translatesAutoresizingMaskIntoConstraints = false

zou niet werken. Vier expliciete ankers toegevoegd aan de contentView en het werkte.

class AnnoyingCell: UICollectionViewCell {
    @IBOutlet var word: UILabel!
    override init(frame: CGRect) {
        super.init(frame: frame); common() }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder); common() }
    private func common() {
        contentView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            contentView.leftAnchor.constraint(equalTo: leftAnchor),
            contentView.rightAnchor.constraint(equalTo: rightAnchor),
            contentView.topAnchor.constraint(equalTo: topAnchor),
            contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
    }
}

en zoals gewoonlijk

   estimatedItemSize = UICollectionViewFlowLayout.automaticSize

in YourLayout: UICollectionViewFlowLayout

Wie weet? Kan iemand helpen.

Tegoed

https://www.vadimbulavin.com/collection-view-cells -zelf aanpassen/

vond de tip daar – heb hem nergens anders gezien in alle 1000-artikelen hierover.


Antwoord 8

Ik heb een dynamische celhoogte van de collectieweergave gemaakt. Hier is git hub repo.

En zoek uit waarom preferredLayoutAttributesFittingAttributes meer dan eens wordt aangeroepen. Eigenlijk wordt er minimaal 3 keer gebeld.

De console log afbeelding:
voer hier de afbeeldingsbeschrijving in

1e voorkeurLayoutAttributesFittingAttributes:

(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa405c290e0> index path: (<NSIndexPath:    0xc000000000000016> 
{length = 2, path = 0 - 0}); frame = (15 12; 384 57.5); 
(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 0);

De layoutAttributes.frame.size.height heeft de huidige status 57,5.

2e voorkeurLayoutAttributesFittingAttributes:

(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa405c16370> index path: (<NSIndexPath: 0xc000000000000016> 
{length = 2, path = 0 - 0}); frame = (15 12; 384 534.5); 
(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 0);

De hoogte van het celframe is gewijzigd in 534,5zoals verwacht. Maar de collectieweergave is nog steeds nul.

3e voorkeurLayoutAttributesFittingAttributes:

(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa403d516a0> index path: (<NSIndexPath: 0xc000000000000016> 
{length = 2, path = 0 - 0}); frame = (15 12; 384 534.5); 
(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 477);

Je kunt zien dat de hoogte van de collectieweergave is gewijzigd van 0 in 477.

Het gedrag is vergelijkbaar met scroll handvat:

1. Before self-sizing cell
2. Validated self-sizing cell again after other cells recalculated.
3. Did changed self-sizing cell

In het begin dacht ik dat deze methode maar één keer kon worden aangeroepen. Dus ik codeerde als volgt:

CGRect frame = layoutAttributes.frame;
frame.size.height = frame.size.height + self.collectionView.contentSize.height;
UICollectionViewLayoutAttributes* newAttributes = [layoutAttributes copy];
newAttributes.frame = frame;
return newAttributes;

Deze regel:

frame.size.height = frame.size.height + self.collectionView.contentSize.height;

veroorzaakt systeemoproep oneindige lus en app-crash.

Elke grootte die wordt gewijzigd, valideert de geprefereerde LayoutAttributesFittingAttributes van alle cellen keer op keer totdat de posities van elke cel (d.w.z. frames) niet meer veranderen.


Antwoord 9

Naast bovenstaande antwoorden,

Zorg ervoor dat u de eigenschap estimatedItemSizevan UICollectionViewFlowLayoutop een bepaalde grootte instelt en nietsizeForItem:atIndexPathimplementeert gedelegeerde methode.

Dat is het.


Antwoord 10

De oplossing bestaat uit 3 eenvoudige stappen:

  1. Dynamische celgrootte inschakelen

flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

  1. Stel de containerView.widthAnchor.constraint van collectionView(:cellForItemAt:)in om de breedte van contentView te beperken tot de breedte van 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
        }
    }
    ....
}

Omdat je zelf-sizing van UITextView wilt inschakelen, heeft het een extra stap om;

3. Bereken en stel de heightAnchor.constant van UITextView in.

Dus, wanneer de breedte van contentView is ingesteld, passen we de hoogte van UITextView aan in didSetof maxWidth.

In UICollectionViewCell:

var maxWidth: CGFloat? {
    didSet {
        guard let maxWidth = maxWidth else {
            return
        }
        containerViewWidthAnchor.constant = maxWidth
        containerViewWidthAnchor.isActive = true
        let sizeToFitIn = CGSize(width: maxWidth, height: CGFloat(MAXFLOAT))
        let newSize = self.textView.sizeThatFits(sizeToFitIn)
        self.textViewHeightContraint.constant = newSize.height
    }
}

Met deze stappen krijgt u het gewenste resultaat.

Volledig uitvoerbare gist

Referentie: Vadim Bulavinblogpost – Collection View Cells Self-Sizing: stapsgewijze zelfstudie

Schermafbeelding:

voer hier de afbeeldingsbeschrijving in


Antwoord 11

Als u de UICollectionViewDelegateFlowLayout-methode implementeert:

- (CGSize)collectionView:(UICollectionView*)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath*)indexPath

Als u collectionview performBatchUpdates:completion:aanroept, gebruikt de hoogte van de grootte sizeForItemAtIndexPathin plaats van
preferredLayoutAttributesFittingAttributes.

Het weergaveproces van performBatchUpdates:completiondoorloopt de methode preferredLayoutAttributesFittingAttributesmaar negeert uw wijzigingen.


Antwoord 12

Aan wie het kan helpen,

Ik had die vervelende crash als estimatedItemSizewas ingesteld. Zelfs als ik 0 retourneerde in numberOfItemsInSection. Daarom waren de cellen zelf en hun automatische lay-out niet de oorzaak van de crash… De collectionView crashte gewoon, zelfs als hij leeg was, alleen maar omdat estimatedItemSizewas ingesteld op zelf-sizing.

In mijn geval heb ik mijn project gereorganiseerd, van een controller met een collectionView naar een collectionViewController, en het werkte.

Kijk maar.


Antwoord 13

Voor iedereen die alles zonder geluk heeft geprobeerd, dit is het enige waardoor het voor mij werkte.
Voor de labels met meerdere regels in de cel, probeer deze magische lijn toe te voegen:

label.preferredMaxLayoutWidth = 200

Meer info: hier

Proost!


Antwoord 14

De bovenstaande voorbeeldmethode compileert niet. Hier is een gecorrigeerde versie (maar niet getest of het werkt.)

override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes 
{
    let attr: UICollectionViewLayoutAttributes = layoutAttributes.copy() as! UICollectionViewLayoutAttributes
    var newFrame = attr.frame
    self.frame = newFrame
    self.setNeedsLayout()
    self.layoutIfNeeded()
    let desiredHeight: CGFloat = self.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
    newFrame.size.height = desiredHeight
    attr.frame = newFrame
    return attr
}

Antwoord 15

Meer informatie bijwerken:

  • Als u flowLayout.estimatedItemSizegebruikt, raadt u aan een latere versie van iOS8.3 te gebruiken. Vóór iOS8.3 crasht het [super layoutAttributesForElementsInRect:rect];.
    De foutmelding is

    *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'

  • Ten tweede, in iOS8.x-versie, zal flowLayout.estimatedItemSizeervoor zorgen dat een andere instelling voor sectie-inzet niet werkte. d.w.z. functie: (UIEdgeInsets)collectionView:layout:insetForSectionAtIndex:.

Other episodes