UICollectionView, komórki o pełnej szerokości, czy zezwolić na automatyczne układanie dynamicznej wysokości?

120

W pionie UICollectionView,

Czy można mieć komórki o pełnej szerokości , ale zezwolić na kontrolowanie wysokości dynamicznej przez autoukładanie ?

To wydaje mi się być może „najważniejszym pytaniem w iOS, na które nie ma dobrej odpowiedzi”.


Ważny:

Zwróć uwagę, że w 99% przypadków, aby uzyskać komórki o pełnej szerokości + autoukładanie dynamicznej wysokości, po prostu użyj widoku tabeli. To jest takie proste.


Więc jaki jest przykład, gdzie potrzebujesz widoku kolekcji?

Widoki kolekcji są niewiarygodnie potężniejsze niż widoki tabel.

Jeden prosty przykład, w którym w rzeczywistości musisz użyć widoku kolekcji, aby uzyskać komórki o pełnej szerokości + automatycznie układającą się dynamiczną wysokość:

Jeśli animujesz między dwoma układami w widoku kolekcji. Na przykład układ między 1 a 2 kolumnami, gdy urządzenie się obraca.

To powszechny i ​​normalny idiom w iOS. Niestety można to osiągnąć tylko poprzez rozwiązanie problemu przedstawionego w tej kontroli jakości. : - /

Fattie
źródło
tak jest możliwe dzięki klasie UICollectionViewDelegateFlowLayout
Sunil Prajapati
czy nie oznacza to, że faktycznie można użyć tableView? jaka dodatkowa funkcjonalność byłaby potrzebna, aby to skomplikować?
Catalina T.
1
Cześć @CatalinaT. , widoki kolekcji mają wiele, wiele, wiele dodatkowych funkcji w porównaniu z widokami tabeli. Prostym przykładem jest dodanie fizyki. (Jeśli nie wiesz, co mam na myśli, to w jaki sposób tworzysz listy „odbijające” w iOS.)
Fattie
możesz także zastosować fizykę (jak SpringDampingi initialSpringVelocity) w tabeli Cell ..... spójrz na tę listę Bouncy używając tableView ,,,, pastebin.com/pFRQhkrX możesz animować TableViewCelltabelę w willDisplayCell:(UITableViewCell *)cellmetodzie delgate @Fattie możesz zaakceptować odpowiedź, która rozwiązała Twój problem, aby pomóc innym, którzy szukają tego samego problemu
Dhiru
ohk, czy wypróbowałeś listę Bouncy w widoku listy? @Fattie
Dhiru

Odpowiedzi:

124

1. Rozwiązanie dla iOS 13+

Dzięki Swift 5.1 i iOS 13 możesz użyć obiektów układu kompozycyjnego w celu rozwiązania problemu.

Poniższy kompletny przykładowy kod pokazuje, jak wyświetlić multilinię UILabelw pełnej szerokości 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")
    }

}

Oczekiwany wyświetlacz:

wprowadź opis obrazu tutaj


2. Rozwiązanie dla iOS 11+

W Swift 5.1 i iOS 11 możesz podklasować UICollectionViewFlowLayouti ustawić jego estimatedItemSizewłaściwość na UICollectionViewFlowLayout.automaticSize(to mówi systemowi, że chcesz zająć się autorezacją UICollectionViewCell). Będziesz wtedy musiał nadpisać layoutAttributesForElements(in:)i layoutAttributesForItem(at:)ustawić szerokość komórek. Na koniec będziesz musiał nadpisać preferredLayoutAttributesFitting(_:)metodę swojej komórki i obliczyć jej wysokość.

Poniższy kompletny kod pokazuje, jak wyświetlić multilinię UILabelwewnątrz pełnej szerokości UIcollectionViewCell(ograniczone przez UICollectionViewbezpieczny obszar i UICollectionViewFlowLayoutwstawki):

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
    }

}

Oto kilka alternatywnych implementacji 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
}

Oczekiwany wyświetlacz:

wprowadź opis obrazu tutaj

