Émulation du comportement d'ajustement d'aspect à l'aide des contraintes de mise en page automatique dans Xcode 6

336

Je souhaite utiliser AutoLayout pour dimensionner et mettre en page une vue d'une manière qui rappelle le mode de contenu adapté à UIImageView.

J'ai une sous-vue dans une vue de conteneur dans Interface Builder. La sous-vue a un rapport d'aspect inhérent que je souhaite respecter. La taille de la vue du conteneur est inconnue jusqu'à l'exécution.

Si le rapport hauteur / largeur de la vue de conteneur est plus large que la sous-vue, je veux que la hauteur de la sous-vue soit égale à la hauteur de la vue parent.

Si le rapport hauteur / largeur de la vue de conteneur est plus élevé que la sous-vue, je souhaite que la largeur de la sous-vue soit égale à la largeur de la vue parent.

Dans les deux cas, je souhaite que la sous-vue soit centrée horizontalement et verticalement dans la vue du conteneur.

Existe-t-il un moyen d'y parvenir en utilisant les contraintes AutoLayout dans Xcode 6 ou dans la version précédente? Idéalement en utilisant Interface Builder, mais sinon il est peut-être possible de définir de telles contraintes par programme.

chris838
la source

Réponses:

1046

Vous ne décrivez pas l'adaptation à l'échelle; vous décrivez l'aspect en forme. (J'ai édité votre question à cet égard.) La sous-vue devient aussi grande que possible tout en conservant ses proportions et en s'adaptant entièrement à l'intérieur de son parent.

Quoi qu'il en soit, vous pouvez le faire avec la mise en page automatique. Vous pouvez le faire entièrement dans IB à partir de Xcode 5.1. Commençons par quelques vues:

quelques vues

La vue vert clair a un rapport d'aspect de 4: 1. La vue vert foncé a un rapport d'aspect de 1: 4. Je vais configurer des contraintes pour que la vue bleue remplisse la moitié supérieure de l'écran, la vue rose remplisse la moitié inférieure de l'écran et chaque vue verte se développe autant que possible tout en conservant son rapport hauteur / largeur et en s'adaptant à sa récipient.

Tout d'abord, je vais créer des contraintes sur les quatre côtés de la vue bleue. Je vais l'épingler à son voisin le plus proche sur chaque bord, avec une distance de 0. Je m'assure de désactiver les marges:

contraintes bleues

Notez que je ne mets pas encore à jour le cadre. Je trouve plus facile de laisser de la place entre les vues lors de la configuration des contraintes, et de définir les constantes à 0 (ou autre) à la main.

Ensuite, j'épingle les bords gauche, inférieur et droit de la vue rose à son voisin le plus proche. Je n'ai pas besoin de configurer une contrainte de bord supérieur car son bord supérieur est déjà contraint au bord inférieur de la vue bleue.

contraintes roses

J'ai également besoin d'une contrainte d'égale hauteur entre les vues rose et bleue. Cela les fera remplir chacun la moitié de l'écran:

entrez la description de l'image ici

Si je dis à Xcode de mettre à jour tous les cadres maintenant, j'obtiens ceci:

conteneurs disposés

Les contraintes que j'ai mises en place jusqu'à présent sont donc correctes. Je défais cela et commence à travailler sur la vue vert clair.

Ajuster l'aspect de la vue vert clair nécessite cinq contraintes:

  • Une contrainte de rapport d'aspect prioritaire requise sur la vue vert clair. Vous pouvez créer cette contrainte dans un xib ou un storyboard avec Xcode 5.1 ou version ultérieure.
  • Une contrainte de priorité requise limitant la largeur de la vue vert clair à être inférieure ou égale à la largeur de son conteneur.
  • Une contrainte de priorité élevée définissant la largeur de la vue vert clair pour être égale à la largeur de son conteneur.
  • Une contrainte de priorité requise limitant la hauteur de la vue vert clair à une valeur inférieure ou égale à la hauteur de son conteneur.
  • Une contrainte de haute priorité définissant la hauteur de la vue vert clair pour être égale à la hauteur de son conteneur.

