Autolayout avec sous-vues pouvant ne pas être présentes

muqaddarmuqaddar Administrateur
septembre 2014 modifié dans Objective-C, Swift, C, C++ #1

Salut,


 


J'arrive à  bien utiliser autolayout dans 90% des cas que je peux rencontrer.


Là , je voudrais connaà®tre la meilleure façon de gérer l'espace d'une vue par rapport à  une autre sachant que l'autre peut ne pas être présente.


 


Dessin:


 


Réponses

  • Joanna CarterJoanna Carter Membre, Modérateur
    septembre 2014 modifié #2

    Avec autolayout, on ne doit pas modifier la taille des frames - jamais !  ::)


     


    Tu devrais avoir une contrainte entre la droite de B et sa vue contenant. Il faut la connecter à  un IBOutlet dans ton contrôleur et modifier le constante de cette contrainte afin que B se déplace au droit selon la valeur (peut-être negative) de la constante.


  • MayerickMayerick Membre
    septembre 2014 modifié #3
    Exacte, pour m'être pris la tête avec récemment, je crois que le seul moyen d'ajouter une contrainte à  un élément n'étant pas présent dans la vue ( parce qu'il ne rentre pas dedans ou parce que tu l'ajoutes plus tard par le code) est de le faire par le code. B à  zéro en largeur, c'est ce que te propose xCode en suggested constraint dans le storyboard, perso j'ai essayé de m'en sortir avec et ça n'a pas fonctionné dans mon cas. Après on cherchait pas faire exactement la même chose.

    Donc je dirai, soit tu crées un IBOutlet de ta contrainte sur ta vue A pour en avoir une référence et la modifier par la suite. Soit tu crées la contrainte par le code et tu la configure dans le viewDidLoad.


    Dans tous les cas avec Auto-Layout tu ne peux plus toucher aux frames des objets. Je pense que tu le sais mais un peu de rappel ne fait pas de mal, pour les déplacer dans la vue, il faut utiliser -updateConstraint et configurer la taille des contraintes que tu souhaites, avec maContrainte.constant = x. (x pouvant être négatif ou positif). Tu peux aussi appeler [self updateConstraint] à  tout moment dans ton code, par exemple si tu souhaites une position particulière sous certains critères.


    Dans ton cas j'ajouterai la vue et la contrainte dans le viewDidLoad, ça devrait te donner un meilleur contrôle du comportement.


    Après je suis bien preneur d'autres solutions ! Tiens nous au courant.
  • Je n'ai pas vérifié mais je me demande s'il n'est pas possible de définir deux contraintes avec des priorités différentes, une avec B et une avec la vue contenante.


  • AliGatorAliGator Membre, Modérateur

    Pour moi tu as plusieurs solutions qui restent simples :


     


    Solution 1) (La solution la plus simple à  mon avis)


     - Tu crées une contrainte par IB sur la largeur de ta vue B, et tu mets un IBOutlet dessus


     - Quand tu veux cacher ta vue B, tu changes la propriété "constant" de cette contrainte pour la passer à  0


     


    Gros avantage de cette solution, en plus d'être simple, c'est que tu peux aussi si ça t'intéresse animer ce changement de contrainte en seulement 1 ou 2 lignes de code. Ce qui te permet de donner un effet comme quoi la vue B se réduit en largeur jusqu'à  disparaà®tre.


     


    Solution 2)


     - Tu crées 2 contraintes par IB, une entre la droite de A et la gauche de B, et une autre entre la droite de A et la droite de sa vue parente, et tu mets un IBOutlet strong sur chacune de ces 2 contraintes


     - Au démarrage, tu en supprimes une des 2 (la 2ème si ta vue B est visible au démarrage par exemple) de sa vue, mais tu la gardes dans ta @property quand même


     - Quand tu veux masquer ta vue B, du coup tu supprimes la 1ère contrainte et tu rajoutes la 2ème.


     


    Le fait de les avoir créées dans IB et de les garder dans des "@property(strong) IBOutlet" t'évite d'avoir à  écrire les contraintes par code et d'avoir à  les recréer à  chaque fois. Dans ton code la seule chose que tu as à  faire c'est ajouter une contrainte et en supprimer une autre, mais tu n'as pas à  t'embêter à  les créer par code. Mais bon tu as à  jongler entre 2 contraintes du coup.


     


    Solution 3)


     - Même chose que la solution 2, mais au lieu de supprimer une contrainte ou l'autre par code, tu changes leur priorité. Tu mets la contrainte 1 plus prioritaire que la contrainte 2 pour qu'elle prenne le pas sur cette dernière et basta


  • Joanna CarterJoanna Carter Membre, Modérateur


    Solution 1) (La solution la plus simple à  mon avis)


     - Tu crées une contrainte par IB sur la largeur de ta vue B, et tu mets un IBOutlet dessus


     - Quand tu veux cacher ta vue B, tu changes la propriété "constant" de cette contrainte pour la passer à  0




     


    ça peut avoir l'effet de "coincer" les sous-vues et, si on a créé les contraintes de taille minimum ou d'espace minimum sur ces sous-vues, on pourrait arriver aux conflits.


     


    Muqaddar, c'est quoi que tu veux qui se passe en termes de mouvements entre les trois vues ?

  • AliGatorAliGator Membre, Modérateur


    ça peut avoir l'effet de "coincer" les sous-vues et, si on a créé les contraintes de taille minimum ou d'espace minimum sur ces sous-vues, on pourrait arriver aux conflits.




    En effet. Mais dans ce cas on peut jouer sur les priorités des contraintes, mettre une priorité plus forte sur cette contrainte de largeur de vue B que les contraintes des sous-vues de la vue B.

  • muqaddarmuqaddar Administrateur
    septembre 2014 modifié #8

    Je viens de vous lire.


    C'est passionnant.


     


    Les animations ne sont pas obligatoires mais ça ferait un plus sympa.


     


    Pour les contraintes des sous-vues, et pour info:


    - la vue A est une scrollView qui contient une subview.


    - la vue B est une UITableView


     


    mais aussi:


    - la vue A a une largeur dynamique


    - la vue B a une largeur fixe (genre 100 pixels)


     


    Enfin, la superview s'étire dans tous les sens en mode portrait et paysage.


     


    Je sais quand j'ai besoin d'afficher la vue B dans une méthode "reloadData" de mon VC qui fait 1 petit calcul sur mes données.


  • AliGatorAliGator Membre, Modérateur
    Dans ce cas la solution 1 me paraà®t parfaite.
  • muqaddarmuqaddar Administrateur

    Je vais la tester sous 2 ou 3 jours et je reviens vers vous pour vous dire ce qu'il en est.


    Merci à  tous.


  • muqaddarmuqaddar Administrateur

    Bon, je débute avec la programmation des contraintes.


     


    J'ai fait ça, sans succès, pour passer la largeur de ma tableView à  0 (rappel: tableLayoutContraint est connecté avec un outlet sur la contrainte dans IB):



      self.tableLayoutContraint = [NSLayoutConstraint
                                   constraintWithItem:self.tableView
                                   attribute:NSLayoutAttributeWidth
                                   relatedBy:NSLayoutRelationEqual
                                   toItem:nil
                                   attribute:NSLayoutAttributeNotAnAttribute
                                   multiplier:1.0f constant:0.0f];

    Le code est lu, mais rien ne change.


     


    J'ai également essayé de la placer dans:



    - (void)updateViewConstraints

    mais rien n'a changé.


  • Joanna CarterJoanna Carter Membre, Modérateur
    Quelle sont les containts autour du UITableView ?
  • AliGatorAliGator Membre, Modérateur
    Je ne comprends pas tu dis que ta contrainte est connectée avec un IBOutlet dans IB d'un côté mais de l'autre côté ton code recrée une nouvelle NSLayoutConstraint à  la place de l'ancienne (et en plus n'en fait rien ensuite puisque tu ne l'as pas ajoutée à  une vue parente)


    C'est un peu comme si tu avais un IBOutlet sur une UIView dans ton XIB et que pour changer la frame de ta vue tu faisais self.monoutlet = [[UIView alloc] initWithFrame:newFrame] (donc créait une nouvelle vue, que tu oublierais en plus d'ajouter en subview de sa parente) au lieu de faire self.monoutlet.frame = newFrame; comme il se doit.



    Si tu as déjà  un IBOutlet sur une contrainte existante et déjà  placée dans ton XIB faut pas en instancier une autre, il faut changer l'existante. self.tableLayoutConstraint.constant = 0; fera donc l'affaire.
  • muqaddarmuqaddar Administrateur


    Je ne comprends pas tu dis que ta contrainte est connectée avec un IBOutlet dans IB d'un côté mais de l'autre côté ton code recrée une nouvelle NSLayoutConstraint à  la place de l'ancienne (et en plus n'en fait rien ensuite puisque tu ne l'as pas ajoutée à  une vue parente)


    C'est un peu comme si tu avais un IBOutlet sur une UIView dans ton XIB et que pour changer la frame de ta vue tu faisais self.monoutlet = [[UIView alloc] initWithFrame:newFrame] (donc créait une nouvelle vue, que tu oublierais en plus d'ajouter en subview de sa parente) au lieu de faire self.monoutlet.frame = newFrame; comme il se doit.



    Si tu as déjà  un IBOutlet sur une contrainte existante et déjà  placée dans ton XIB faut pas en instancier une autre, il faut changer l'existante. self.tableLayoutConstraint.constant = 0; fera donc l'affaire.




     


    Je suis entièrement d'accord avec toi et la logique des choses.


     


    Sauf qu'en lisant la doc de NSLayoutConstraint, je n'avais pas compris que je pouvais passer directement par cette propriété, et le fait que tous les exemples de code lus sur le net utilisent la méthode "constraintWithItem" m'a fait croire à  cette hypothèse.

  • muqaddarmuqaddar Administrateur


    Quelle sont les containts autour du UITableView ?




     


    Wouah, ça marche beaucoup beaucoup mieux... :-)


    Y'a plus qu'à  l'animer.

  • AliGatorAliGator Membre, Modérateur
    septembre 2014 modifié #16
    La structure de code pour animer la mise à  jour d'une contrainte :
    constraint.constant = newValue;
    [parentView setNeedsUpdateConstraints];
    [UIView animateWithDuration:duration animations:^{
    [parentView layoutIfNeeded];
    }];
    Où parentView est la vue sur laquelle la contrainte est ajoutée, ce qui est généralement le parent commun aux 2 vues entre lesquelles tu as mis ta contrainte (l'ancêtre commun dans la hiérarchie de vue qui contient à  la fois ta vue A et ta vue B.)

    Tu peux aussi mettre la ligne constraint.constant dans le block d'animation, si cela te paraà®t plus logique/lisible (pour faire le parallèle avec ce que tu ferais si tu voulais animer une autre propriété, comme la frame d'une vue quand on n'utilise pas AutoLayout, etc, où tu mettrais le code dans le bloc d'animation) : les 2 fonctionnent.

    Le secret est surtout d'appeler layoutIfNeeded dans le block d'animation pour que le changement dans la contrainte soit animé, car juste changer la constante dans le bloc d'animation ne suffira pas.

    ---

    PS : l'appel à  setNeedsUpdateConstraints n'est pas obligatoire (dans le sens où ça va souvent marcher même si tu ne le mets pas) mais cela permet de laisser une chance aux subviews d'exécuter leur code updateConstraints dans le cas où certaines subviews auraient surchargé cette méthode (l'implémentation par défaut de updateConstraints est vide, donc si aucune subview ne surcharge cette méthode, appeler setNeedsUpdateConstraints n'a finalement aucun effet, mais autant l'appeler et mettre du code sûr pour que ça continue de marcher le jour où tu utiliserais une subview dont l'implémentation de updateConstraints aurait été surchargée)
  • muqaddarmuqaddar Administrateur
    septembre 2014 modifié #17


     


    PS : l'appel à  setNeedsUpdateConstraints n'est pas obligatoire (dans le sens où ça va souvent marcher même si tu ne le mets pas) mais cela permet de laisser une chance aux subviews d'exécuter leur code updateConstraints dans le cas où certaines subviews auraient surchargé cette méthode (l'implémentation par défaut de updateConstraints est vide, donc si aucune subview ne surcharge cette méthode, appeler setNeedsUpdateConstraints n'a finalement aucun effet, mais autant l'appeler et mettre du code sûr pour que ça continue de marcher le jour où tu utiliserais une subview dont l'implémentation de updateConstraints aurait été surchargée)

     




     


    Voilà , c'est ce que j'avais remarqué hier soir avant que tu postes.


    setNeedsUpdateConstraints n'avait pas d'incidence pour mon cas, mais je vais le rajouter quand-même.


     


    J'avais fait ça:



        self.tableLayoutContraint.constant = 0.0;
        self.tableView.layer.opacity = 0.0;
        
        [self.view layoutIfNeeded];
        [UIView animateWithDuration:0.5
                         animations:^{
                           if DEVICE_IPAD self.tableLayoutContraint.constant = TABLEVIEW_WIDTH_IPAD;
                           if DEVICE_IPHONE self.tableLayoutContraint.constant = TABLEVIEW_WIDTH_IPHONE;
                           self.tableView.layer.opacity = 1.0;
                           [self.view layoutIfNeeded];
                         }];

    Apparemment, le premier [self.view layoutIfNeeded]; hors du bloc ne sert à  rien, pourtant j'avais trouvé de nombreux exemples qui le mettaient.

  • AliGatorAliGator Membre, Modérateur
    Ouais pour le layoutIfNeeded hors du bloc je l'ai vu aussi je pense que de même c'est une sécurité inutile dans 99% des cas mais permet de d'assurer qu'on part d'un état stable et qu'il restait pas d'autres Layout à  faire qui n'auraient rien à  voir avec celle que tu veux animer.


    Perso je l'ai jamais mis dans mes codes et j'ai jamais eu de soucis mais bon.
  • muqaddarmuqaddar Administrateur
    septembre 2014 modifié #19

    Merci à  vous pour les conseils !


    J'ai bien avancé grâce à  vous.


     


    Un petit extrait (vous voyez pas où je clique mais c'est pas grave) :


     


    test.mov 264.6K
  • LeChatNoirLeChatNoir Membre, Modérateur

    Bon bon bon...


     


    AutoLayout, c'est sympa, mais ca fait des noeuds au cerveau parfois !


     


    Alors, j'ai un peu la même question que Muqua mais en vertical...


     


    J'ai une vue X qui comporte des sous vues. Disons, un titre, une image pour faire court.


    Le titre a une contrainte vertical spacing avec X et avec l'image qui est en dessous.


    L'image a une contrainte avec le bottom de X, ce qui permet d'avoir une vue X avec la bonne hauteur.


     


    Maintenant, je veux masquer cette vue...


    Je fais comment ?  B)


     


    J'ai essayé d'ajouter une contrainte de hauteur à  X en la mettant <= valeur max (car l'image peut varier de hauteur par ex) et lorsque je veux masquer la vue, je passe cette contrainte à  0.


    Mais rien ne bouge :( 


    :'(


  • LeChatNoirLeChatNoir Membre, Modérateur

    Bon laissez tomber. Avec la contrainte Height que je passe à  0, ça fonctionne.


    Faut juste utiliser la bonne outlet...  :o


  • LeChatNoirLeChatNoir Membre, Modérateur

    Avec la bonne outlet, ça fonctionne. Seulement voilà .


     


    Dans ma vue que je veux masquer, il y a des sous-vues, elles-mêmes agencées via des contraintes.


    Il y a un label, dessous, un séparateur et dessous, une tableView.


     


    Si ma vue doit être masquée, je passe sa contrainte de hauteur à  0 et je la passe en hidden.


    Seulement voilà , Dans ce cas précis (celui avec la tableView), je me retrouve avec des warnings qui me disent qu'XCode est obligé de supprimer des contraintes car en passant à  0, il n'arrive plus à  ttes les satisfaire...


     


    Faut il s'embêter à  tout passer à  0 à  l'intérieur ?

  • LeChatNoirLeChatNoir Membre, Modérateur

    J'ai même fait un :



    [[self.boxGuideBooks subviews]
    makeObjectsPerformSelector:@selector(removeFromSuperview)];

    Mais rien n'y fait. Le warning apparait quand meme...


     


    Unable to simultaneously satisfy constraints.


     


    :(


  • LeChatNoirLeChatNoir Membre, Modérateur

    Je continue mon long monologue de l'Autolayout (à  défaut de vagin  ) :


    => il faut que la contrainte de hauteur de la vue à  masquer ait une priority supérieure aux contraintes de ses sous vues.


     


    Un jour sans doute, j'aimerai Autolayout :)


  • Joanna CarterJoanna Carter Membre, Modérateur

    J'ai fait quelque chose comme ci :


     


    1. La sous-vue avec ses deux contraintes vers la super-vue et le label


     


    2. Une contrainte entre la super-vue et la sous-vue


     


    J'enlève la sous-vue (ses contraintes sont automatiquement enlevés)


     


    J'ajoute la contrainte entre la super-vue et le label


     


    Ou l'inverse.


  • AliGatorAliGator Membre, Modérateur
    novembre 2014 modifié #26
    Moi je mets une priorité plus forte à  la contrainte de la vue parente qu'aux contraintes des sous-vues (puisque finalement c'est exactement ça que je veux : que la vue parente soit prioritaire)
  • LeChatNoirLeChatNoir Membre, Modérateur

    Voilà , c'est ça. C'est ce que j'ai fait et ça fonctionne.


  • LeChatNoirLeChatNoir Membre, Modérateur
    novembre 2014 modifié #29

    Merci Joanna. Non mais je trouve très bien Autolayout.


    C'est juste que je suis sur un écran avec des listes dont le nombre n'est pas connu a priori, parfois y a rien, etc...


     


    Et dans ce cas, c'est un peu compliqué.


    Par exemple, j'ai une box Topos dans laquelle, je vais pouvoir avoir :


    => 0 à  n topos guides papiers pour lesquels je vais présenter la couverture (image) avec le titre et l'auteur


    => 0 à  n liens de topos sur internet


    => 0 à  n liens de sites web qui en parlent


    => 0 à  n topos téléchargeables sur ClimbingAway


     


    Je me demande si j'aurai pas intérêt à  utiliser une WebView parfois...


  • AliGatorAliGator Membre, Modérateur
    A se demander si une UITableView avec 4 sections (topos guides papiers, liens topos internet, liens sites web, topos téléchargables) " avec ou sans en-tête de section visible " avec des cells différentes ne serait pas plus simple à  gérer du coup pour ce genre de cas ?
Connectez-vous ou Inscrivez-vous pour répondre.