Imanou Petit
źródło
Działało to dobrze, dopóki nie dodałem nagłówków sekcji. Nagłówki sekcji są umieszczone w niewłaściwym miejscu i zakrywają inne komórki w iOS 11. Ale działa dobrze w iOS 12. Czy napotkałeś jakiś problem z dodawaniem nagłówków sekcji?
Nakul
Dziękuję bardzo. Uratowałeś mój tydzień. 🙇‍♂️
Gaetan
1
CollectionView przewija się do góry, gdy zmienia się rozmiar
Sajith
1
fantastyczne wieści, @ImanouPetit! Chciałbym, żeby nagroda była większa! Fantastyczne informacje o obiektach w układzie kompozycyjnym, dzięki.
Fattie
1
Cześć, próbuję tego. Mimo że ustawiam układ w viewdidload, tak jak zrobiłeś to powyżej, pojawia się awaria informująca, że ​​widok kolekcji musi zostać zainicjowany z parametrem układu innym niż zero. Czy jest coś, czego mi brakuje? To jest Xcode 11 dla iOS 13.
geistmate
29

Problem

Szukasz automatycznej wysokości, a także chcesz mieć pełną szerokość, nie jest możliwe uzyskanie obu w użyciu UICollectionViewFlowLayoutAutomaticSize.

Chcesz to zrobić, UICollectionViewwięc poniżej jest rozwiązanie dla Ciebie.

Rozwiązanie

Krok I : Oblicz oczekiwaną wysokość komórki

1. Jeśli masz tylko UILabel w CollectionViewCellniż ustawiono numberOfLines=0i obliczono oczekiwaną wysokość UIlable, przekaż wszystkie trzy parametry

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. Jeśli twój CollectionViewCellzawiera tylko UIImageViewi jeśli przypuszczalnie ma być dynamiczny w Height, niż potrzebujesz uzyskać wysokość UIImage ( UIImageViewmusisz mieć AspectRatioograniczenia)

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

3. Jeśli zawiera oba, oblicz ich wysokość i zsumuj je.

KROK II : Zwróć rozmiarCollectionViewCell

1. Dodaj UICollectionViewDelegateFlowLayoutdelegata w swoim viewController

2. Zaimplementuj metodę delegata

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

Mam nadzieję, że to ci pomoże.

Dhiru
źródło
1
Czy możliwe jest przewijanie w poziomie UICollectionView z wysokością komórek rosnącą w pionie z automatycznym układem?
nr5
Chcę osiągnąć tę samą, ale połowę szerokości i dynamiczną wysokość
Mihir Mehta
return CGSize(width: view.frame.width/2, height: expectedHeight)również zachowaj Padding na koncie, niż zwróć szerokość, w przeciwnym razie dwie komórki nie zmieściłyby się w jednym wierszu.
Dhiru
@ nr5 Czy otrzymałeś rozwiązanie? Moje wymagania są takie same jak Twoje. Dzięki.
Maulik Bhuptani
@MaulikBhuptani Nie mogłem tego zrobić całkowicie z Auto Layout. Musiałem utworzyć niestandardową klasę Layout i zastąpić niektóre metody klas.
nr5
18

Istnieje kilka sposobów rozwiązania tego problemu.

Jednym ze sposobów jest nadanie układowi przepływu widoku kolekcji szacunkowego rozmiaru i obliczenie rozmiaru komórki.

Uwaga: jak wspomniano w komentarzach poniżej, od iOS 10 nie musisz już podawać i szacować rozmiaru, aby wywołać wywołanie komórek func preferredLayoutAttributesFitting(_ layoutAttributes:). Wcześniej (iOS 9) wymagało podania szacunkowego rozmiaru, jeśli chcesz zapytać o komórki prefferedLayoutAttributes.

(zakładając, że używasz scenorysów, a widok kolekcji jest połączony przez IB)

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

W przypadku prostych komórek zwykle wystarczy. Jeśli rozmiar jest nadal nieprawidłowy, w komórce widoku kolekcji można nadpisać func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes, co zapewni dokładniejszą kontrolę nad rozmiarem komórki. Uwaga: nadal musisz podać szacunkowy rozmiar układu przepływu .

Następnie zastąp, func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributesaby zwrócić prawidłowy rozmiar.

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
}

Alternatywnie możesz użyć komórki rozmiaru, aby obliczyć rozmiar komórki w 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)
}

Chociaż nie są to doskonałe przykłady gotowe do produkcji, powinny one pomóc Ci zacząć we właściwym kierunku. Nie mogę powiedzieć, że jest to najlepsza praktyka, ale działa to dla mnie, nawet w przypadku dość złożonych komórek zawierających wiele etykiet, które mogą, ale nie muszą, zawijać się w wiele wierszy.

