Clase de tamaño para los modos vertical y horizontal de iPad

97

Básicamente, quiero que mis subvistas se coloquen de manera diferente según la orientación del iPad (vertical u horizontal) utilizando las clases de tamaño introducidas en xcode 6. He encontrado numerosos tutoriales que explican cómo las diferentes clases de tamaño están disponibles para Iphones en vertical y horizontal en el IB. Sin embargo, parece que no hay ninguno que cubra los modos de paisaje o retrato individual para el iPad en IB. ¿Alguien puede ayudar?

neelIVP
fuente
Parece que no hay una solución no programática, que sería necesaria para la pantalla predeterminada
SwiftArchitect

Respuestas:

174

Parece que la intención de Apple es tratar ambas orientaciones del iPad de la misma manera, pero como algunos de nosotros estamos descubriendo, existen razones de diseño muy legítimas para querer variar el diseño de la interfaz de usuario para iPad vertical frente a iPad horizontal.

Desafortunadamente, el sistema operativo actual no parece brindar soporte para esta distinción ... lo que significa que volvemos a manipular las restricciones de diseño automático en el código o soluciones alternativas similares para lograr lo que idealmente deberíamos poder obtener de forma gratuita utilizando la interfaz de usuario adaptable .

No es una solución elegante.

¿No hay una manera de aprovechar la magia que Apple ya ha incorporado en IB y UIKit para usar una clase de tamaño de nuestra elección para una orientación determinada?

~

Al pensar en el problema de manera más genérica, me di cuenta de que las 'clases de tamaño' son simplemente formas de abordar varios diseños que se almacenan en IB, de modo que se puedan llamar según sea necesario en tiempo de ejecución.

De hecho, una 'clase de tamaño' es en realidad solo un par de valores de enumeración. Desde UIInterface.h:

typedef NS_ENUM(NSInteger, UIUserInterfaceSizeClass) {
    UIUserInterfaceSizeClassUnspecified = 0,
    UIUserInterfaceSizeClassCompact     = 1,
    UIUserInterfaceSizeClassRegular     = 2,
} NS_ENUM_AVAILABLE_IOS(8_0);

Entonces, independientemente de cómo Apple haya decidido nombrar estas diferentes variaciones, fundamentalmente, son solo un par de números enteros que se usan como una especie de identificador único, para distinguir un diseño de otro, almacenado en IB.

Ahora, suponiendo que creamos un diseño alternativo (usando una clase de tamaño no utilizada) en IB, digamos, para iPad Portrait ... ¿hay alguna manera de que el dispositivo use nuestra elección de clase de tamaño (diseño de interfaz de usuario) según sea necesario en tiempo de ejecución? ?

Después de probar varios enfoques diferentes (menos elegantes) del problema, sospeché que podría haber una forma de anular la clase de tamaño predeterminada mediante programación. Y hay (en UIViewController.h):

// Call to modify the trait collection for child view controllers.
- (void)setOverrideTraitCollection:(UITraitCollection *)collection forChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0);
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0);

Por lo tanto, si puede empaquetar la jerarquía de su controlador de vista como un controlador de vista 'secundario' y agregarlo a un controlador de vista principal de nivel superior ... entonces puede anular condicionalmente al niño para que piense que es una clase de tamaño diferente al predeterminado desde el sistema operativo.

Aquí hay una implementación de muestra que hace esto, en el controlador de vista 'padre':

@interface RDTraitCollectionOverrideViewController : UIViewController {
    BOOL _willTransitionToPortrait;
    UITraitCollection *_traitCollection_CompactRegular;
    UITraitCollection *_traitCollection_AnyAny;
}
@end

@implementation RDTraitCollectionOverrideViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setUpReferenceSizeClasses];
}

- (void)setUpReferenceSizeClasses {
    UITraitCollection *traitCollection_hCompact = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
    UITraitCollection *traitCollection_vRegular = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassRegular];
    _traitCollection_CompactRegular = [UITraitCollection traitCollectionWithTraitsFromCollections:@[traitCollection_hCompact, traitCollection_vRegular]];

    UITraitCollection *traitCollection_hAny = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassUnspecified];
    UITraitCollection *traitCollection_vAny = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassUnspecified];
    _traitCollection_AnyAny = [UITraitCollection traitCollectionWithTraitsFromCollections:@[traitCollection_hAny, traitCollection_vAny]];
}

