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

Quiero utilizar AutoLayout para dimensionar y organizar una vista de una manera que recuerde el modo de contenido de ajuste de aspecto 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 ancha 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 se centre 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 sería usar Interface Builder, pero si no es posible definir tales restricciones programáticamente.

No estás describiendo la escala para ajustar; estás describiendo aspecto-ajuste. (He editado su pregunta al respecto.) La subvista se vuelve lo más grande posible, manteniendo su relación de aspecto y encajando completamente dentro de su elemento primario.

De todos modos, puedes hacer esto con el diseño automático. Puedes hacerlo completamente en IB desde 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. Estableceré restricciones para que la vista azul ocupe la mitad superior de la pantalla, la vista rosa ocupe la mitad inferior de la pantalla, y cada vista verde se expanda tanto como sea posible, manteniendo su relación de aspecto y encajando en su envase.

Primero, crearé restricciones en los cuatro lados de la vista azul. Lo fijaré en 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 aún no actualizo el marco. Me resulta más fácil dejar espacio entre las vistas al configurar las restricciones, y simplemente establecer las constantes a 0 (o lo que sea) a mano.

A continuación, fije los bordes izquierdo, inferior y derecho de la vista rosa a su vecino más cercano. No es necesario 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 hará que cada uno llene la mitad de la pantalla:

enter image description here

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. Deshago eso y comienzo a trabajar en la vista verde claro.

La adaptación de aspecto a 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 claro sea igual a la altura de su contenedor.

Consideremos las dos restricciones de ancho. La restricción menor que 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. Como hay ambigüedad, Autolayout intentará elegir una solución que minimice el error en la otra restricción (alta prioridad pero no requerida). Minimizar el error significa hacer que el ancho sea lo más parecido 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 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 relación de aspecto:

aspecto superior

Luego creo las mismas restricciones de ancho y altura con el contenedor:

tamaño superior igual

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

arriba menor o igual tamaño

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

tamaño igual superior de nuevo

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

superior igual no requerido

Finalmente, solicitó que la subvista esté centrada en su contenedor, por lo que estableceré esas restricciones:

arriba centrado

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

diseño superior incorrecto

Oops! La subvista se ha expandido para llenar completamente su contenedor. Si lo selecciono, puedo ver que de hecho mantiene su relación de aspecto, pero está haciendo un relleno de aspecto en lugar de un ajuste de aspecto.

El problema es que con una restricción menor o igual, importa qué vista se encuentra en cada extremo de la restricción, y Xcode ha establecido la restricción opuesta a la que yo esperaba. Podría seleccionar cada una de las dos restricciones e invertir sus elementos primero y segundo. En cambio, solo seleccionaré la subvista y cambiaré las restricciones para que sean mayores que o iguales:

arreglar las principales restricciones

Xcode actualiza el diseño:

corregir el diseño superior

Ahora hago todas las mismas cosas con la vista verde oscuro en la parte inferior. Necesito asegurarme de que su relación de aspecto es 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 lo mismo. Este es el resultado:

corregir el diseño superior e inferior

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

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

AspectFitView.h :

 #import  @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 sean AspectFitView . Finalmente establece dos salidas para su viewcontroller topAspectFitView y bottomAspectFitView y establece sus childView 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 el código y todavía es muy adaptable y funciona con vistas de tamaño variable y diferentes relaciones de aspecto.

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

Necesitaba una solución de la respuesta aceptada, pero ejecutada desde el código. La forma más elegante que he encontrado es utilizando el 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); }]; 

Esto es para macOS.

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

Básicamente, agregue dos espacios iniciales donde uno es> = 0 @ 1000 prioridad requerida y otro es = 0 @ 250 baja prioridad. Haga las mismas configuraciones en el espacio posterior, superior e inferior.

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

¡Y luego el trabajo está hecho!

enter image description here enter image description here

Descubrí que quería un comportamiento de relleno de aspecto para que un UIImageView mantuviera su propia relación de aspecto y completara por completo la vista de contenedor. Confusamente, mi UIImageView estaba rompiendo AMBAS restricciones de igual-ancho e igual altura de alta prioridad (descritas en la respuesta de Rob) y renderizado a resolución completa.

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

Resistencia a la compresión del contenido

Esta es una respuesta excelente de @ rob_mayoff a un enfoque centrado en el código, utilizando objetos NSLayoutAnchor y portado a Xamarin. Para mí, NSLayoutAnchor y las clases relacionadas han hecho que AutoLayout sea mucho más fácil de progtwigr:

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

Tal vez esta es la respuesta más corta, con la 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); } } }];