Eric Murphey
źródło
@Fattie Niejasne, dlaczego „esitmatedItemSize” jest nieistotne dla komórki widoku kolekcji o dynamicznym rozmiarze, więc chciałbym poznać Twoje przemyślenia. (nie sarkazm)
Eric Murphey
Dodatkowo, jeśli w tej odpowiedzi brakuje czegoś konkretnego, co mogłoby być pomocne i / lub tego, co uważasz za kanoniczne, daj mi znać. Nie będę się kłócił o nagrodę, po prostu chciałbym być pomocny każdemu, kto widzi to pytanie.
Eric Murphey
1
(w tym lub nie uwzględnienie elementu RatedItemSize nie ma znaczenia w obecnym iOS i nie jest to żadna pomoc, tak czy inaczej, w kwestii „pełna szerokość + dynamiczna wysokość”). Kod na końcu, końcówki są obecnie rzadko używane i ma małe znaczenie dla dynamicznej wysokości (na przykład z kilku widoków tekstu z dynamiczną wysokością). dzięki
Fattie
Doszedłem również do tego rozwiązania, ale w moim przypadku iOS 11, Xcode 9.2 zawartość komórki wydawała się odłączona od samej komórki - nie mogłem uzyskać obrazu, aby rozszerzył się do pełnej wysokości lub szerokości ustalonego wymiaru. Ponadto zauważyłem, że CV było ograniczeniami ustawień w preferredLayoutAttributesFitting. Następnie dodałem własne ograniczenie we wspomnianej funkcji, wiążące się ze stałym wymiarem z assessmentItemSize, FWIW, zobacz moją odpowiedź poniżej.
Chris Conover,
16

Znalazłem całkiem proste rozwiązanie tego problemu: wewnątrz mojej CollectionViewCell otrzymałem UIView (), który jest właściwie tylko tłem. Aby uzyskać pełną szerokość, po prostu ustawiłem następujące kotwice

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

„Magia” dzieje się w pierwszej linii. Ustawiłem widthAnchor dynamicznie na szerokość ekranu. Ważne jest również odjęcie wstawek z CollectionView. W przeciwnym razie komórka się nie pojawi. Jeśli nie chcesz mieć takiego widoku tła, po prostu uczyń go niewidocznym.

FlowLayout używa następujących ustawień

layout.itemSize = UICollectionViewFlowLayoutAutomaticSize
layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize

Wynikiem jest komórka o pełnej szerokości i dynamicznej wysokości.

wprowadź opis obrazu tutaj

inf1783
źródło
1
święta krowa - to niesamowite! tak oczywiste ! znakomity !
Fattie
1
tylko TBC @ inf1783, robisz to w UICollectionView? (nie widok tabeli) - prawda?
Fattie
1
@Fattie Tak, to UICollectionView;)
inf1783
fantastycznie @ inf1783, nie mogę się doczekać, aby to wypróbować. Nawiasem mówiąc, innym dobrym sposobem na zrobienie tego jest podklasa UIView i po prostu dodanie wewnętrznej szerokości (zgaduję, że miałoby to ten sam efekt i prawdopodobnie sprawiłoby, że działałoby również w czasie tworzenia scenorysu)
Fattie
1
Za każdym razem, gdy próbuję tego, kończę w nieskończonej pętli z Zachowanie UICollectionViewFlowLayout nie jest zdefiniowane błąd: /
Skodik.o
7

PRACUJĄCY!!! Przetestowano na IOS: 12.1 Swift 4.1

Mam bardzo proste rozwiązanie, które po prostu działa bez łamania ograniczeń.

wprowadź opis obrazu tutaj

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


}

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

}

Głównym składnikiem jest systemLayoutSizeFitting funkcja w Customcell. Musimy także ustawić szerokość widoku wewnątrz komórki z ograniczeniami.

Abhishek Biswas
źródło
6

Musisz dodać ograniczenie szerokości do CollectionViewCell

class SelfSizingCell: UICollectionViewCell {