-(void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    _willTransitionToPortrait = self.view.frame.size.height > self.view.frame.size.width;
}

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]
    _willTransitionToPortrait = size.height > size.width;
}

-(UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController {
    UITraitCollection *traitCollectionForOverride = _willTransitionToPortrait ? _traitCollection_CompactRegular : _traitCollection_AnyAny;
    return traitCollectionForOverride;
}
@end

Como demostración rápida para ver si funcionaba, agregué etiquetas personalizadas específicamente a las versiones 'Regular / Regular' y 'Compacta / Regular' del diseño del controlador secundario en IB:

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

Y así es como se ve funcionando, cuando el iPad está en ambas orientaciones: ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

¡Voila! Configuraciones de clases de tamaño personalizado en tiempo de ejecución.

Con suerte, Apple hará que esto sea innecesario en la próxima versión del sistema operativo. Mientras tanto, este puede ser un enfoque más elegante y escalable que jugar programáticamente con restricciones de diseño automático o hacer otras manipulaciones en el código.

~

EDITAR (4/6/15): tenga en cuenta que el código de muestra anterior es esencialmente una prueba de concepto para demostrar la técnica. Siéntase libre de adaptarse según sea necesario para su propia aplicación específica.

~

EDITAR (24/7/15): Es gratificante que la explicación anterior parezca ayudar a desmitificar el problema. Aunque no lo he probado, el código de mohamede1945 [abajo] parece una optimización útil para propósitos prácticos. No dude en probarlo y hacernos saber lo que piensa. (En aras de la integridad, dejaré el código de muestra anterior como está).

RonDiamond
fuente
1
¡Gran publicación @RonDiamond!
partir del
3
Apple adopta un enfoque similar para redefinir la clase de tamaño de ancho en Safari, por lo que puede estar seguro de que este es un enfoque más o menos compatible. Realmente no debería necesitar los ivars adicionales; UITraitCollectionestá lo suficientemente optimizado y overrideTraitCollectionForChildViewControllerrara vez se llama por lo que no debería ser un problema hacer la verificación de ancho y crearlo luego.
zwaldowski
1
@zwaldowski Gracias. El código de muestra aquí es solo para demostrar la técnica y, obviamente, se puede optimizar de varias maneras según se desee. (Como regla general, con un objeto que se usa una y otra vez repetidamente [p. Ej., Cada vez que el dispositivo cambia de orientación], no creo que sea una mala idea sujetar un objeto; pero como usted señala, la diferencia de rendimiento aquí puede ser mínimo.)
RonDiamond
1
@Rashmi: Como mencioné en la explicación, en última instancia, no importa cuál elija, simplemente está mapeando el diseño (s) a un par de valores de enumeración. Por lo tanto, realmente puede utilizar cualquier "clase de tamaño" que tenga sentido para usted. Solo me aseguraría de que no entre en conflicto con alguna otra clase de tamaño (legítima) que pueda heredarse en algún lugar por defecto.
RonDiamond
1
En cuanto al controlador de vista infantil, no creo que importe. Debería poder instanciarlo mediante programación o desde una plumilla, siempre que se agregue correctamente como controlador secundario al contenedor.
RonDiamond
41

Como resumen de la larga respuesta de RonDiamond. Todo lo que necesita hacer es en su controlador de vista raíz.

C objetivo

- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController
{
    if (CGRectGetWidth(self.view.bounds) < CGRectGetHeight(self.view.bounds)) {
        return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
    } else {
        return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
    }
}

Rápido:

override func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection! {
        if view.bounds.width < view.bounds.height {
            return UITraitCollection(horizontalSizeClass: .Compact)
        } else {
            return UITraitCollection(horizontalSizeClass: .Regular)
        }
    }

Luego, en storyborad, use ancho compacto para vertical y ancho regular para horizontal.

mohamede1945
fuente
Me pregunto cómo funcionará eso con los nuevos modos de pantalla dividida ... ¡una forma de averiguarlo!
mm2001
UITraitCollection es solo para iOS 8.0+
mohamede1945
No funciona para mí. Tengo una vista con dos vistas de contenedor, estas deben mostrarse en una ubicación diferente según la orientación. Creé todas las clases de tamaño necesarias, todo funciona para iPhone. Intenté usar su código para separar la orientación del iPad también. Sin embargo, actuará de forma extraña en las vistas. No lo cambia como se supone que debe hacerlo. ¿Alguna pista de lo que podría estar pasando?
NoSixties
1
Esto funcionó para mí cuando anulé en - (UITraitCollection *)traitCollectionlugar de overrideTraitCollectionForChildViewController. Además, las restricciones deben coincidir con las colecciones de rasgos, por lo que wC (hAny).
H. de Jonge
1
@Apollo Lo haría, pero no respondería a la pregunta real. Eche un vistazo aquí para ver la muestra de Apple de cómo usar setOverrideTraitCollection github.com/ios8/AdaptivePhotosAnAdaptiveApplication/blob/master/…
malhal
5

El iPad tiene el rasgo de tamaño "normal" tanto para las dimensiones horizontales como verticales, sin distinción entre retrato y paisaje.

Estos rasgos de tamaño se pueden anular en su UIViewControllercódigo de subclase personalizado , mediante un método traitCollection, por ejemplo:

- (UITraitCollection *)traitCollection {
    // Distinguish portrait and landscape size traits for iPad, similar to iPhone 7 Plus.
    // Be aware that `traitCollection` documentation advises against overriding it.
    UITraitCollection *superTraits = [super traitCollection];
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
        UITraitCollection *horizontalRegular = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
        UITraitCollection *verticalRegular = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassRegular];
        UITraitCollection *regular = [UITraitCollection traitCollectionWithTraitsFromCollections:@[horizontalRegular, verticalRegular]];

        if ([superTraits containsTraitsInCollection:regular]) {
            if (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation])) {
                // iPad in portrait orientation
                UITraitCollection *horizontalCompact = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
                return [UITraitCollection traitCollectionWithTraitsFromCollections:@[superTraits, horizontalCompact, verticalRegular]];
            } else {
                // iPad in landscape orientation
                UITraitCollection *verticalCompact = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassCompact];
                return [UITraitCollection traitCollectionWithTraitsFromCollections:@[superTraits, horizontalRegular, verticalCompact]];
            }
        }
    }
    return superTraits;
}

