Emulación del comportamiento de ajuste de aspecto mediante restricciones de diseño automático en Xcode 6


Quiero usar AutoLayout para dimensionar y diseñar una vista de una manera que recuerde al modo de contenido aspect-fit de UIImageView.

Tengo una subview dentro de una vista de contenedor en Interface Builder. El subview tiene una relación de aspecto inherente que deseo respetar. El tamaño de la vista del contenedor se desconoce hasta el tiempo de ejecución.

Si la relación de aspecto de la vista contenedor es más ancha que la vista secundaria, entonces quiero que la altura de la vista secundaria sea igual a la altura de la vista principal.

Si el envase la relación de aspecto de la vista es más alta que la vista secundaria, entonces quiero que el ancho de la vista secundaria sea igual al ancho de la vista principal.

En cualquier caso, deseo que la subvista se centre horizontal y verticalmente dentro de la vista del contenedor.

¿Hay alguna manera de lograr esto usando restricciones de diseño automático en Xcode 6 o en la versión anterior? Idealmente usando Interface Builder, pero si no, tal vez sea posible definir tales restricciones mediante programación.

Author: rob mayoff, 2014-09-10

7 answers

No estás describiendo escala a ajuste; estás describiendo ajuste de aspecto. (He editado su pregunta en este sentido.) El subview se vuelve lo más grande posible mientras mantiene su relación de aspecto y se ajusta completamente dentro de su padre.

De todos modos, puede hacer esto con auto layout. Puede hacerlo completamente en IB a partir de Xcode 5.1. Comencemos con algunas vistas:

algunas vistas

La vista verde claro tiene una relación de aspecto de 4:1. La vista verde oscuro tiene una relación de aspecto de 1: 4. Soy ir a configurar restricciones para que la vista azul llene la mitad superior de la pantalla, la vista rosa llene la mitad inferior de la pantalla, y cada vista verde se expanda tanto como sea posible, manteniendo su relación de aspecto y ajustándose a su contenedor.

Primero, crearé restricciones en los cuatro lados de la vista azul. Lo fijaré a su vecino más cercano en cada borde, con una distancia de 0. Me aseguro de desactivar los márgenes:

restricciones azules

Ten en cuenta que yo no actualizo el marco todavía. Me resulta más fácil dejar espacio entre las vistas al establecer restricciones, y simplemente establecer las constantes a 0 (o lo que sea) a mano.

A continuación, fijo los bordes izquierdo, inferior y derecho de la vista rosa a su vecino más cercano. No necesito configurar una restricción de borde superior porque su borde superior ya está restringido al borde inferior de la vista azul.

restricciones de color rosa

También necesito una restricción de alturas iguales entre las vistas rosa y azul. Esto los hará cada uno llena la mitad de la pantalla:

introduzca la descripción de la imagen aquí

Si le digo a Xcode que actualice todos los fotogramas ahora, obtengo esto:

contenedores dispuestos

Así que las restricciones que he configurado hasta ahora son correctas. Deshago eso y empiezo a trabajar en la vista verde claro.

El ajuste de aspecto de la vista verde claro requiere cinco restricciones:

  • Una restricción de relación de aspecto de prioridad requerida en la vista verde claro. Puede crear esta restricción en un xib o storyboard con Xcode 5.1 o tarde.
  • Una restricción de prioridad requerida que limita el ancho de la vista verde claro a ser menor o igual al ancho de su contenedor.
  • Una restricción de alta prioridad que establece el ancho de la vista verde claro para que sea igual al ancho de su contenedor.
  • Una restricción de prioridad requerida que limita la altura de la vista verde claro a ser menor o igual a la altura de su contenedor.
  • Una restricción de alta prioridad que establece la altura de la vista verde claro ser igual a la altura de su contenedor.

Consideremos las dos restricciones de ancho. La restricción menor o igual, por sí misma, no es suficiente para determinar el ancho de la vista verde claro; muchos anchos se ajustarán a la restricción. Dado que hay ambigüedad, autolayout intentará elegir una solución que minimice el error en la otra restricción (de alta prioridad pero no requerida). Minimizar el error significa hacer que el ancho se acerque lo más posible al ancho del contenedor, sin violar la restricción requerida menor o igual.

Lo mismo sucede con la restricción de altura. Y dado que también se requiere la restricción de relación de aspecto, solo puede maximizar el tamaño del subview a lo largo de un eje (a menos que el contenedor tenga la misma relación de aspecto que el subview).

Así que primero creo la restricción de relación de aspecto:

top aspecto

Luego creo restricciones de ancho y alto iguales con el contenedor:

tamaño superior igual

Necesito editar estas restricciones para que sean restricciones menores o iguales:

parte superior de menor o igual tamaño

A continuación necesito crear otro conjunto de restricciones de anchura y altura iguales con el contenedor:

top igual tamaño de nuevo

Y necesito hacer que estas nuevas restricciones sean menos de la prioridad requerida:

top equal no es necesario

Finalmente, pediste que el subview se centrara en su contenedor, así que configuraré esas restricciones:

centrado en la parte superior