  override func awakeFromNib() {
      super.awakeFromNib()
      contentView.translatesAutoresizingMaskIntoConstraints = false
      contentView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
  }
}
Mecid
źródło
w końcu ktoś zrobił to perfekcyjnie w dwóch liniach
Omar N Shamali
Ograniczy to komórkę do stałej szerokości, prawie nie zmieniającej się samoczynnie. Jeśli obrócisz urządzenie lub dostosujesz rozmiar ekranu na iPadzie, pozostanie nieruchomy.
Thomas Verbeek
4
  1. Zestaw estimatedItemSizeukładu przepływu:

    collectionViewLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
  2. Zdefiniuj ograniczenie szerokości w komórce i ustaw je tak, aby było równe szerokości 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()
        }
    
        ...
    }

Pełny przykład

Przetestowano na iOS 11.

rzymski
źródło
3

Osobiście znalazłem najlepsze sposoby na uzyskanie UICollectionView, w którym AutoLayout określa rozmiar, podczas gdy każda komórka może mieć inny rozmiar, polega na zaimplementowaniu funkcji UICollectionViewDelegateFlowLayout sizeForItemAtIndexPath podczas używania rzeczywistej komórki do pomiaru rozmiaru.

Mówiłem o tym w jednym z moich postów na blogu

Mam nadzieję, że ten pomoże Ci osiągnąć to, czego chcesz. Nie jestem pewien w 100%, ale uważam, że w przeciwieństwie do UITableView, w którym można faktycznie uzyskać w pełni automatyczną wysokość komórek, używając funkcji AutoLayout w połączeniu z

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44

UICollectionView nie ma takiego sposobu, aby pozwolić AutoLayout określić rozmiar, ponieważ UICollectionViewCell niekoniecznie wypełnia całą szerokość ekranu.

Ale oto pytanie do Ciebie : jeśli potrzebujesz komórek o pełnej szerokości ekranu, dlaczego w ogóle zawracasz sobie głowę używaniem UICollectionView zamiast starego dobrego UITableView, który jest dostarczany z komórkami o automatycznym rozmiarze?

xxtesaxx
źródło
trzymaj to, Aprit poniżej mówi, że UICollectionViewFlowLayoutAutomaticSize jest teraz dostępny w układzie przepływu, w iOS10 ...
Fattie
Właśnie to widziałem. Wygląda na to, że teraz jest to rzecz w iOS 10. Właśnie obejrzałem odpowiednią rozmowę WWDC na ten temat: developer.apple.com/videos/play/wwdc2016/219 Jednak to całkowicie automatycznie dostosuje rozmiar komórki do jej zawartości. Nie jestem pewien, jak możesz powiedzieć komórce (z AutoLayout), aby wypełniła szerokość ekranu, ponieważ nie możesz ustawić ograniczenia między UICollectionViewCell a jego rodzicem (przynajmniej nie w StoryBoards)
xxtesaxx
dobrze. wydaje się, że nie jesteśmy bliżej rzeczywistego rozwiązania tego niezwykle oczywistego problemu. : /
Fattie
Zgodnie z wykładem, nadal możesz nadpisać sizeThatFits () lub preferowanyLayoutAttributesFitting () i samodzielnie obliczyć rozmiar, ale myślę, że nie o to prosił OP. W każdym razie nadal byłbym zainteresowany przypadkiem użycia, kiedy on / ona potrzebuje UICollectionViewCell o pełnej szerokości i dlaczego byłoby tak wielką sprawą nie używać UITableView w tym konkretnym przypadku (z pewnością mogą być pewne przypadki, ale myślę, że po prostu musisz sobie poradzić z samodzielnym wykonaniem niektórych obliczeń)
xxtesaxx,
myśląc o tym, jest całkiem niewiarygodne, że nie można po prostu ustawić „kolumn == 1” (a potem być może kolumn == 2, gdy urządzenie jest ustawione bokiem), z widokami kolekcji.
Fattie
3

Zgodnie z moim komentarzem do odpowiedzi Erica, moje rozwiązanie jest bardzo podobne do jego, ale musiałem dodać ograniczenie w preferowanym rozmiarze dla ... w celu ograniczenia do stałego wymiaru.

    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
    }

To pytanie ma wiele duplikatów, szczegółowo odpowiedziałem na nie tutaj i przedstawiłem tutaj działającą przykładową aplikację.

Chris Conover
źródło
2

