web-dev-qa-db-fra.com

Guide pratique: affichage personnalisé à partir de XIB avec vue StackView dans iOS

J'ai récemment commencé à plonger dans le développement iOS avec Swift à nouveau et j'examine maintenant un UIControl personnalisé. Pour la mise en page et la flexibilité, cette vue personnalisée est construite à l'aide d'un StackView.

Ce que je veux réaliser ...

... est essentiellement d'avoir simplement la même fonctionnalité avec le StackView que j'ai habituellement quand je le fais glisser directement dans le storyboard - où il se redimensionne pour ne pas poser de problème. C'est essentiellement la même chose que les cellules de table à dimensionnement automatique.

Vitrine

Je peux réduire mon CustomView à un exemple simple: il s'agit d'un StackView avec trois étiquettes, Alignment défini sur Center, DistributionEqual Centering. Le StackView serait épinglé aux guides de mise en page gauche, droite et haut (ou à la fin, en tête et en haut). Ce que je peux facilement recréer avec un CustomView, chargé à partir de XIB, le traduire en un CustomView avec les mêmes paramètres - j'ai joint les deux comme capture d'écran.

Capture d'écran de Simple StackView

Capture d'écran de CustomView

Chargement du fichier XIB et configuration.

import UIKit

@IBDesignable
class CustomView: UIView {

    // loaded from NIB
    private weak var view: UIView!

    convenience init() {
        self.init(frame: CGRect())
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.loadNib()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.loadNib()
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        // we need to adjust the frame of the subview to no longer match the size used
        // in the XIB file BUT the actual frame we got assinged from the superview
        self.view.frame = bounds
    }

    private func loadNib ()  {
        self.view = Bundle (for: type(of: self)).loadNibNamed(
            "CustomView", owner: self, options: nil)! [0] as! UIView
        self.addSubview(self.view)
    }
}

Ce que vous pouvez voir ici, c'est que je charge simplement le fichier XIB, remplace la disposition des sous-vues pour adapter le cadre à la vue réelle et c'est tout.

Des questions

Mes questions sont donc liées à la façon dont cela est généralement abordé. Les étiquettes de mon StackView ont une taille de contenu intrinsèque, c'est-à-dire que généralement le StackView s'adapte simplement à la hauteur des étiquettes sans se plaindre - SI vous le concevez dans le storyboard. Ce n'est pas le cas, si vous mettez tout cela dans un XIB et laissez la mise en page telle qu'elle est - l'IB se plaindra d'une hauteur inconnue si la vue est épinglée de trois façons comme dans la capture d'écran (en haut, en tête, en bas).

J'ai lu le guide de mise en page automatique et regardé les vidéos de la WWDC "Mysteries of AutoLayout" mais le sujet est encore assez nouveau pour moi et je ne pense pas avoir encore la bonne approche. C'est donc là que j'ai besoin d'aide

(1) Dois-je définir des contraintes et désactiver translatesAutoresizingMaskIntoConstraints?

Ce que je pourrais et devrais faire en premier est de définir translatesAutoresizingMaskIntoConstraintsfalse de manière à ne pas obtenir de contraintes générées automatiquement et à définir mes propres contraintes. Je pouvais donc changer loadNib en ceci:

private func loadNib ()  {
    self.view = Bundle (for: type(of: self)).loadNibNamed(
        "CustomView", owner: self, options: nil)! [0] as! UIView
    self.view.translatesAutoresizingMaskIntoConstraints = false
    self.addSubview(self.view)

    self.addConstraint(NSLayoutConstraint (
        item: self
        , attribute: .bottom
        , relatedBy: .equal
        , toItem: self.view
        , attribute: .bottom
        , multiplier: 1.0
        , constant: 0))

    self.addConstraint(NSLayoutConstraint (
        item: self
        , attribute: .top
        , relatedBy: .equal
        , toItem: self.view
        , attribute: .top
        , multiplier: 1.0
        , constant: 0))

    self.addConstraint(NSLayoutConstraint (
        item: self.view
        , attribute: .leading
        , relatedBy: .equal
        , toItem: self
        , attribute: .leading
        , multiplier: 1.0
        , constant: 0))

    self.addConstraint(NSLayoutConstraint (
        item: self.view
        , attribute: .trailing
        , relatedBy: .equal
        , toItem: self
        , attribute: .trailing
        , multiplier: 1.0
        , constant: 0))
}