- (BOOL)prefersStatusBarHidden {
    // Override to negate this documented special case, and avoid erratic hiding of status bar in conjunction with `traitCollection` override:
    // For apps linked against iOS 8 or later, this method returns true if the view controller is in a vertically compact environment.
    return NO;
}

Esto le da al iPad los mismos rasgos de tamaño que el iPhone 7 Plus. Tenga en cuenta que otros modelos de iPhone generalmente tienen el rasgo de 'ancho compacto' (en lugar de ancho regular) independientemente de la orientación.

La imitación del iPhone 7 Plus de esta manera permite que ese modelo se use como un sustituto del iPad en el Interface Builder de Xcode, que desconoce las personalizaciones en el código.

Tenga en cuenta que la Vista dividida en el iPad puede usar características de tamaño diferentes a las del funcionamiento normal de pantalla completa.

Esta respuesta se basa en el enfoque adoptado en esta publicación de blog , con algunas mejoras.

Actualización 2019-01-02: actualizado para corregir la barra de estado oculta intermitente en el panorama del iPad y el posible pisoteo de rasgos (más nuevos) en UITraitCollection. También señaló que la documentación de Apple en realidad recomienda no anular traitCollection, por lo que en el futuro pueden surgir problemas con esta técnica.

jedwidz
fuente
Apple especifica que la traitCollectionpropiedad sea de solo lectura: developer.apple.com/documentation/uikit/uitraitenvironment/…
cleverbit
4

La respuesta larga y útil de RonDiamond es un buen comienzo para comprender los principios, sin embargo, el código que funcionó para mí (iOS 8+) se basa en un método primordial (UITraitCollection *)traitCollection

Por lo tanto, agregue restricciones en InterfaceBuilder con variaciones para Ancho - Compacto, por ejemplo, para la propiedad de restricción Instalada. Entonces Ancho: Cualquiera será válido para paisaje, Ancho: Compacto para Retrato.