Considérons les deux contraintes de largeur. La contrainte inférieure ou égale, en soi, n'est pas suffisante pour déterminer la largeur de la vue vert clair; de nombreuses largeurs s'adapteront à la contrainte. Puisqu'il y a ambiguïté, la mise en page automatique essaiera de choisir une solution qui minimise l'erreur dans l'autre contrainte (prioritaire mais non requise). Minimiser l'erreur signifie rendre la largeur aussi proche que possible de la largeur du conteneur, tout en ne violant pas la contrainte inférieure ou égale requise.

La même chose se produit avec la contrainte de hauteur. Et comme la contrainte de rapport hauteur / largeur est également requise, elle ne peut que maximiser la taille de la sous-vue le long d'un axe (sauf si le conteneur a le même rapport hauteur / largeur que la sous-vue).

Donc, je crée d'abord la contrainte de rapport d'aspect:

aspect supérieur

Ensuite, je crée des contraintes de largeur et de hauteur égales avec le conteneur:

taille égale supérieure

J'ai besoin de modifier ces contraintes pour être des contraintes inférieures ou égales:

haut de taille inférieure ou égale

Ensuite, je dois créer un autre ensemble de contraintes de largeur et de hauteur égales avec le conteneur:

à nouveau de taille égale

Et je dois faire de ces nouvelles contraintes moins que la priorité requise:

égal supérieur non requis

Enfin, vous avez demandé que la sous-vue soit centrée dans son conteneur, je vais donc configurer ces contraintes:

centré en haut

Maintenant, pour tester, je vais sélectionner le contrôleur de vue et demander à Xcode de mettre à jour toutes les images. Voici ce que j'obtiens:

disposition supérieure incorrecte

Oups! La sous-vue s'est développée pour remplir complètement son conteneur. Si je le sélectionne, je peux voir qu'en fait, il a conservé son rapport d'aspect, mais il fait un remplissage d' aspect au lieu d'un ajustement d' aspect .

Le problème est que sur une contrainte inférieure ou égale, il importe quelle vue se trouve à chaque extrémité de la contrainte, et Xcode a mis en place la contrainte opposée à mon attente. Je pouvais sélectionner chacune des deux contraintes et inverser ses premier et deuxième éléments. Au lieu de cela, je vais simplement sélectionner la sous-vue et modifier les contraintes pour qu'elles soient supérieures ou égales:

corriger les contraintes supérieures

Xcode met à jour la disposition:

disposition supérieure correcte

Maintenant, je fais tout de même la vue vert foncé en bas. Je dois m'assurer que son rapport d'aspect est de 1: 4 (Xcode l'a redimensionné de manière étrange car il n'avait pas de contraintes). Je ne montrerai plus les étapes car elles sont les mêmes. Voici le résultat:

disposition correcte en haut et en bas

Maintenant, je peux l'exécuter dans le simulateur iPhone 4S, qui a une taille d'écran différente de celle utilisée par IB, et tester la rotation:

test de l'iphone 4s

Et je peux tester dans le simulateur iPhone 6:

test iphone 6

J'ai téléchargé mon storyboard final dans cet élément pour votre commodité.