Ahora, a prueba, seleccionaré el controlador de vista y le pediré a Xcode que actualice todos los fotogramas. Esto es lo que obtengo:

diseño superior incorrecto

Oops! El subview se ha expandido para llenar completamente su contenedor. Si lo selecciono, puedo ver que de hecho ha mantenido su relación de aspecto, pero está haciendo un aspecto-fill en lugar de un aspecto - fit.

El problema es que en una restricción menor o igual, importa qué vista está en cada extremo de la restricción, y Xcode ha configurado la restricción opuesta a mi expectativa. Podría seleccionar cada una de las dos restricciones e invertir sus elementos primero y segundo. En su lugar, solo seleccionaré la subview y cambiaré las restricciones para que sean mayores o iguales:

corregir las principales restricciones

Xcode actualiza el diseño:

diseño superior correcto

Ahora hago todas las mismas cosas a la vista verde oscuro en la parte inferior. Necesito asegurarme de que su relación de aspecto sea 1:4 (Xcode lo redimensionó de una manera extraña ya que no tenía restricciones). No voy a mostrar los pasos de nuevo, ya que son los mismos. Aquí está el resultado:

correcta disposición superior e inferior

Ahora puedo ejecutarlo en el simulador del iPhone 4S, que tiene un tamaño de pantalla diferente al que usa IB, y probar la rotación:

prueba de iphone 4s

Y puedo probar en el simulador del iPhone 6:

prueba de iphone 6

He subido mi guion gráfico final a este contenido esencial para su conveniencia.

 1024
Author: rob mayoff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-26 18:39:01

Rob, tu respuesta es impresionante! También sé que esta pregunta se trata específicamente de lograr esto mediante el uso de diseño automático. Sin embargo, solo como referencia, me gustaría mostrar cómo se puede hacer esto en código. Se configuran las vistas superior e inferior (azul y rosa) al igual que Rob mostró. Luego se crea un AspectFitView personalizado:

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

A continuación, cambia la clase de las vistas azul y rosa en el guion gráfico para ser AspectFitView s. Finalmente establece dos salidas en su viewcontroller topAspectFitView y bottomAspectFitView y establece sus childView s en 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;
}

Así que no es difícil hacer esto en código y todavía es muy adaptable y funciona con vistas de tamaño variable y diferentes relaciones de aspecto.

Actualización de julio de 2015 : Encuentre una aplicación de demostración aquí: https://github.com/jfahrenkrug/SPWKAspectFitView

 24
Author: Johannes Fahrenkrug,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-17 13:06:04

Necesitaba una solución a partir de la respuesta aceptada, pero ejecutada desde el código. La forma más elegante que he encontrado es usando marco de mampostería.

#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);
}];
 6
Author: Tomasz Bąk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-14 13:05:39

Esto es para macOS.

Tengo problemas para usar la forma de Rob para lograr el ajuste de aspecto en la aplicación OS X. Pero lo hice con otra forma Instead en lugar de usar ancho y alto, usé espacio inicial, final, superior e inferior.

Básicamente, agregue dos espacios iniciales donde uno es >= 0 @1000 prioridad requerida y otro es = 0 @250 prioridad baja. Haga la misma configuración en el espacio final, superior e inferior.

Por supuesto, debe establecer la relación de aspecto y el centro X y el centro Y.

Y entonces el trabajo está hecho!

introduzca la descripción de la imagen aquí introduzca la descripción de la imagen aquí

 4
Author: brianLikeApple,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-17 09:51:31

Me encontré queriendo el comportamiento aspect- fill para que una UIImageView mantuviera su propia relación de aspecto y llenara completamente la vista del contenedor. Confusamente, mi UIImageView estaba rompiendo LAS restricciones de alta prioridad de igual ancho e igual altura (descritas en la respuesta de Rob) y renderizando a resolución completa.

La solución fue simplemente establecer la Prioridad de Resistencia a la Compresión de Contenido de UIImageView más baja que la prioridad de igual ancho e igual altura limitaciones:

Resistencia a la Compresión del Contenido

 3
Author: Gregor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-05-31 03:37:23

Esta es una adaptación de la excelente respuesta de @rob_mayoff a un enfoque centrado en el código, usando objetos NSLayoutAnchor y portado a Xamarin. Para mí, NSLayoutAnchor y clases relacionadas han hecho que el diseño automático sea mucho más fácil de programar:

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
Author: Larry OBrien,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-20 00:00:36

Tal vez esta sea la respuesta más corta, con Masonería.

[containerView addSubview:subview];

[subview mas_makeConstraints:^(MASConstraintMaker *make) {
    if (contentMode == ContentMode_scaleToFill) {
        make.edges.equalTo(containerView);
    }
    else {
        make.center.equalTo(containerView);
        make.edges.equalTo(containerView).priorityHigh();
        make.width.equalTo(content.mas_height).multipliedBy(4.0 / 3);
        if (contentMode == ContentMode_scaleAspectFit) {
            make.width.height.lessThanOrEqualTo(containerView);
        }
        else { // contentMode == ContentMode_scaleAspectFill
            make.width.height.greaterThanOrEqualTo(containerView);
        }
    }
}];
 0
Author: iwill,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-12 12:33:17