web-dev-qa-db-fra.com

Emulation d'un comportement d'ajustement d'aspect à l'aide de contraintes AutoLayout dans Xcode 6

Je souhaite utiliser AutoLayout pour dimensionner et mettre en forme 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 certain rapport d'aspect inhérent que je souhaite respecter. La taille de la vue conteneur est inconnue jusqu'au moment de l'exécution.

Si les proportions de la vue conteneur sont plus larges que la sous-vue, je souhaite que la hauteur de la sous-vue soit égale à celle de la vue parente.

Si les proportions de la vue conteneur sont plus hautes que celles de la sous-vue, je souhaite que la largeur de la sous-vue soit égale à celle de la vue parente.

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, à l'aide d'Interface Builder, mais sinon, il est possible de définir de telles contraintes par programme.

327
chris838

Vous n'êtes pas en train de décrire l'échelle; vous décrivez l'aspect-fit. (J'ai modifié votre question à cet égard.) La sous-vue devient aussi grande que possible tout en conservant son rapport de format et en s'intégrant entièrement dans sa 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:

some views

La vue vert clair a un format d'image de 4: 1. La vue vert foncé a un format d'image de 1: 4. Je vais définir des contraintes pour que la vue bleue remplisse la moitié supérieure de l’écran, que la vue rose remplisse la moitié inférieure de l’écran et que chaque vue verte s’agrandisse autant que possible tout en conservant son rapport de format et d’adaptation au fond. récipient.

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

blue constraints

Notez que je ne met pas encore le cadre à jour. 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'attache 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 sur le bord inférieur de la vue bleue.

pink constraints

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

enter image description here

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

containers laid out

Donc, les contraintes que j'ai mises en place jusqu'ici sont correctes. Je défait cela et commence à travailler sur la vue vert clair.

L'aspect adapté à la vue vert clair nécessite cinq contraintes:

  • Une contrainte de format d'image prioritaire 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.
  • Contrainte de priorité élevée, définissant la largeur de la vue vert clair pour qu'elle soit égale à la largeur de son conteneur.
  • Une contrainte de priorité requise limitant la hauteur de la vue vert clair à être inférieure ou égale à la hauteur de son conteneur.
  • Contrainte de priorité élevée, définissant la hauteur de la vue vert clair sur la hauteur de son conteneur.

Considérons les deux contraintes de largeur. La contrainte inférieure ou égale, en elle-même, ne suffit pas pour déterminer la largeur de la vue vert clair; de nombreuses largeurs conviendront à la contrainte. En raison de l'ambiguïté, autolayout essaiera de choisir une solution qui minimise l'erreur dans l'autre contrainte (priorité élevée mais non requise). Minimiser l'erreur signifie que la largeur doit être aussi proche que possible de la largeur du conteneur, sans pour autant enfreindre la contrainte inférieure ou égale requise.

La même chose se produit avec la contrainte de hauteur. Et comme la contrainte de format d'image est également requise, elle ne peut que maximiser la taille de la sous-vue le long d'un axe (à moins que le conteneur ait le même rapport de format que la sous-vue).

Donc, je crée d'abord la contrainte de format d'image:

top aspect

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

top equal size

J'ai besoin de modifier ces contraintes pour qu'elles soient inférieures ou égales:

top less than or equal size

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

top equal size again

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

top equal not required

Enfin, vous avez demandé que la sous-vue soit centrée dans son conteneur. Je vais donc définir ces contraintes:

top centered

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

incorrect top layout

Oops! La sous-vue a été étendue pour remplir complètement son conteneur. Si je le sélectionne, je constate qu'en fait, il conserve son format d'image, mais il effectue un aspect - fill au lieu d'un aspect - fit .

Le problème est que, sur une contrainte inférieure ou égale, il importe de savoir quelle vue se trouve à chaque extrémité de la contrainte et Xcode a défini la contrainte opposée à celle que je prévoyais. 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:

fix top constraints

Xcode met à jour la mise en page:

correct top layout

Maintenant, je fais les mêmes choses avec la vue vert foncé en bas. Je dois m'assurer que son format d'image est 1: 4 (Xcode l'a redimensionné de façon étrange car il n'avait pas de contraintes). Je ne montrerai plus les étapes car elles sont identiques. Voici le résultat:

correct top and bottom layout

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

iphone 4s test

Et je peux tester dans le simulateur iPhone 6:

iphone 6 test

J'ai téléchargé mon dernier storyboard sur this Gist pour votre commodité.

1037
rob mayoff

Rob, ta réponse est géniale! Je sais aussi que cette question concerne spécifiquement la réalisation de cet objectif en utilisant la mise en page automatique. Cependant, à titre de référence, je voudrais montrer comment cela peut être fait dans le code. Vous configurez les vues du haut et du bas (bleu et rose) exactement comme l'a montré Rob. 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 modifiez la classe des vues bleue et rose dans le scénario en AspectFitViews. Enfin, vous définissez deux prises pour votre contrôleur de vue topAspectFitView et bottomAspectFitView et définissez leur 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 faire cela dans le code, mais il reste très adaptable et fonctionne avec des vues de tailles variables et des formats d’image différents.

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

24
Johannes Fahrenkrug

J'avais besoin d'une solution de la réponse acceptée, mais exécutée à partir du code. La manière la plus élégante que j'ai trouvée consiste à utiliser framework de maçonnerie .

#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);
}];
7
Tomasz Bąk

Ceci est pour macOS.

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

En gros, ajoutez deux espaces de début où l'un est> = 0 @ 1000 priorité requise et un autre = = 0 @ 250 priorité faible. Faites les mêmes réglages pour les espaces de fin, haut et bas.

Bien sûr, vous devez définir les proportions, le centre X et le centre Y.

Et puis le travail est fait!

enter image description hereenter image description here

4
brianLikeApple

Je me suis retrouvé à vouloir un aspect -remplissage comportement afin qu'un UIImageView maintienne son propre rapport d'aspect et remplisse entièrement la vue du conteneur. De manière confuse, mon UIImageView supprimait les contraintes de priorité élevée et de largeur égale (décrites dans la réponse de Rob) de priorité élevée et rendait le rendu en pleine résolution.

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

Content Compression Resistance

3
Gregor

Ceci est une excellente réponse de @ rob_mayoff à une approche centrée sur le code, en utilisant des objets NSLayoutAnchor et portés sur Xamarin. Pour moi, NSLayoutAnchor et les classes associées ont beaucoup simplifié la programmation de la mise en forme automatique:

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 ();
    }
}
1
Larry OBrien

Peut-être que c'est la réponse la plus courte, avec 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);
        }
    }
}];
1
iwill