rob mayoff
la source
27
J'ai utilisé LICEcap . C'est gratuit (GPL) et enregistre directement sur GIF. Tant que le GIF animé ne dépasse pas 2 Mo, vous pouvez le publier sur ce site comme n'importe quelle autre image.
rob mayoff
1
@LucaTorella Avec seulement les contraintes de haute priorité, la disposition est ambiguë. Ce n'est pas parce qu'il obtient le résultat souhaité aujourd'hui qu'il le sera dans la prochaine version d'iOS. Supposons que la sous-vue souhaite un rapport hauteur / largeur de 1: 1, la vue d'ensemble est de taille 99 × 100 et que vous ne disposez que des contraintes de rapport hauteur / largeur et d'égalité prioritaires requises. Ensuite, la mise en page automatique peut définir la sous-vue sur la taille 99 × 99 (avec l'erreur totale 1) ou la taille 100 × 100 (avec l'erreur totale 1). Une taille est adaptée à l'aspect; l'autre est de remplissage d'aspect. L'ajout des contraintes requises produit une solution unique de moindre erreur.
rob mayoff
1
@LucaTorella Merci pour la correction concernant la disponibilité des contraintes de rapport hauteur / largeur.
rob mayoff
11
10000 meilleures applications avec un seul post ... incroyable.
DonVaughn
2
Wow .. Meilleure réponse ici dans le débordement de pile que j'ai jamais vu .. Merci pour ce monsieur ..
0yeoj
24

Rob, ta réponse est géniale! Je sais également que cette question concerne spécifiquement la réalisation de cet objectif en utilisant la mise en page automatique. Cependant, juste comme référence, je voudrais montrer comment cela peut être fait dans le code. Vous configurez les vues de dessus et de dessous (bleu et rose) comme Rob l'a montré. Ensuite, vous créez un personnalisé AspectFitView:

AspectFitView.h :

#import <UIKit/UIKit.h>

@interface AspectFitView : UIView

@property (nonatomic, strong) UIView *childView;

@end

AspectFitView.m :

#import "AspectFitView.h"

@implementation AspectFitView

- (void)setChildView:(UIView *)childView
{
    if (_childView) {
        [_childView removeFromSuperview];
    }

    _childView = childView;

    [self addSubview:childView];
    [self setNeedsLayout];
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    if (_childView) {
        CGSize childSize = _childView.frame.size;
        CGSize parentSize = self.frame.size;
        CGFloat aspectRatioForHeight = childSize.width / childSize.height;
        CGFloat aspectRatioForWidth = childSize.height / childSize.width;

        if ((parentSize.height * aspectRatioForHeight) > parentSize.height) {
            // whole height, adjust width
            CGFloat width = parentSize.width * aspectRatioForWidth;
            _childView.frame = CGRectMake((parentSize.width - width) / 2.0, 0, width, parentSize.height);
        } else {
            // whole width, adjust height
            CGFloat height = parentSize.height * aspectRatioForHeight;
            _childView.frame = CGRectMake(0, (parentSize.height - height) / 2.0, parentSize.width, height);
        }
    }
}

@end

Ensuite, vous changez la classe des vues bleues et roses dans le storyboard en AspectFitViews. Enfin, vous définissez deux prises pour votre viewcontroller topAspectFitViewet bottomAspectFitViewet définissez leurs childViews dans viewDidLoad:

- (void)viewDidLoad {
    [super viewDidLoad];

    UIView *top = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 100)];
    top.backgroundColor = [UIColor lightGrayColor];

    UIView *bottom = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 500)];
    bottom.backgroundColor = [UIColor greenColor];

    _topAspectFitView.childView = top;
    _bottomAspectFitView.childView = bottom;
}

Il n'est donc pas difficile de le faire dans le code et il est toujours très adaptable et fonctionne avec des vues de taille variable et des rapports d'aspect différents.

Mise à jour de juillet 2015 : trouvez une application de démonstration ici: https://github.com/jfahrenkrug/SPWKAspectFitView

Johannes Fahrenkrug
la source
1
@DanielGalasko Merci, vous avez raison. Je le mentionne dans ma réponse. J'ai simplement pensé que ce serait une référence utile pour comparer les étapes impliquées dans les deux solutions.
Johannes Fahrenkrug
1
Très utile d'avoir également la référence programmatique.
ctpenrose
@JohannesFahrenkrug si vous avez un projet de tutoriel, pouvez-vous partager? merci :)
Erhan Demirci
1
@ErhanDemirci Bien sûr, vous allez ici: github.com/jfahrenkrug/SPWKAspectFitView
Johannes Fahrenkrug
8

J'avais besoin d'une solution de la réponse acceptée, mais exécutée à partir du code. La façon la plus élégante que j'ai trouvée est d'utiliser le framework Masonry .

#import "Masonry.h"

...

[view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.width.equalTo(view.mas_height).multipliedBy(aspectRatio);
    make.size.lessThanOrEqualTo(superview);
    make.size.equalTo(superview).with.priorityHigh();
    make.center.equalTo(superview);
}];
Tomasz Bąk
la source
5

C'est pour macOS.

