web-dev-qa-db-fra.com

Dans iOS 12, quand les cellules de mise en page UICollectionView fonctionnent-elles, utilisez autolayout dans nib

Même code comme celui-ci

collectionLayout.estimatedItemSize = CGSize(width: 50, height: 25)
collectionLayout.itemSize = UICollectionViewFlowLayoutAutomaticSize
collectionLayout.minimumInteritemSpacing = 10 

for _ in 0 ..< 1000 {
    let length = Int(arc4random() % 8)
    let string = randomKeyByBitLength(length)
    array.append(string!)
}
collectionView.reloadData()

contraintes de cellule:

 enter image description here

quand je le lance sur iOS 12, c'est différent . le simulateur de gauche est iOS 11, et à droite, iOS 12:

 enter image description here

Mais, lorsque je le ferai défiler, les cadres des cellules seront normaux.


Exemple de projet pour reproduire le problème: https://github.com/Coeur/StackOverflow51375566

27
EvilHydra

Pour toutes les solutions, notez qu'il n'est pas nécessaire d'appeler explicitement reloadData dans viewDidLoad: cela se produira automatiquement.

Solution 1

Inspiré par Idée Samantha : invalidateLayout de manière asynchrone dans viewDidLoad.

override func viewDidLoad() {
    super.viewDidLoad()

    //[...]

    for _ in 0 ..< 1000 {
        array.append(randomKeyByBitLength(Int(arc4random_uniform(8)))!)
    }

    DispatchQueue.main.async {
        self.collectionView.collectionViewLayout.invalidateLayout()
    }
}

Solution 2

(imparfait, voir amélioration DHennessy13 dessus)

Basé sur Peter Lapisu répond . invalidateLayout in viewWillLayoutSubviews.

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    collectionView.collectionViewLayout.invalidateLayout()
}

Comme l'a noté DHennessy13, cette solution actuelle avec viewWillLayoutSubviews est imparfaite, car elle invalidera Layout lors de la rotation de l'écran.

Vous pouvez suivre amélioration DHennessy13 concernant cette solution.

Solution 3

Basé sur une combinaison de Tyler Sheaffer répond , Le port de Shawn Aukstak à Swift et l’idée de Samantha. Sous-classez votre CollectionView pour exécuter invalidateLayout sur layoutSubviews.

class AutoLayoutCollectionView: UICollectionView {

    private var shouldInvalidateLayout = false

    override func layoutSubviews() {
        super.layoutSubviews()
        if shouldInvalidateLayout {
            collectionViewLayout.invalidateLayout()
            shouldInvalidateLayout = false
        }
    }

    override func reloadData() {
        shouldInvalidateLayout = true
        super.reloadData()
    }
}

Cette solution est élégante car elle ne nécessite pas de changer votre code ViewController. Je l'ai implémenté sur la branche AutoLayoutCollectionView de cet exemple de projet https://github.com/Coeur/StackOverflow51375566/tree/AutoLayoutCollectionView .

Solution 4

Réécrivez les contraintes par défaut UICollectionViewCell. Voir Larry répondre .

Solution 5

Implémentez collectionView(_:layout:sizeForItemAt:) et retournez cell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize). Voir réponse mate .

20
Cœur

Voici une autre solution qui fonctionne sur l'échantillon de code de Cœur et qui a également fonctionné pour mon cas particulier, où les autres réponses ne fonctionnaient pas. Le code ci-dessous remplace la précédente implémentation de la sous-classe CollectionViewCell dans ViewController.Swift:

class CollectionViewCell: UICollectionViewCell {
    @IBOutlet weak var label: UILabel!

    override func awakeFromNib() {
        super.awakeFromNib()

        contentView.translatesAutoresizingMaskIntoConstraints = false

        let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor)
        let rightConstraint = contentView.rightAnchor.constraint(equalTo: rightAnchor)
        let topConstraint = contentView.topAnchor.constraint(equalTo: topAnchor)
        let bottomConstraint = contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
        NSLayoutConstraint.activate([leftConstraint, rightConstraint, topConstraint, bottomConstraint])
    }
}

Ceci est inspiré par la réponse de ale84 de UICollectionViewFlowLayout EstiméItemSize ne fonctionne pas correctement avec iOS12 bien que cela fonctionne correctement avec iOS 11. *

19
Larry

Le problème est que la caractéristique proposée ici - les cellules de vue de collection qui se dimensionnent en fonction de leurs contraintes internes - n'existe pas. Il a (jamais} _ existé. Apple prétend que c'est le cas, mais ce n'est pas le cas. J'ai déposé un bogue à ce sujet chaque année depuis que les vues de collection ont été introduites et que cette réclamation a été faite pour la première fois; et mes rapports de bogues n'ont jamais été fermés, car le bogue est réel. Il n’existe pas de cellules à affichage de collection auto-dimensionnées.

