Emulación de comportamiento de ajuste de aspecto utilizando restricciones de AutoLayout en Xcode 6

336

Quiero usar AutoLayout para dimensionar y diseñar una vista de una manera que recuerde el modo de contenido de aspecto adaptado de UIImageView.

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

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

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

En cualquier caso, deseo que la subvista esté centrada horizontal y verticalmente dentro de la vista del contenedor.

¿Hay alguna manera de lograr esto usando las restricciones de AutoLayout en Xcode 6 o en la versión anterior? Lo ideal es usar Interface Builder, pero si no es posible, es posible definir tales restricciones mediante programación.

chris838
fuente

Respuestas:

1046

No estás describiendo escala a medida; Estás describiendo un ajuste de aspecto. (He editado su pregunta a este respecto.) La subvista 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, puedes hacer esto con el diseño automático. 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. Voy a establecer 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 mientras mantiene su relación de aspecto y ajuste en su envase.

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

Tenga en cuenta que todavía no actualizo el marco. Me resulta más fácil dejar espacio entre las vistas al configurar 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 rosadas

También necesito una restricción de igual altura entre las vistas rosa y azul. Esto hará que cada uno llene la mitad de la pantalla:

ingrese la descripción de la imagen aquí

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

contenedores dispuestos

Entonces, las restricciones que he establecido hasta ahora son correctas. Lo deshago 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 posterior.
  • Una restricción de prioridad requerida que limita el ancho de la vista verde claro para que sea menor o igual que el 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 para que sea menor o igual que la altura de su contenedor.
  • Una restricción de alta prioridad que establece que la altura de la vista verde clara sea igual a la altura de su contenedor.

Consideremos las dos restricciones de ancho. La restricción menor o igual, por sí sola, no es suficiente para determinar el ancho de la vista verde claro; muchos anchos se ajustarán a la restricción. Dado que existe ambigüedad, la distribución automática intentará elegir una solución que minimice el error en la otra restricción (de alta prioridad pero no obligatoria). Minimizar el error significa hacer que el ancho sea lo más cercano 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 la relación de aspecto, solo puede maximizar el tamaño de la subvista a lo largo de un eje (a menos que el contenedor tenga la misma relación de aspecto que la subvista).

Entonces, primero creo la restricción de la relación de aspecto:

aspecto superior

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

tamaño igual superior

Necesito editar estas restricciones para que sean menores o iguales:

superior menor o igual tamaño

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

superior igual tamaño de nuevo

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

igual superior no requerido

Finalmente, solicitó que la subvista se centre en su contenedor, así que estableceré esas restricciones:

centrado en la parte superior

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

diseño superior incorrecto

¡Uy! La subvista 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 relleno de aspecto en lugar de un ajuste de aspecto .

El problema es que en una restricción menor o igual, importa qué vista se encuentre en cada extremo de la restricción, y Xcode ha establecido la restricción opuesta a mi expectativa. Podría seleccionar cada una de las dos restricciones e invertir su primer y segundo elemento. En cambio, solo seleccionaré la subvista y cambiaré las restricciones para que sean mayores o iguales:

arreglar las principales restricciones

Xcode actualiza el diseño:

diseño superior correcto

Ahora hago lo mismo con 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 volveré a mostrar los pasos ya que son los mismos. Aquí está el resultado:

correcto diseño superior e inferior

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

prueba de iphone 4s

Y puedo probar en el simulador de iPhone 6:

prueba de iphone 6

He subido mi guión gráfico final a esta esencia para su conveniencia.

Rob Mayoff
fuente
27
Solía LICEcap . Es gratis (GPL) y graba directamente en GIF. Siempre que el GIF animado no supere los 2 MB, puede publicarlo en este sitio como cualquier otra imagen.
rob mayoff
1
@LucaTorella Con solo las restricciones de alta prioridad, el diseño es ambiguo. El hecho de que obtenga el resultado deseado hoy no significa que lo hará en la próxima versión de iOS. Suponga que la subvista desea una relación de aspecto 1: 1, la supervista tiene un tamaño de 99 × 100 y solo tiene la relación de aspecto requerida y las restricciones de igualdad de alta prioridad. Luego, el diseño automático puede establecer la subvista en tamaño 99 × 99 (con error total 1) o tamaño 100 × 100 (con error total 1). Un tamaño es de ajuste de aspecto; el otro es de aspecto lleno. Agregar las restricciones requeridas produce una solución única de mínimo error.
rob mayoff
1
@LucaTorella Gracias por la corrección con respecto a la disponibilidad de restricciones de relación de aspecto.
rob mayoff
11
10,000 aplicaciones mejores con solo una publicación ... increíble.
DonVaughn
2
Wow .. Mejor respuesta aquí en desbordamiento de pila que he visto .. Gracias por este señor ..
0yeoj
24

¡Rob, tu respuesta es asombrosa! También sé que esta pregunta se trata específicamente de lograr esto mediante el diseño automático. Sin embargo, solo como referencia, me gustaría mostrar cómo se puede hacer esto en código. Configura las vistas superior e inferior (azul y rosa) tal como mostró Rob. Luego creas una costumbre 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

A continuación, cambia la clase de las vistas azul y rosa en el guión gráfico para que sea AspectFitViews. Por último se establece dos salidas a su viewcontroller topAspectFitViewy bottomAspectFitViewy establecer sus childViews 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;
}

Por lo tanto, 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

Johannes Fahrenkrug
fuente
1
@DanielGalasko Gracias, tienes razón. Menciono eso en mi respuesta. Simplemente pensé que sería una referencia útil para comparar qué pasos están involucrados en ambas soluciones.
Johannes Fahrenkrug
1
Muy útil para tener la referencia programática también.
ctpenrose
@JohannesFahrenkrug si tienes un proyecto tutorial, ¿puedes compartirlo? gracias :)
Erhan Demirci
1
@ErhanDemirci Claro, aquí tienes: github.com/jfahrenkrug/SPWKAspectFitView
Johannes Fahrenkrug
8

Necesitaba una solución de la respuesta aceptada, pero ejecutada desde el código. La forma más elegante que he encontrado es usando el marco de 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
fuente
5

Esto es para macOS.

Tengo problemas para usar la forma de Rob para lograr un ajuste de aspecto en la aplicación OS X. Pero lo hice de otra manera: en lugar de usar el ancho y la altura, utilicé el espacio inicial, posterior, superior e inferior.

Básicamente, agregue dos espacios iniciales donde uno es> = 0 @ 1000 prioridad requerida y otro es = 0 @ 250 prioridad baja. Realice la misma configuración en el espacio posterior, 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!

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

brianLikeApple
fuente
¿Cómo podrías hacer esto programáticamente?
FateNuller
@FateNuller ¿Solo sigue los pasos?
brianLikeApple 02 de
4

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

La solución fue simplemente establecer la prioridad de resistencia de compresión de contenido de UIImageView por debajo de la prioridad de las restricciones de igual ancho y altura:

Resistencia a la compresión de contenido

Gregor
fuente
2

Este es un puerto de la excelente respuesta de @ rob_mayoff a un enfoque centrado en el código, utilizando NSLayoutAnchorobjetos y portado a Xamarin. Para mí, NSLayoutAnchory las clases relacionadas han hecho que AutoLayout 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 ();
    }
}
Larry OBrien
fuente
1

Tal vez esta sea la respuesta más corta, con Masonry , que también admite relleno de aspecto y estiramiento.

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);
        }
    }
}];
Señor ming
fuente