J'ai du mal à utiliser la méthode de Rob pour obtenir un ajustement d'aspect sur l'application OS X. Mais je l'ai fait d'une autre manière - Au lieu d'utiliser la largeur et la hauteur, j'ai utilisé les espaces de début, de fin, haut et bas.

Fondamentalement, ajoutez deux espaces de tête où l'un est> = 0 @ 1000 priorité requise et un autre est = 0 @ 250 priorité faible. Faites les mêmes réglages pour les espaces de fin, supérieur et inférieur.

Bien sûr, vous devez définir le rapport d'aspect et le centre X et le centre Y.

Et puis le travail est fait!

entrez la description de l'image ici entrez la description de l'image ici

brianLikeApple
la source
Comment pourriez-vous procéder par programmation?
FateNuller
@FateNuller Vous suivez simplement les étapes?
brianLikeApple
4

Je me suis retrouvé à vouloir un comportement de remplissage d' aspect afin qu'un UIImageView conserve son propre rapport d'aspect et remplisse entièrement la vue du conteneur. De façon confuse, mon UIImageView brisait les DEUX contraintes de largeur et de hauteur égales de haute priorité (décrites dans la réponse de Rob) et le rendu à pleine résolution.

La solution consistait simplement à définir la priorité de résistance à la compression du contenu de UIImageView inférieure à la priorité des contraintes de largeur et de hauteur égales:

Résistance à la compression du contenu

Gregor
la source
2

Il s'agit d'un port de l'excellente réponse de @ rob_mayoff à une approche centrée sur le code, utilisant des NSLayoutAnchorobjets et portés sur Xamarin. Pour moi, NSLayoutAnchoret les classes connexes ont rendu AutoLayout beaucoup plus facile à programmer:

public class ContentView : UIView
{
        public ContentView (UIColor fillColor)
        {
            BackgroundColor = fillColor;
        }
}