Voir aussi ma réponse ici: https://stackoverflow.com/a/51585910/341994 \

Certaines années, l’essai d’utiliser des cellules à auto-dimensionnement s’est écrasé. Les autres années, cela ne plante pas, mais la mise en page est incorrecte. Mais ça ne fonctionne pas.

La seule manière de faire ce genre de chose consiste à implémenter la méthode de délégué sizeForItemAt et à fournir la taille vous-même. Vous pouvez facilement le faire en appelant 

cell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)

sur une cellule modèle que vous avez configurée à l'avance. C'est ce que le moteur d'exécution {devrait} _ fait pour vous - mais ce n'est pas le cas.

Alors voici ma solution à la question initiale. Au lieu d'un simple tableau de chaînes, nous avons généré un tableau de paires taille-chaîne (sous forme de tuple). Ensuite:

override func collectionView(_ collectionView: UICollectionView, 
    cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! MyCell
        cell.label.text = self.array[indexPath.row].0
        return cell
}

func collectionView(_ collectionView: UICollectionView, 
    layout collectionViewLayout: UICollectionViewLayout, 
    sizeForItemAt indexPath: IndexPath) -> CGSize {
        return self.array[indexPath.row].1
}
9
matt

J'ai le même problème, les cellules utilisent la taille estimée (au lieu de la taille automatique) jusqu'au défilement. Le même code construit avec Xcode 9.x fonctionne parfaitement sur iOS 11 et 12, et construit dans Xcode 10, il fonctionne correctement sur iOS 11 mais pas sur iOS 12.

La seule solution que j’ai trouvée jusqu’à présent pour résoudre ce problème consiste à invalider la présentation de la vue de collection, soit dans viewDidAppear, ce qui peut causer des sauts, ou dans un bloc asynchrone dans viewWillAppear (je ne sais pas dans quelle mesure cette solution est fiable).

override func viewDidLoad() {
    super.viewDidLoad()
    let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout
    layout?.estimatedItemSize = CGSize(width: 50, height: 50)
    layout?.itemSize = UICollectionViewFlowLayout.automaticSize
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    // The following block also "fixes" the problem without jumpiness after the view has already appeared on screen.
    DispatchQueue.main.async {
        self.collectionView.collectionViewLayout.invalidateLayout()
    }
}

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    // The following line makes cells size properly in iOS 12.
    collectionView.collectionViewLayout.invalidateLayout()
}
8
Samantha

La solution 2 de Cœur 'empêche la mise en page de clignoter ou de se mettre à jour devant l'utilisateur. Mais cela peut créer des problèmes lorsque vous faites pivoter l'appareil. J'utilise une variable "shouldInvalidateLayout" dans viewWillLayoutSubviews et la définit sur false dans viewDidAppear.

private var shouldInvalidateLayout = true

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    shouldInvalidateLayout = false
}

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    if shouldInvalidateLayout {
        collectionView.collectionViewLayout.invalidateLayout()
    }
}
6
DHennessy13

Nous avons eu le même problème sur notre projet. Nous avons également remarqué des différences entre les multiples périphériques dans iOS 12, nécessitant un appel à layoutIfNeeded & invalidateLayout. La solution est basée sur l'approche de @ DHennessy13 mais ne nécessite pas de booléen pour gérer des états qui semblaient légèrement hacky.

Ici, il est basé sur un code Rx, la première ligne concerne essentiellement le moment où les données changent. Dans la variable subscribe, il convient de corriger le problème lié au problème méchant de l'interface utilisateur iOS 12:

        viewModel.cellModels.asObservable()
            .subscribe(onNext: { [weak self] _ in
                // iOS 12 bug in UICollectionView for cell size
                self?.collectionView.layoutIfNeeded()

                // required for iPhone 8 iOS 12 bug cell size
                self?.collectionView.collectionViewLayout.invalidateLayout()
            })
            .disposed(by: rx.disposeBag)

Modifier:

À propos, cela semble être un problème connu sous iOS 12: https://developer.Apple.com/documentation/ios_release_notes/ios_12_release_notes (dans la section UIKit).

0
Toka

Essaye ça

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()

    DispatchQueue.main.async {
        self.collectionView.collectionViewLayout.invalidateLayout()
    }
}

Ajouter à viewDidAppear et viewWillAppear fonctionnera bien sûr. Mais viewDidAppear causera un problème technique à l'utilisateur.

0
Sreedeepkesav M S