web-dev-qa-db-fra.com

Taille d'en-tête UICollectionView dynamique basée sur UILabel

J'ai lu de nombreux articles sur l'ajout d'en-tête à UICollectionView. Dans une application iOS 7+ dans Swift, j'essaie d'ajouter un en-tête avec un UILabel dont la hauteur doit être ajustée en fonction de la hauteur de UILabel. UILabel a des lignes = 0.

J'ai configuré l'en-tête dans IB avec AutoLayout

enter image description here

Le ViewController implémente UICollectionViewDelegate, UICollectionViewDataSource. Je n'ai pas configuré de classe personnalisée pour l'en-tête, mais j'utilise ces deux fonctions:

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
      //description is a String variable defined in the class
    let size:CGSize = (description as NSString).boundingRectWithSize(CGSizeMake(CGRectGetWidth(collectionView.bounds) - 20.0, 180.0), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: [NSFontAttributeName: UIFont(name: "Helvetica Neue", size: 16.0)], context: nil).size
    return CGSizeMake(CGRectGetWidth(collectionView.bounds), ceil(size.height))
}

func collectionView(collectionView: UICollectionView!, viewForSupplementaryElementOfKind kind: String!, atIndexPath indexPath: NSIndexPath!) -> UICollectionReusableView! {
    var reusableview:UICollectionReusableView = UICollectionReusableView()
    if (kind == UICollectionElementKindSectionHeader) {
                    //listCollectionView is an @IBOutlet UICollectionView defined at class level, using collectionView crashes
            reusableview = listCollectionView.dequeueReusableSupplementaryViewOfKind(UICollectionElementKindSectionHeader, withReuseIdentifier: "ListHeader", forIndexPath: indexPath) as UICollectionReusableView
            let label = reusableview.viewWithTag(200) as UILabel  //the UILabel within the header is tagged with 200
            label.text = description   //description is a String variable defined in the class
        }
    }
    return reusableview
}

L'affichage du texte semble fonctionner mais le calcul de la hauteur ne semble pas fonctionner (voir la capture d'écran ci-dessous). De plus, je ne pense pas non plus que je puisse accéder à UILabel via la fonction collectionView...referenceSizeForHeaderInSection. Des suggestions sur la façon de calculer correctement CGSize?

enter image description here

23
Dean

Voici comment je l'ai fait:

let labels = [
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ac lorem enim. Curabitur rhoncus efficitur quam, et pretium ipsum. Nam eu magna at velit sollicitudin fringilla nec nec nisi. Quisque nec enim et ipsum feugiat pretium. Vestibulum hendrerit arcu ut ipsum gravida, ut tincidunt justo pellentesque. Etiam lacus ligula, aliquet at lorem vel, ullamcorper commodo turpis. Nullam commodo sollicitudin mauris eu faucibus.",
"Lorem ipsum dolor",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ac lorem enim. Curabitur rhoncus efficitur quam, et pretium ipsum. Nam eu magna at velit sollicitudin fringilla nec nec nisi. Quisque nec enim et ipsum feugiat pretium."]

L'idée de base est de créer une variable UILabel identique à celle qui sera affichée dans l'en-tête de la section. Cette étiquette sera utilisée pour définir la taille souhaitée pour l'en-tête dans la méthode referenceSizeForHeaderInSection.

J'ai une sortie étiquette nommée label dans ma sous-classe UICollectionReusableView (MyHeaderCollectionReusableView), que j'utilise pour la vue d'en-tête de section en l'affectant dans le storyboard (en définissant "MyHeader" comme identificateur de réutilisation pour la vue en coupe). Cette étiquette mentionnée a les contraintes d’espace horizontal et vertical aux frontières de l’en-tête de la section afin de pouvoir effectuer une autolayout correctement.

override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 3
    }

override func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {

        let headerView =
        collectionView.dequeueReusableSupplementaryViewOfKind(kind,
            withReuseIdentifier: "MyHeader",
            forIndexPath: indexPath)
            as MyHeaderCollectionReusableView

        headerView.label.text = labels[indexPath.section]

        return headerView

    }

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        // that -16 is because I have 8px for left and right spacing constraints for the label.
        let label:UILabel = UILabel(frame: CGRectMake(0, 0, collectionView.frame.width - 16, CGFloat.max))
        label.numberOfLines = 0
        label.lineBreakMode = NSLineBreakMode.ByWordWrapping
       //here, be sure you set the font type and size that matches the one set in the storyboard label
        label.font = UIFont(name: "Helvetica", size: 17.0)
        label.text = labels[section]
        label.sizeToFit()

// Set some extra pixels for height due to the margins of the header section.  
//This value should be the sum of the vertical spacing you set in the autolayout constraints for the label. + 16 worked for me as I have 8px for top and bottom constraints.
        return CGSize(width: collectionView.frame.width, height: label.frame.height + 16)
    }