public class MyController : UIViewController 
{
    public override void ViewDidLoad ()
    {
        base.ViewDidLoad ();

        //Starting point:
        var view = new ContentView (UIColor.White);

        blueView = new ContentView (UIColor.FromRGB (166, 200, 255));
        view.AddSubview (blueView);

        lightGreenView = new ContentView (UIColor.FromRGB (200, 255, 220));
        lightGreenView.Frame = new CGRect (20, 40, 200, 60);

        view.AddSubview (lightGreenView);

        pinkView = new ContentView (UIColor.FromRGB (255, 204, 240));
        view.AddSubview (pinkView);

        greenView = new ContentView (UIColor.Green);
        greenView.Frame = new CGRect (80, 20, 40, 200);
        pinkView.AddSubview (greenView);

        //Now start doing in code the things that @rob_mayoff did in IB

        //Make the blue view size up to its parent, but half the height
        blueView.TranslatesAutoresizingMaskIntoConstraints = false;
        var blueConstraints = new []
        {
            blueView.LeadingAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.LeadingAnchor),
            blueView.TrailingAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.TrailingAnchor),
            blueView.TopAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.TopAnchor),
            blueView.HeightAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.HeightAnchor, (nfloat) 0.5)
        };
        NSLayoutConstraint.ActivateConstraints (blueConstraints);

        //Make the pink view same size as blue view, and linked to bottom of blue view
        pinkView.TranslatesAutoresizingMaskIntoConstraints = false;
        var pinkConstraints = new []
        {
            pinkView.LeadingAnchor.ConstraintEqualTo(blueView.LeadingAnchor),
            pinkView.TrailingAnchor.ConstraintEqualTo(blueView.TrailingAnchor),
            pinkView.HeightAnchor.ConstraintEqualTo(blueView.HeightAnchor),
            pinkView.TopAnchor.ConstraintEqualTo(blueView.BottomAnchor)
        };
        NSLayoutConstraint.ActivateConstraints (pinkConstraints);


        //From here, address the aspect-fitting challenge:

        lightGreenView.TranslatesAutoresizingMaskIntoConstraints = false;
        //These are the must-fulfill constraints: 
        var lightGreenConstraints = new []
        {
            //Aspect ratio of 1 : 5
            NSLayoutConstraint.Create(lightGreenView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, lightGreenView, NSLayoutAttribute.Width, (nfloat) 0.20, 0),
            //Cannot be larger than parent's width or height
            lightGreenView.WidthAnchor.ConstraintLessThanOrEqualTo(blueView.WidthAnchor),
            lightGreenView.HeightAnchor.ConstraintLessThanOrEqualTo(blueView.HeightAnchor),
            //Center in parent
            lightGreenView.CenterYAnchor.ConstraintEqualTo(blueView.CenterYAnchor),
            lightGreenView.CenterXAnchor.ConstraintEqualTo(blueView.CenterXAnchor)
        };
        //Must-fulfill
        foreach (var c in lightGreenConstraints) 
        {
            c.Priority = 1000;
        }
        NSLayoutConstraint.ActivateConstraints (lightGreenConstraints);

        //Low priority constraint to attempt to fill parent as much as possible (but lower priority than previous)
        var lightGreenLowPriorityConstraints = new []
         {
            lightGreenView.WidthAnchor.ConstraintEqualTo(blueView.WidthAnchor),
            lightGreenView.HeightAnchor.ConstraintEqualTo(blueView.HeightAnchor)
        };
        //Lower priority
        foreach (var c in lightGreenLowPriorityConstraints) 
        {
            c.Priority = 750;
        }

        NSLayoutConstraint.ActivateConstraints (lightGreenLowPriorityConstraints);

        //Aspect-fit on the green view now
        greenView.TranslatesAutoresizingMaskIntoConstraints = false;
        var greenConstraints = new []
        {
            //Aspect ratio of 5:1
            NSLayoutConstraint.Create(greenView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, greenView, NSLayoutAttribute.Width, (nfloat) 5.0, 0),
            //Cannot be larger than parent's width or height
            greenView.WidthAnchor.ConstraintLessThanOrEqualTo(pinkView.WidthAnchor),
            greenView.HeightAnchor.ConstraintLessThanOrEqualTo(pinkView.HeightAnchor),
            //Center in parent
            greenView.CenterXAnchor.ConstraintEqualTo(pinkView.CenterXAnchor),
            greenView.CenterYAnchor.ConstraintEqualTo(pinkView.CenterYAnchor)
        };
        //Must fulfill
        foreach (var c in greenConstraints) 
        {
            c.Priority = 1000;
        }
        NSLayoutConstraint.ActivateConstraints (greenConstraints);

        //Low priority constraint to attempt to fill parent as much as possible (but lower priority than previous)
        var greenLowPriorityConstraints = new []
        {
            greenView.WidthAnchor.ConstraintEqualTo(pinkView.WidthAnchor),
            greenView.HeightAnchor.ConstraintEqualTo(pinkView.HeightAnchor)
        };
        //Lower-priority than above
        foreach (var c in greenLowPriorityConstraints) 
        {
            c.Priority = 750;
        }

        NSLayoutConstraint.ActivateConstraints (greenLowPriorityConstraints);

        this.View = view;

        view.LayoutIfNeeded ();
    }
}
Larry OBrien
la source
1

C'est peut-être la réponse la plus courte, avec la maçonnerie , qui prend également en charge le remplissage d'aspect et l'étirement.

typedef NS_ENUM(NSInteger, ContentMode) {
    ContentMode_aspectFit,
    ContentMode_aspectFill,
    ContentMode_stretch
}

// ....

[containerView addSubview:subview];

[subview mas_makeConstraints:^(MASConstraintMaker *make) {
    if (contentMode == ContentMode_stretch) {
        make.edges.equalTo(containerView);
    }
    else {
        make.center.equalTo(containerView);
        make.edges.equalTo(containerView).priorityHigh();
        make.width.equalTo(content.mas_height).multipliedBy(4.0 / 3); // the aspect ratio
        if (contentMode == ContentMode_aspectFit) {
            make.width.height.lessThanOrEqualTo(containerView);
        }
        else { // contentMode == ContentMode_aspectFill
            make.width.height.greaterThanOrEqualTo(containerView);
        }
    }
}];
M. Ming
la source