Fondamentalement, j'ai étiré la vue pour l'adapter à toute la largeur de mon UIView dans le Storyboard et restreint son haut et de bas en haut et en bas de StackView.

J'ai eu des tonnes de problèmes avec XCode 8 qui m'écrasaient lors de l'utilisation de celui-ci, donc je ne sais pas trop quoi en penser.

(2) Ou devrais-je remplacer intrinsicContentSize?

Je pourrais aussi simplement remplacer intrinsicContentSize en vérifiant la hauteur de tous mes arrangedSubviews et prendre celle qui est la plus haute, définir ma hauteur de vue:

override var intrinsicContentSize: CGSize {
    var size = CGSize.zero

    for (_ , aSubView) in (self.view as! UIStackView).arrangedSubviews.enumerated() {
        let subViewHeight = aSubView.intrinsicContentSize.height
        if  subViewHeight > size.height {
            size.height = aSubView.intrinsicContentSize.height
        }
    }

    // add some padding
    size.height += 0
    size.width = UIViewNoIntrinsicMetric
    return size
}

Bien que cela me donne sûrement plus de flexibilité, je devrais également invalider la taille du contenu intrinsèque, vérifiez le type dynamique et beaucoup d'autres choses que je préfère éviter.

(3) Suis-je même en train de charger le XIB de la manière "proposée"?

Ou existe-t-il un meilleur moyen, par exemple en utilisant UINib.instantiate? Je ne pouvais vraiment pas trouver une meilleure pratique pour le faire.

(4) Pourquoi dois-je le faire en premier lieu?

Vraiment pourquoi? J'ai seulement déplacé le StackView dans un CustomView. Alors pourquoi StackView cesse-t-il de s'adapter maintenant, pourquoi dois-je définir les contraintes supérieures et inférieures?

Ce qui a le plus aidé jusqu'à présent, c'est cette phrase clé dans https://stackoverflow.com/a/24441368 :

"En général, la mise en page automatique est effectuée de haut en bas. En d'autres termes, une mise en page de vue parent est effectuée en premier, puis toutes les mises en page de vue enfant sont effectuées."

.. mais je n'en suis vraiment pas si sûr.

(5) Et pourquoi dois-je ajouter les contraintes dans le code?

En supposant que je désactive translatesAutoresizingMaskIntoConstraints, je ne peux pas définir de contraintes comme pour les cellules de table dans IB? Cela se traduira toujours par quelque chose comme label.top = top et ainsi l'étiquette serait étirée au lieu du StackView héritant de la taille/hauteur. De plus, je ne peux pas vraiment épingler StackView à sa vue de conteneur dans l'IB pour le fichier XIB.

J'espère que quelqu'un pourra m'aider là-bas. À votre santé!

26
martin

Voici quelques réponses à vos questions, bien que dans un ordre différent:

(5) Et pourquoi dois-je ajouter les contraintes dans le code?

En supposant que je désactive translatesAutoresizingMaskIntoConstraints, je ne peux pas définir de contraintes comme pour les cellules de table dans IB? Cela se traduira toujours par quelque chose comme label.top = top et ainsi l'étiquette serait étirée au lieu du StackView héritant de la taille/hauteur. De plus, je ne peux pas vraiment épingler StackView à sa vue de conteneur dans l'IB pour le fichier XIB.

Vous devez ajouter les contraintes dans le code, dans cet exemple, car vous chargez une vue arbitraire à partir d'une pointe, puis l'ajoutez à votre CustomView codé. La vue dans la plume n'a aucune connaissance préexistante du contexte ou de la hiérarchie dans laquelle elle sera placée, il n'est donc pas possible de définir à l'avance comment se contraindre à une superview future inconnue. Dans le cas de StackView sur le storyboard, ou à l'intérieur d'une cellule prototype, ce contexte est connu ainsi que sa vue parent.

Une alternative intéressante que vous pouvez envisager dans ce cas, si vous ne voulez pas écrire manuellement des contraintes, est de créez votre sous-classe CustomView UIStackView au lieu de UIView. Comme UIStackView génère automatiquement les contraintes appropriées, il vous permettra d'économiser le codage manuel. Voir la réponse suivante pour un exemple de code.

(3) Suis-je même en train de charger le XIB de la manière "proposée"?