22
Alvaro

Comme l'interrogateur, j'avais un UICollectionView contenant un en-tête avec une seule étiquette, dont je voulais varier la hauteur. J'ai créé une extension à UILabel pour mesurer la hauteur d'une étiquette multiligne avec une largeur connue:

public extension UILabel {
    public class func size(withText text: String, forWidth width: CGFloat) -> CGSize {
        let measurementLabel = UILabel()
        measurementLabel.text = text
        measurementLabel.numberOfLines = 0
        measurementLabel.lineBreakMode = .byWordWrapping
        measurementLabel.translatesAutoresizingMaskIntoConstraints = false
        measurementLabel.widthAnchor.constraint(equalToConstant: width).isActive = true
        let size = measurementLabel.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
        return size
    }
}

Remarque : la syntaxe ci-dessus est celle de Swift 3.

Ensuite, j'implémente la méthode de taille d'en-tête de UICollectionViewDelegateFlowLayout en tant que:

extension MyCollectionViewController : UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        let text = textForHeader(inSection: section)
        var size =  UILabel.size(withAttributedText: text, forWidth: collectionView.frame.size.width)
        size.height = size.height + 16
        return size
    }
}

Le travail de calcul de la taille de l'en-tête est délégué à l'extension UILabel ci-dessus. Le +16 est un décalage fixe dérivé expérimentalement (8 + 8) basé sur les marges et pouvant être obtenu par programme. 

Tout ce dont vous avez besoin dans le rappel d'en-tête est simplement de définir le texte:

override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
    if kind == UICollectionElementKindSectionHeader, let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerIdentifier, for: indexPath) as?  MyCollectionHeader {
        let text = textForHeader(inSection: section)
        headerView.label.text = text
        return headerView
    }
    return UICollectionReusableView()
}
5
Nick Ager

L'idée est d'avoir une instance d'en-tête de modèle en mémoire pour calculer la hauteur souhaitée avant de créer la vue d'en-tête de résultat. Vous devez déplacer votre vue d'en-tête de section dans un fichier .nib distinct, configurer toutes les contraintes de mise en attente automatique et instancier le modèle dans votre méthode viewDidLoad de la manière suivante:

class MyViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

  @IBOutlet var collectionView : UICollectionView?
  private var _templateHeader : MyHeaderView

  override func viewDidLoad() {
    super.viewDidLoad()

    let nib = UINib(nibName: "HeaderView", bundle:nil)
    self.collectionView?.registerNib(nib, forCellWithReuseIdentifier: "header_view_id")

    _templateHeader = nib.instantiateWithOwner(nil, options:nil)[0] as! MyHeaderView
  }

}

Ensuite, vous pourrez calculer la taille de l'en-tête (height dans mon exemple) dans votre méthode de délégué de présentation de flux:

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {

    _templateHeader.lblTitle.text = "some title here"
    _templateHeader.lblDescription.text = "some long description"

    _templateHeader.setNeedsUpdateConstraints();
    _templateHeader.updateConstraintsIfNeeded()

    _templateHeader.setNeedsLayout();
    _templateHeader.layoutIfNeeded();

    let computedSize = _templateHeader.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)

    return CGSizeMake(collectionView.bounds.size.width, computedSize.height);
}

Et puis créez et retournez votre vue en-tête habituelle, comme toujours, puisque vous avez déjà calculé sa taille dans la méthode de délégation de présentation de flux:

func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {

    switch kind {

    case UICollectionElementKindSectionHeader:

        let headerView = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "header_view_id", forIndexPath: indexPath) as! MyHeaderView
        headerView.lblTitle.text = "some title here"
        headerView.lblDescription.text = "some long description"

        headerView.setNeedsUpdateConstraints()
        headerView.updateConstraintsIfNeeded()

        headerView.setNeedsLayout()
        headerView.layoutIfNeeded()

        return headerView
    default:
        assert(false, "Unexpected kind")
    }

}

Vous avez oublié de mentionner un moment important - votre vue en-tête devrait avoir une contrainte d'autolayout sur son contentView pour s'adapter à la largeur de la vue de collection (plus ou moins les marges souhaitées). 

3
Vladimir Afinello

J'ai eu de la chance en utilisant la méthode de Vladimir, mais je devais définir le cadre de la vue des modèles afin que sa largeur soit égale à celle de ma collection. 

    templateHeader.bounds = CGRectMake(templateHeader.bounds.minX, templateHeader.bounds.minY, self.collectionView.bounds.width, templateHeader.bounds.height)

De plus, mon point de vue comporte plusieurs composants redimensionnables et disposer d'une vue de modèle semble être suffisamment robuste pour gérer les modifications. Toujours se sentir comme il devrait y avoir un moyen plus facile.

0
Jacob Ruth