Para cambiar las restricciones en el código según el tamaño del controlador de vista actual, simplemente agregue lo siguiente en su clase UIViewController:

- (UITraitCollection *)traitCollection
{
    UITraitCollection *verticalRegular = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassRegular];

    if (self.view.bounds.size.width < self.view.bounds.size.height) {
        // wCompact, hRegular
        return [UITraitCollection traitCollectionWithTraitsFromCollections:
                @[[UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact],
                  verticalRegular]];
    } else {
        // wRegular, hRegular
        return [UITraitCollection traitCollectionWithTraitsFromCollections:
                @[[UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular],
                  verticalRegular]];
    }
}
Jadamec
fuente
0

¿Qué tan diferente será su modo de paisaje que su modo de retrato? Si es muy diferente, podría ser una buena idea crear otro controlador de vista y cargarlo cuando el dispositivo esté en horizontal

Por ejemplo

    if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) 
    //load landscape view controller here
MendyK
fuente
Sí, esta es una opción. Pero no creo que sea el más óptimo. Mi punto es que si hay una opción para usar diferentes rasgos de clase de tamaño para los modos vertical y horizontal para el iphone en ios8, ¿por qué no lo mismo para el iPad?
neelIVP
Antes de Xcode 6, podemos usar diferentes guiones gráficos para diferentes orientaciones. Eso no es muy eficiente si la mayoría de los controladores de vista son iguales. Pero es muy conveniente para diferentes diseños. En Xcode 6, no hay forma de hacerlo. Tal vez la única solución sea crear un controlador de vista diferente para una orientación diferente.
Bagusflyer
2
Cargar un viewController diferente cada vez parece muy ineficaz, especialmente si sucede algo en la pantalla. Es mucho mejor usar la misma vista y manipular la restricción de diseño automático o la posición de los elementos en el código. O utilizar el truco mencionado anteriormente, hasta que Apple solucione este problema.
Pahnev
0

Versión Swift 5. Funciona bien.

override func overrideTraitCollection(forChild childViewController: UIViewController) -> UITraitCollection? {
    if UIScreen.main.bounds.width > UIScreen.main.bounds.height {
        let collections = [UITraitCollection(horizontalSizeClass: .regular),
                           UITraitCollection(verticalSizeClass: .compact)]
        return UITraitCollection(traitsFrom: collections)
    }
    return super.overrideTraitCollection(forChild: childViewController)
}
Li Jin
fuente
-3

Código Swift 3.0 para la solución @RonDiamond

class Test : UIViewController {


var _willTransitionToPortrait: Bool?
var _traitCollection_CompactRegular: UITraitCollection?
var _traitCollection_AnyAny: UITraitCollection?

func viewDidLoad() {
    super.viewDidLoad()
    self.upReferenceSizeClasses = null
}

func setUpReferenceSizeClasses() {
    var traitCollection_hCompact: UITraitCollection = UITraitCollection(horizontalSizeClass: UIUserInterfaceSizeClassCompact)
    var traitCollection_vRegular: UITraitCollection = UITraitCollection(verticalSizeClass: UIUserInterfaceSizeClassRegular)
    _traitCollection_CompactRegular = UITraitCollection(traitsFromCollections: [traitCollection_hCompact,traitCollection_vRegular])
    var traitCollection_hAny: UITraitCollection = UITraitCollection(horizontalSizeClass: UIUserInterfaceSizeClassUnspecified)
    var traitCollection_vAny: UITraitCollection = UITraitCollection(verticalSizeClass: UIUserInterfaceSizeClassUnspecified)
    _traitCollection_AnyAny = UITraitCollection(traitsFromCollections: [traitCollection_hAny,traitCollection_vAny])
}

func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    _willTransitionToPortrait = self.view.frame.size.height > self.view.frame.size.width
}

func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    _willTransitionToPortrait = size.height > size.width
}

func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection {
    var traitCollectionForOverride: UITraitCollection = _willTransitionToPortrait ? _traitCollection_CompactRegular : _traitCollection_AnyAny
    return traitCollectionForOverride
}}
Zeeshan
fuente