Ou existe-t-il un meilleur moyen, par exemple en utilisant UINib.instantiate? Je ne pouvais vraiment pas trouver une meilleure pratique pour le faire.

Je ne connais pas de méthode "standard" ou "correcte" pour gérer le chargement d'une plume dans une vue personnalisée. J'ai vu une variété d'approches qui fonctionnent toutes. Cependant, je noterai que vous effectuez plus de travail que nécessaire dans votre exemple de code. Voici un moyen plus minimal d'atteindre le même résultat, mais également d'utiliser une sous-classe UIStackView au lieu d'une sous-classe UIView (comme mentionné ci-dessus):

class CustomView: UIStackView {
    required init(coder: NSCoder) {
        super.init(coder: coder)
        distribution = .fill
        alignment = .fill
        if let nibView = Bundle.main.loadNibNamed("CustomView", owner: self, options: nil)?.first as? UIView {
            addArrangedSubview(nibView)
        }
    }
}

Notez que, puisque CustomView est maintenant une sous-classe d'UIStackView, il créera automatiquement les contraintes nécessaires pour toutes les sous-vues. Dans ce cas, parce que distribution et alignment sont tous deux définis sur .fill, Il épinglera les bords de la sous-vue à ses propres bords, comme vous le souhaitez.

(4) Pourquoi dois-je le faire en premier lieu?

Vraiment pourquoi? J'ai seulement déplacé le StackView dans un CustomView. Alors pourquoi StackView cesse-t-il de s'adapter maintenant, pourquoi dois-je définir les contraintes supérieures et inférieures?

En fait, le problème clé ici est la propriété alignment de votre $ StackView de .center. Cela signifie que les étiquettes dans StackView seront centrées verticalement, mais cela ne les contraint pas en haut ou en bas de StackView. C'est comme ajouter une contrainte centerY à une sous-vue à l'intérieur d'une vue. Vous pouvez définir la vue extérieure à la hauteur de votre choix, et la sous-vue sera centrée, mais elle ne "repoussera" pas le haut et le bas de la vue extérieure, car les contraintes centrales ne contraignent que les centres, pas les bords supérieur et inférieur.

Dans votre version storyboard de StackView, la hiérarchie des vues au-dessus de laquelle StackView est connue, et il existe également des options pour générer automatiquement des contraintes à partir du redimensionnement des masques. Quoi qu'il en soit, la hauteur finale de StackView est connue de UIKit et les étiquettes seront centrées verticalement à l'intérieur de cette hauteur, mais ne l'affecteront pas réellement. Vous devez définir l'alignement de votre StackView sur .fill, ou ajouter des contraintes supplémentaires depuis le haut et le bas des étiquettes vers le haut et le bas du StackView afin que la disposition automatique détermine la hauteur du StackView devrait être.

(2) Ou devrais-je remplacer intrinsicContentSize?

Cela n'aiderait pas beaucoup, car la taille intrinsèque du contenu ne repoussera pas à elle seule les bords d'une vue parent, sauf si les bords de la sous-vue sont également contraints aux bords de la vue parent. Dans tous les cas, vous obtiendrez automatiquement le comportement de taille de contenu intrinsèque correct dont vous avez besoin dans ce cas, tant que vous résolvez le problème d'alignement .center Mentionné ci-dessus.

(1) Dois-je définir des contraintes et désactiver translatesAutoresizingMaskIntoConstraints?

Ce serait normalement une approche valable. Si vous sous-classe UIStackView comme je le décris ci-dessus, vous n'avez pas besoin de le faire non plus.

En résumé

Si vous:

  1. Créez votre CustomView en tant que sous-classe de UIStackView comme dans l'exemple de code ci-dessus

  2. Créez une vue sur votre scénarimage dans votre classe personnalisée CustomView. Si vous souhaitez que CustomView se redimensionne correctement, vous devez lui donner des contraintes et ne pas utiliser de contraintes générées automatiquement à partir du masque de redimensionnement.

  3. Modifiez le StackView dans votre nib pour que alignment soit réglé sur .fill Ou pour avoir des contraintes supplémentaires entre le haut et le bas d'au moins une des étiquettes (probablement la grande au centre) et le haut et le bas de la vue de la pile

La mise en page automatique devrait alors fonctionner correctement et votre vue personnalisée avec StackView devrait être mise en page comme prévu.

14
Daniel Hall