Nie jestem pewien, czy kwalifikuje się to jako „naprawdę dobra odpowiedź”, ale właśnie tego używam, aby to osiągnąć. Mój układ przepływu jest poziomy i próbuję dopasować szerokość za pomocą automatycznego układu, więc jest podobny do twojej sytuacji.

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

Następnie w kontrolerze widoku, w którym ograniczenie autoukładu jest modyfikowane, uruchamiam NSNotification.

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

W mojej podklasie UICollectionView nasłuchuję tego powiadomienia:

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

i unieważnić układ:

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

Powoduje sizeForItemAtto ponowne wywołanie przy użyciu nowego rozmiaru widoku kolekcji. W twoim przypadku powinna być w stanie zaktualizować, biorąc pod uwagę nowe ograniczenia dostępne w układzie.

Mark Suman
źródło
tylko TBC Mark, czy masz na myśli, że faktycznie zmieniasz rozmiar komórki? (tj. pojawia się komórka i ma rozmiar X, wtedy coś dzieje się w Twojej aplikacji i staje się rozmiar Y ..?) thx
Fattie
Tak. Gdy użytkownik przeciąga element na ekranie, wyzwalane jest zdarzenie, które aktualizuje rozmiar komórki.
Mark Suman
1

Na twoim viewDidLayoutSubviewsustaw na estimatedItemSizepełną szerokość (układ odwołuje się do obiektu UICollectionViewFlowLayout):

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

W komórce upewnij się, że ograniczenia dotykają zarówno góry, jak i dołu komórki (poniższy kod używa Kartografii, aby uprościć ustawianie ograniczeń, ale możesz to zrobić za pomocą NSLayoutConstraint lub IB, jeśli chcesz):

constrain(self, nameLabel, valueLabel) { view, name, value in
        name.top == view.top + 10
        name.left == view.left
        name.bottom == view.bottom - 10
        value.right == view.right
        value.centerY == view.centerY
    }

Voila, twoje komórki będą teraz automatycznie rosły!

Raphael
źródło
0

Żadne z rozwiązań nie działało dla mnie, ponieważ potrzebuję dynamicznej szerokości, aby dostosować szerokość między iPhone'ami.

    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()
        }
    }
Przepływ w iOS
źródło
0

Od iOS 10 mamy do tego nowy interfejs API w układzie przepływu.

Wszystko co musisz zrobić, to ustawić swój flowLayout.estimatedItemSizedo nowej stałej, UICollectionViewFlowLayoutAutomaticSize.

Źródło

Arpit Dongre
źródło
Hmm, jesteś prawie pewien, że to * pełna szerokość ? Więc zasadniczo jedna kolumna?
Fattie
Może się tak zdarzyć, jeśli jawnie ustawisz szerokość UICollectionViewCell na pełną szerokość.
Arpit Dongre
Nie, nie ustawia szerokości do pełnej wysokości i ignoruje szacowaną wartość rozmiaru, chyba że zastosujesz się do odpowiedzi Erica powyżej lub mojej poniżej / później.
Chris Conover,
OK, niestety nie ma to absolutnie żadnego związku z problemem! he! : O
Fattie,
0

AutoLayout może służyć do automatycznego określania rozmiaru komórek w CollectionView w 2 prostych krokach:

  1. Włączanie dynamicznego określania rozmiaru komórek

flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

  1. Miej widok kontenera i ustaw containerView.widthAnchor.constraint from, collectionView(:cellForItemAt:)aby ograniczyć szerokość contentView do width of 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
        }
    }

    ....
}

To wszystko, uzyskasz pożądany efekt. Zapoznaj się z poniższymi streszczeniami, aby uzyskać pełny kod:

Referencje / kredyty:

Zrzut ekranu: wprowadź opis obrazu tutaj

JolasJoe
źródło
-6

Musisz odziedziczyć klasę UICollectionViewDelegateFlowLayout w swojej collectionViewController. Następnie dodaj funkcję:

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

Używając tego, masz rozmiar szerokości ekranu.

Teraz masz collectionViewController z wierszami jako tableViewController.

Jeśli chcesz, aby wysokość każdej komórki była dynamiczna, być może powinieneś utworzyć niestandardowe komórki dla każdej potrzebnej komórki.

Edu
źródło
7
To nie jest dokładnie odpowiedź na pytanie :) To dałoby stałą wysokość 100, czyli tak, jak zaprogramowano przed istnieniem autoukładu.
Fattie