Guiones gráficos unificados en Xamarin.iOS

iOS 8 incluye un nuevo mecanismo más sencillo de usar para crear la interfaz de usuario: el guión gráfico unificado. Con un único guión gráfico para cubrir todos los diferentes tamaños de pantalla de hardware, se pueden crear vistas rápidas y dinámicas con el concepto "diseñar una vez, usar muchas veces".

Como el desarrollador ya no necesita crear un guión gráfico independiente y específico para dispositivos iPhone o iPad, tiene flexibilidad para diseñar aplicaciones con una interfaz común y, luego, personalizar esa interfaz para diferentes clases de tamaño. De este modo, se pueden adaptar las aplicaciones a los puntos fuertes de cada factor de forma, así como ajustar cada interfaz de usuario para proporcionar una experiencia óptima.

Clases de tamaño

Antes de iOS 8, el desarrollador usaba UIInterfaceOrientation y UIInterfaceIdiom para diferenciar entre los modos vertical y horizontal y entre los dispositivos iPhone y iPad. En iOS8, la orientación y el dispositivo se determinan mediante las clases de tamaño.

Los dispositivos se definen mediante clases de tamaño, tanto en los ejes verticales como en los horizontales, y hay dos tipos de clases de tamaño en iOS 8:

  • Normal: para un tamaño de pantalla grande (como un iPad) o un gadget que dé la impresión de un tamaño grande (como UIScrollView).
  • Compacto: para dispositivos más pequeños (como el iPhone). Este tamaño tiene en cuenta la orientación del dispositivo.

Si los dos conceptos se usan juntos, el resultado es una cuadrícula de 2 x 2 que define los diferentes tamaños posibles que se pueden usar en ambas orientaciones, como se muestra en el siguiente diagrama:

A 2 x 2 grid that defines the different possible sizes that can be used in Regular and Compact orientations

El desarrollador puede crear un controlador de vista que use cualquiera de las cuatro posibilidades que darán lugar a diseños diferentes (como se muestra en los gráficos anteriores).

Clases de tamaño de iPad

El iPad, debido a su tamaño, tiene un tamaño de clase normal para ambas orientaciones.

iPad Size Classes

Clases de tamaño de iPhone

El iPhone tiene clases de tamaño diferentes en función de la orientación del dispositivo:

iPhone Size Classes

  • Cuando el dispositivo está en modo vertical, la pantalla tiene una clase compacta horizontalmente y una clase normal verticalmente.
  • Cuando el dispositivo está en modo horizontal, las clases de pantalla se invierten con respecto al modo vertical.

Clases de tamaño del iPhone 6 Plus

Los tamaños son los mismos que los de los anteriores dispositivos iPhone cuando están en orientación vertical, pero diferentes en el modo horizontal:

iPhone 6 Plus Size Classes

Dado que el iPhone 6 Plus tiene una pantalla lo suficientemente grande, puede tener una clase de tamaño de ancho normal en el modo horizontal.

Compatibilidad con una nueva escala de pantalla

El iPhone 6 Plus tiene una nueva pantalla Retina HD con un factor de escala de pantalla de 3,0 (tres veces la resolución de pantalla del iPhone original). Para proporcionar la mejor experiencia posible en estos dispositivos, incluya nuevas ilustraciones diseñadas para esta escala de pantalla. En Xcode 6 y las versiones superiores, los catálogos de recursos pueden incluir imágenes en tamaños 1x, 2x y 3x; basta con agregar los nuevos recursos de imagen para que iOS elija los recursos correctos al ejecutarse en un iPhone 6 Plus.

El comportamiento de carga de imágenes en iOS también reconoce un sufijo @3x en los archivos de imagen. Por ejemplo, si el desarrollador incluye un recurso de imagen (en resoluciones diferentes) en el paquete de la aplicación con los siguientes nombres de archivo: MonkeyIcon.png, MonkeyIcon@2x.png y MonkeyIcon@3x.png. En el iPhone 6 Plus, se usará automáticamente la imagen MonkeyIcon@3x.png si el desarrollador carga una imagen con el código siguiente:

UIImage icon = UIImage.FromFile("MonkeyImage.png");

Pantallas de inicio dinámicas

Cuando se inicia una aplicación iOS, el archivo de la pantalla de inicio se muestra como una pantalla de presentación para informar al usuario de que la aplicación se está iniciando. Antes de iOS 8, el desarrollador tenía que incluir varios recursos de imagen Default.png para cada tipo de dispositivo, orientación y resolución de pantalla en los que se ejecutaría la aplicación.

Como novedad en iOS 8, el desarrollador puede crear un único archivo atómico .xib en Xcode que use clases de diseño y tamaño automáticos para crear una pantalla de inicio dinámica que funcione para cada dispositivo, resolución y orientación. Esto no solo disminuye la cantidad de trabajo que debe realizar el desarrollador para crear y mantener todos los recursos de imagen necesarios, sino que también reduce el tamaño del paquete instalado de la aplicación.

Rasgos

Los rasgos son propiedades que se pueden usar para determinar cómo cambia un diseño a medida que cambia su entorno. Constan de un conjunto de propiedades (HorizontalSizeClass y VerticalSizeClass en función de UIUserInterfaceSizeClass), así como el idioma de la interfaz (UIUserInterfaceIdiom) y la escala de visualización.

Todos los estados anteriores se encapsulan en un contenedor al que Apple hace referencia como colección de rasgos (UITraitCollection), que contiene no solo las propiedades, sino también sus valores.

Entorno de rasgos

Los entornos de rasgos son una nueva interfaz en iOS 8 y pueden devolver una colección de rasgos para los siguientes objetos:

  • Pantallas (UIScreens).
  • Windows (UIWindows).
  • Controladores de vista (UIViewController).
  • Vistas (UIView).
  • Controlador de presentación (UIPresentationController).

El desarrollador usa la colección de rasgos devuelta por un entorno de rasgos para determinar cómo se debe diseñar una interfaz de usuario.

Todos los entornos de rasgos forman una jerarquía como se muestra en el diagrama siguiente:

The Trait Environments hierarchy diagram

La colección de rasgos de cada uno de los entornos de rasgos anteriores fluirá, de forma predeterminada, del entorno primario al secundario.

Además de obtener la colección de rasgos actual, el entorno de rasgos tiene un método TraitCollectionDidChange que se puede invalidar en las subclases Vista o Controlador de vista. El desarrollador puede usar este método para modificar cualquiera de los elementos de la interfaz de usuario que dependen de rasgos cuando esos rasgos cambien.

Colecciones de rasgos típicas

En esta sección se tratan los tipos habituales de colecciones de rasgos que el usuario se encontrará al trabajar con iOS 8.

A continuación se muestra una colección de rasgos típica que el desarrollador puede ver en un iPhone:

Propiedad Valor
HorizontalSizeClass Compacto
VerticalSizeClass Regular
UserInterfaceIdom Teléfono
DisplayScale 2.0

El conjunto anterior representaría una colección de rasgos completa, ya que tiene valores para todas sus propiedades de rasgo.

También es posible que en una colección de rasgos falten algunos de sus valores (lo que Apple denomina No especificado):

Propiedad Valor
HorizontalSizeClass Compacto
VerticalSizeClass Sin especificar
UserInterfaceIdom Sin especificar
DisplayScale Sin especificar

Generalmente, sin embargo, cuando el desarrollador solicita al entorno de rasgos la colección de rasgos, este devolverá una colección completa, como se muestra en el ejemplo anterior.

Si un entorno de rasgos (como una vista o un controlador de vista) no se encuentra en la jerarquía de vistas actual, es posible que el desarrollador obtenga valores no especificados para una o varias de las propiedades de rasgo.

El desarrollador también obtendrá una colección de rasgos parcialmente completa si usa uno de los métodos de creación proporcionados por Apple, como UITraitCollection.FromHorizontalSizeClass, para crear una nueva colección.

Una de las operaciones que se pueden realizar en varias colecciones de rasgos es compararlas entre sí, lo que implica preguntar a una colección de rasgos si contiene otra. Lo que se entiende por Contención es que, para cualquier rasgo especificado en la segunda colección, el valor debe coincidir exactamente con el valor de la primera colección.

Para probar dos rasgos, use el método Contains de UITraitCollection para pasar el valor del rasgo que se va a probar.

El desarrollador puede realizar las comparaciones manualmente en el código para determinar cómo diseñar vistas o controladores de vista. Sin embargo, UIKit usa este método internamente para proporcionar parte de su funcionalidad, como, por ejemplo, en el proxy de apariencia.

Proxy de apariencia

El proxy de apariencia se introdujo en versiones anteriores de iOS para permitir a los desarrolladores personalizar las propiedades de las vistas. Se ha ampliado en iOS 8 para admitir las colecciones de rasgos.

Los proxies de apariencia ahora incluyen un nuevo método, AppearanceForTraitCollection, que devuelve un nuevo proxy de apariencia para la colección de rasgos especificada que se haya pasado. Las personalizaciones que realiza el desarrollador en ese proxy de apariencia solo surtirán efecto en las vistas que se ajustan a la colección de rasgos especificada.

Generalmente, el desarrollador pasará una colección de rasgos parcialmente especificada al método AppearanceForTraitCollection, como, por ejemplo, una que solo especifique una clase de tamaño horizontal compacto, de modo que pueda personalizar cualquier vista de la aplicación que sea compacta horizontalmente.

UIImage

Otra clase a la que Apple ha agregado la colección de rasgos es UIImage. Antes, el desarrollador tenía que especificar una versión @1X y otra versión @2X de cualquier recurso gráfico en mapa de bits que fuera a incluir en la aplicación (como un icono).

iOS 8 se ha ampliado para permitir que el desarrollador incluya varias versiones de una imagen en un catálogo de imágenes basadas en una colección de rasgos. Por ejemplo, el desarrollador puede incluir una imagen más pequeña para trabajar con una clase de rasgo compacto y una imagen de tamaño completo para cualquier otra colección.

Cuando se usa una de las imágenes en una clase UIImageView, la vista de imagen mostrará automáticamente la versión correcta de la imagen para su colección de rasgos. Si el entorno de rasgos cambia (por ejemplo, si el usuario cambia el dispositivo de vertical a horizontal), la vista de imagen seleccionará automáticamente el nuevo tamaño de imagen para que coincida con la nueva colección de rasgos y cambiará su tamaño para que coincida con el de la versión actual de la imagen que se muestra.

UIImageAsset

Apple ha agregado una nueva clase a iOS 8 llamada UIImageAsset para dar al desarrollador aún más control sobre la selección de imágenes.

Un recurso de imagen encapsula todas las distintas versiones de una imagen y permite al desarrollador solicitar una imagen específica que coincida con una colección de rasgos que se haya pasado. Las imágenes se pueden agregar o quitar de un recurso de imagen, sobre la marcha.

Para obtener más información sobre los recursos de imagen, consulte la documentación de UIImageAsset de Apple.

Combinación de colecciones de rasgos

Otra de las funciones que los desarrolladores pueden usar con las colecciones de rasgos es agregar dos colecciones que resulten en una colección combinada, en la que los valores no especificados de una colección se sustituyan por los valores especificados de la segunda. Esto se hace mediante el método FromTraitsFromCollections de la clase UITraitCollection.

Como se indicó anteriormente, si alguno de los rasgos no está especificado en una de las colecciones de rasgos, pero sí está especificado en la otra, el valor se establecerá en la versión especificada. Sin embargo, si se especifican varias versiones de un determinado valor, se usará el valor de la última colección de rasgos.

Controladores de vista adaptables

En esta sección se detalla cómo la vista y los controladores de vista de iOS han adoptado los conceptos de rasgos y clases de tamaño para poder adaptarse mejor automáticamente a las aplicaciones de los desarrolladores.

Controlador de vista dividida

Una de las clases de controlador de vista que más ha cambiado en iOS 8 es la clase UISplitViewController. En el pasado, los desarrolladores usaban a menudo un controlador de vista dividida en la versión para iPad de la aplicación y, luego, tenían que proporcionar una versión completamente diferente de la jerarquía de la vista para la versión para iPhone de la aplicación.

En iOS 8, la clase UISplitViewController está disponible en ambas plataformas (iPad y iPhone), lo que permite al desarrollador crear una jerarquía de controladores de vista que funcionará tanto para iPhone como para iPad.

Cuando un iPhone está en modo horizontal, el controlador de vista dividida presentará las vistas una al lado de la otra, del mismo modo que lo haría al mostrarlas en un iPad.

Invalidación de rasgos

Los entornos de rasgos se extienden en cascada desde el contenedor primario hasta los contenedores secundarios, como en el siguiente gráfico, donde se muestra un controlador de vista dividida en un iPad en la orientación horizontal:

A Split View Controller on an iPad in the landscape orientation

Puesto que el iPad tiene una clase de tamaño regular en las orientaciones horizontales y verticales, la vista dividida mostrará tanto la vista maestra como la de detalles.

En un iPhone, donde la clase de tamaño es compacta en ambas orientaciones, el controlador de vista dividida solo muestra la vista de detalles, como se muestra a continuación:

The Split View Controller only displays the detail view

Si el desarrollador quiere mostrar en una aplicación tanto la vista maestra como la de detalles en un iPhone en la orientación horizontal, debe insertar un contenedor primario para el controlador de vista dividida e invalidar la colección de rasgos. Como se muestra en el gráfico siguiente:

The developer must insert a parent container for the Split View Controller and override the Trait Collection

Se establece UIView como elemento primario del controlador de vista dividida y se llama al método SetOverrideTraitCollection en la vista pasando una nueva colección de rasgos con el controlador de vista dividida como destino. La nueva colección de rasgos invalida HorizontalSizeClass, estableciéndola en Regular, por lo que el controlador de vista dividida mostrará tanto la vista maestra como la vista detallada en un iPhone con orientación horizontal.

Observe que VerticalSizeClass se estableció en unspecified, lo que permite agregar la nueva colección de rasgos a la colección de rasgos en el elemento primario, lo que da como resultado una Compact VerticalSizeClass compacta para el controlador de vista dividida secundario.

Cambios de los rasgos

En esta sección veremos en detalle cómo se produce la transición de las colecciones de rasgos cuando cambia el entorno de rasgos. Por ejemplo, cuando se gira el dispositivo de vertical a horizontal.

The portrait to landscape Trait Changes overview

En primer lugar, iOS 8 realiza algunos ajustes para preparar la transición. A continuación, el sistema anima el estado de transición. Por último, iOS 8 limpia los estados temporales necesarios para la transición.

iOS 8 proporciona varias llamadas de retorno que el desarrollador puede usar para tomar parte en el cambio de rasgos, como se muestra en la siguiente tabla:

Fase Devolución de llamada Descripción
Configuración
  • WillTransitionToTraitCollection
  • TraitCollectionDidChange
  • Se llama a este método al principio de un cambio de rasgos, antes de que la colección de rasgos se establezca en su nuevo valor.
  • Se llama al método cuando el valor de la colección de rasgos ha cambiado, pero antes de que se produzca cualquier animación.
Animación WillTransitionToTraitCollection El coordinador de transición que se pasa a este método tiene una propiedad AnimateAlongside que permite al desarrollador agregar animaciones que se ejecutarán junto con las animaciones predeterminadas.
Limpieza WillTransitionToTraitCollection Proporciona un método para que los desarrolladores incluyan su propio código de limpieza una vez que se realice la transición.

El método WillTransitionToTraitCollection es excelente para animar controladores de vista junto con los cambios de la colección de rasgos. El método WillTransitionToTraitCollection solo está disponible en los controladores de vista (UIViewController) y no en otros entornos de rasgos, como UIViews.

TraitCollectionDidChange es excelente para trabajar con la clase UIView cuando el desarrollador quiere que la interfaz de usuario se actualice a medida que cambien los rasgos.

Contracción de los controladores de vista dividida

Veamos ahora más detalladamente lo que sucede cuando un controlador de vista dividida se contrae de una vista de dos columnas a una de una columna. Para este cambio, deben producirse dos procesos:

  • De forma predeterminada, el controlador de vista dividida usará el controlador de vista principal como vista una vez que se produzca la contracción. Para invalidar este comportamiento, el desarrollador puede invalidar el método GetPrimaryViewControllerForCollapsingSplitViewController de UISplitViewControllerDelegate y proporcionar cualquier controlador de vista que desee que se muestre en el estado contraído.
  • El controlador de vista secundario tiene que combinarse en el controlador de vista principal. Por lo general, el desarrollador no tendrá que realizar ninguna acción para este paso; el controlador de vista dividida incluye el control automático de esta fase en función del dispositivo de hardware. Sin embargo, puede haber algunos casos especiales en los que el desarrollador quiera interactuar con este cambio. Llamar al método CollapseSecondViewController de UISplitViewControllerDelegate permite mostrar el controlador de vista maestro en lugar de la vista de detalles cuando se produce la contracción.

Expansión de los controladores de vista dividida

Veamos ahora más detalladamente lo que sucede cuando se expande un controlador de vista dividida a partir de un estado contraído. Una vez más, se deben dar dos fases:

  • En primer lugar, hay que definir el nuevo controlador de vista principal. De forma predeterminada, el controlador de vista dividida usará automáticamente el controlador de vista principal de la vista contraída. De nuevo, el desarrollador puede invalidar este comportamiento mediante el método GetPrimaryViewControllerForExpandingSplitViewController de UISplitViewControllerDelegate.
  • Una vez elegido el controlador de vista principal, se debe volver a crear el controlador de vista secundario. De nuevo, el controlador de vista dividida incluye el control automático de esta fase en función del dispositivo de hardware. El desarrollador puede invalidar este comportamiento llamando al método SeparateSecondaryViewController de UISplitViewControllerDelegate.

En un controlador de vista dividida, el controlador de vista principal interviene tanto en la expansión como en la contracción de las vistas mediante la implementación de los métodos CollapseSecondViewController y SeparateSecondaryViewController de UISplitViewControllerDelegate. UINavigationController implementa estos métodos para insertar y abrir automáticamente el controlador de vista secundaria.

Visualización de controladores de vista

Otro cambio que Apple realizó en iOS 8 es la manera en que el desarrollador muestra los controladores de vista. En el pasado, si la aplicación tenía un controlador de vista hoja (como un controlador de vista de tabla) y el desarrollador mostraba uno diferente (por ejemplo, en respuesta al usuario pulsando en una celda), la aplicación retrocedía a través de la jerarquía de controladores hasta el controlador de vista de navegación y llamaba al método PushViewController en ella para mostrar la nueva vista.

Esto suponía un acoplamiento muy estrecho entre el controlador de navegación y el entorno en el que se ejecutaba. En iOS 8, Apple lo desacopló proporcionando dos nuevos métodos:

  • ShowViewController: se adapta para mostrar el nuevo controlador de vista en función de su entorno. Por ejemplo, en un UINavigationController simplemente inserta la nueva vista en la pila. En un controlador de vista dividida, el nuevo controlador de vista se presentará en el lado izquierdo como el nuevo controlador de vista principal. Si no hay ningún controlador de vista de contenedor presente, la nueva vista se mostrará como controlador de vista modal.
  • ShowDetailViewController: funciona de forma similar a ShowViewController, pero se implementa en un controlador de vista dividida para reemplazar la vista de detalles por el nuevo controlador de vista que se pasa. Si el controlador de vista dividida está contraído (como se puede ver en una aplicación para iPhone), la llamada se redirigirá al método ShowViewController y la nueva vista se mostrará como el controlador de vista principal. Si no hay ningún controlador de vista de contenedor presente, la nueva vista se mostrará como un controlador de vista modal.

Estos métodos comienzan por el controlador de vista hoja y recorren la jerarquía de vistas hasta encontrar el controlador de vista de contenedor adecuado para controlar la visualización de la nueva vista.

Los desarrolladores pueden implementar ShowViewController y ShowDetailViewController en sus propios controladores de vista personalizados para obtener la misma funcionalidad automatizada que proporcionan UINavigationController y UISplitViewController.

Funcionamiento

En esta sección veremos cómo se implementan realmente estos métodos en iOS 8. En primer lugar, veamos el nuevo método GetTargetForAction:

The new GetTargetForAction method

Este método recorre la cadena de jerarquía hasta que se encuentra el controlador de vista de contenedor correcto. Por ejemplo:

  1. Si se llama a un método ShowViewController, el primer controlador de vista de la cadena que implementa este método es el controlador de navegación, por lo que se usa como elemento primario de la nueva vista.
  2. Si, en cambio, se llama a un método ShowDetailViewController, el controlador de vista dividida es el primer controlador de vista que lo implementa, por lo que se usa como elemento primario.

El método GetTargetForAction localiza un controlador de vista que implementa una acción determinada y le pregunta si quiere recibir esa acción. Dado que este método es público, los desarrolladores pueden crear sus propios métodos personalizados con el mismo funcionamiento que los métodos integrados ShowViewController y ShowDetailViewController.

Presentación adaptable

En iOS 8, Apple también hizo que las presentaciones emergentes (UIPopoverPresentationController) fueran adaptables. Por lo tanto, un controlador de vista de presentación emergente presentará automáticamente una vista emergente normal en una clase de tamaño normal, pero lo mostrará en pantalla completa en una clase de tamaño horizontalmente compacto (por ejemplo, en un iPhone).

Para dar cabida a los cambios en el sistema del guion gráfico unificado, se creó un nuevo objeto de controlador para administrar los controladores de vista presentados: UIPresentationController. Este controlador se crea desde el momento en que se presenta el controlador de vista hasta que se descarta. Como es una clase de administración, se puede considerar una superclase sobre el controlador de vista, ya que responde a los cambios del dispositivo que afectan al controlador de vista (como la orientación), que luego se devuelven al controlador de vista que el controlador de presentación controla.

Cuando el desarrollador presenta un controlador de vista mediante el método PresentViewController, se cede la administración del proceso de presentación a UIKit. UIKit controla (entre otras cosas) el controlador correcto para el estilo que se va a crear, excepto en el caso de que un controlador de vista tenga el estilo establecido en UIModalPresentationCustom. Aquí, la aplicación puede proporcionar su propio controlador de presentación en lugar de usar el controlador UIKit.

Estilos de presentación personalizados

Con un estilo de presentación personalizado, los desarrolladores tienen la opción de usar un controlador de presentación personalizado. Este controlador personalizado se puede usar para modificar la apariencia y el comportamiento de la vista a la que esté asociado.

Trabajo con clases de tamaño

El proyecto de Adaptive Photos de Xamarin que se incluye con este artículo ofrece un ejemplo de trabajo del uso de clases de tamaño y controladores de vistas adaptables en una aplicación de interfaz unificada de iOS 8.

Aunque la aplicación crea la interfaz de usuario en su totalidad a partir del código, en lugar de crear un guión gráfico unificado mediante Interface Builder de Xcode, se aplican las mismas técnicas.

Veamos ahora más detalladamente cómo el proyecto de Adaptive Photos implementa varias de las características de clase de tamaño de iOS 8 para crear una aplicación adaptable.

Adaptación a los cambios del entorno de rasgos

Al ejecutar la aplicación Adaptive Photos en un iPhone, cuando el usuario gire el dispositivo de vertical a horizontal el controlador de vista dividida mostrará tanto la vista maestra como la vista de detalles:

The Split View Controller will display both the master and details view as seen here

Esto se logra invalidando el método UpdateConstraintsForTraitCollection del controlador de vista y ajustando las restricciones en función del valor de VerticalSizeClass. Por ejemplo:

public void UpdateConstraintsForTraitCollection (UITraitCollection collection)
{
    var views = NSDictionary.FromObjectsAndKeys (
        new object[] { TopLayoutGuide, ImageView, NameLabel, ConversationsLabel, PhotosLabel },
        new object[] { "topLayoutGuide", "imageView", "nameLabel", "conversationsLabel", "photosLabel" }
    );

    var newConstraints = new List<NSLayoutConstraint> ();
    if (collection.VerticalSizeClass == UIUserInterfaceSizeClass.Compact) {
        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("|[imageView]-[nameLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("[imageView]-[conversationsLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("[imageView]-[photosLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("V:|[topLayoutGuide]-[nameLabel]-[conversationsLabel]-[photosLabel]",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("V:|[topLayoutGuide][imageView]|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.Add (NSLayoutConstraint.Create (ImageView, NSLayoutAttribute.Width, NSLayoutRelation.Equal,
            View, NSLayoutAttribute.Width, 0.5f, 0.0f));
    } else {
        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("|[imageView]|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("|-[nameLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("|-[conversationsLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("|-[photosLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("V:[topLayoutGuide]-[nameLabel]-[conversationsLabel]-[photosLabel]-20-[imageView]|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));
    }

    if (constraints != null)
        View.RemoveConstraints (constraints.ToArray ());

    constraints = newConstraints;
    View.AddConstraints (constraints.ToArray ());
}

Adición de animaciones de transición

Cuando el controlador de vista dividida de la aplicación Adaptive Photos pasa de contraído a expandido, se agregan animaciones a las animaciones predeterminadas invalidando el método WillTransitionToTraitCollection del controlador de vista. Por ejemplo:

public override void WillTransitionToTraitCollection (UITraitCollection traitCollection, IUIViewControllerTransitionCoordinator coordinator)
{
    base.WillTransitionToTraitCollection (traitCollection, coordinator);
    coordinator.AnimateAlongsideTransition ((UIViewControllerTransitionCoordinatorContext) => {
        UpdateConstraintsForTraitCollection (traitCollection);
        View.SetNeedsLayout ();
    }, (UIViewControllerTransitionCoordinatorContext) => {
    });
}

Invalidación del entorno de rasgos

Como se muestra anteriormente, la aplicación Adaptive Photos obliga al controlador de vista dividida a mostrar tanto la vista maestra como la de detalles cuando el dispositivo iPhone está en la vista horizontal.

Esto se hace mediante el siguiente código en el controlador de vista:

private UITraitCollection forcedTraitCollection = new UITraitCollection ();
...

public UITraitCollection ForcedTraitCollection {
    get {
        return forcedTraitCollection;
    }

    set {
        if (value != forcedTraitCollection) {
            forcedTraitCollection = value;
            UpdateForcedTraitCollection ();
        }
    }
}
...

public override void ViewWillTransitionToSize (SizeF toSize, IUIViewControllerTransitionCoordinator coordinator)
{
    ForcedTraitCollection = toSize.Width > 320.0f ?
         UITraitCollection.FromHorizontalSizeClass (UIUserInterfaceSizeClass.Regular) :
         new UITraitCollection ();

    base.ViewWillTransitionToSize (toSize, coordinator);
}

public void UpdateForcedTraitCollection ()
{
    SetOverrideTraitCollection (forcedTraitCollection, viewController);
}

Expansión y contracción del controlador de vista dividida

A continuación, examinemos cómo se implementó en Xamarin el comportamiento de expansión y contracción del controlador de vista dividida. En AppDelegate, cuando se crea el controlador de vista dividida, se asigna su delegado para controlar estos cambios:

public class SplitViewControllerDelegate : UISplitViewControllerDelegate
{
    public override bool CollapseSecondViewController (UISplitViewController splitViewController,
        UIViewController secondaryViewController, UIViewController primaryViewController)
    {
        AAPLPhoto photo = ((CustomViewController)secondaryViewController).Aapl_containedPhoto (null);
        if (photo == null) {
            return true;
        }

        if (primaryViewController.GetType () == typeof(CustomNavigationController)) {
            var viewControllers = new List<UIViewController> ();
            foreach (var controller in ((UINavigationController)primaryViewController).ViewControllers) {
                var type = controller.GetType ();
                MethodInfo method = type.GetMethod ("Aapl_containsPhoto");

                if ((bool)method.Invoke (controller, new object[] { null })) {
                    viewControllers.Add (controller);
                }
            }

            ((UINavigationController)primaryViewController).ViewControllers = viewControllers.ToArray<UIViewController> ();
        }

        return false;
    }

    public override UIViewController SeparateSecondaryViewController (UISplitViewController splitViewController,
        UIViewController primaryViewController)
    {
        if (primaryViewController.GetType () == typeof(CustomNavigationController)) {
            foreach (var controller in ((CustomNavigationController)primaryViewController).ViewControllers) {
                var type = controller.GetType ();
                MethodInfo method = type.GetMethod ("Aapl_containedPhoto");

                if (method.Invoke (controller, new object[] { null }) != null) {
                    return null;
                }
            }
        }

        return new AAPLEmptyViewController ();
    }
}

El método SeparateSecondaryViewController comprueba si se muestra una foto y realiza acciones en función de ese estado. Si no se muestra ninguna foto, contrae el controlador de vista secundario para que se muestre el controlador de vista maestro.

El método CollapseSecondViewController se usa al expandir el controlador de vista dividida para ver si existen fotos en la pila. Si es así, se vuelve a contraer a esa vista.

Movimiento entre controladores de vista

A continuación, veamos cómo se mueve la aplicación Adaptive Photos entre los controladores de vista. En la clase AAPLConversationViewController, cuando el usuario selecciona una celda de la tabla, se llama al método ShowDetailViewController para mostrar la vista de detalles:

public override void RowSelected (UITableView tableView, NSIndexPath indexPath)
{
    var photo = PhotoForIndexPath (indexPath);
    var controller = new AAPLPhotoViewController ();
    controller.Photo = photo;

    int photoNumber = indexPath.Row + 1;
    int photoCount = (int)Conversation.Photos.Count;
    controller.Title = string.Format ("{0} of {1}", photoNumber, photoCount);
    ShowDetailViewController (controller, this);
}

Visualización de indicadores de divulgación

En la aplicación Adaptive Photos hay varios lugares en los que los indicadores de divulgación se ocultan o se muestran en función de los cambios en el entorno de rasgos. Esto se controla con el código siguiente:

public bool Aapl_willShowingViewControllerPushWithSender ()
{
    var selector = new Selector ("Aapl_willShowingViewControllerPushWithSender");
    var target = this.GetTargetViewControllerForAction (selector, this);

    if (target != null) {
        var type = target.GetType ();
        MethodInfo method = type.GetMethod ("Aapl_willShowingDetailViewControllerPushWithSender");
        return (bool)method.Invoke (target, new object[] { });
    } else {
        return false;
    }
}

public bool Aapl_willShowingDetailViewControllerPushWithSender ()
{
    var selector = new Selector ("Aapl_willShowingDetailViewControllerPushWithSender");
    var target = this.GetTargetViewControllerForAction (selector, this);

    if (target != null) {
        var type = target.GetType ();
        MethodInfo method = type.GetMethod ("Aapl_willShowingDetailViewControllerPushWithSender");
        return (bool)method.Invoke (target, new object[] { });
    } else {
        return false;
    }
}

Se implementan mediante el método GetTargetViewControllerForAction descrito en detalle anteriormente.

Cuando un controlador de vista de tabla muestra datos, usa los métodos implementados anteriormente para ver si se va a producir o no una inserción y si se va a mostrar u ocultar el indicador de divulgación en consecuencia:

public override void WillDisplay (UITableView tableView, UITableViewCell cell, NSIndexPath indexPath)
{
    bool pushes = ShouldShowConversationViewForIndexPath (indexPath) ?
         Aapl_willShowingViewControllerPushWithSender () :
         Aapl_willShowingDetailViewControllerPushWithSender ();

    cell.Accessory = pushes ? UITableViewCellAccessory.DisclosureIndicator : UITableViewCellAccessory.None;
    var conversation = ConversationForIndexPath (indexPath);
    cell.TextLabel.Text = conversation.Name;
}

Nuevo tipo ShowDetailTargetDidChangeNotification

Apple agregó un nuevo tipo de notificación para trabajar con clases de tamaño y entornos de rasgos desde un controlador de vista dividida: ShowDetailTargetDidChangeNotification. Este tipo de notificación se envía cada vez que cambia la vista de detalles de destino de un controlador de vista dividida, como, por ejemplo, cuando el controlador se expande o se contrae.

La aplicación Adaptive Photos usa este tipo de notificación para actualizar el estado del indicador de divulgación cuando cambia el controlador de vista de detalles:

public override void ViewDidLoad ()
{
    base.ViewDidLoad ();
    TableView.RegisterClassForCellReuse (typeof(UITableViewCell), AAPLListTableViewControllerCellIdentifier);
    NSNotificationCenter.DefaultCenter.AddObserver (this, new Selector ("showDetailTargetDidChange:"),
        UIViewController.ShowDetailTargetDidChangeNotification, null);
    ClearsSelectionOnViewWillAppear = false;
}

Examine más detenidamente la aplicación Adaptive Photos para ver todas las formas en que se pueden usar las clases de tamaño, las colecciones de rasgos y los controladores de vistas adaptables para crear fácilmente una aplicación unificada en Xamarin.iOS.

Guiones gráficos unificados

Los guiones gráficos unificados, una novedad de iOS 8, permiten al desarrollador crear un archivo de guión gráfico unificado que se pueda mostrar tanto en dispositivos iPhone como iPad estableciendo como destino varias clases de tamaño. Al usar guiones gráficos unificados, el desarrollador no tiene que escribir tanto código específico de interfaz de usuario y solo tiene que crear y mantener un diseño de interfaz.

Las principales ventajas de los guiones gráficos unificados son las siguientes:

  • Uso del mismo archivo de guión gráfico para dispositivos iPhone y iPad.
  • Implementación de versiones anteriores en iOS 6 e iOS 7.
  • Vista previa del diseño para diferentes dispositivos, orientaciones y versiones del sistema operativo.

Habilitación de clases de tamaño

De forma predeterminada, cualquier nuevo proyecto de Xamarin.iOS usará clases de tamaño. Para usar clases de tamaño y transiciones adaptables en un guión gráfico de un proyecto anterior, primero hay que convertirlo al formato de guión gráfico unificado de Xcode 6 y seleccionar la casilla Usar clases de tamaño en el inspector de archivos de Xcode de los guiones gráficos.

Pantallas de inicio dinámicas

Cuando se inicia una aplicación iOS, el archivo de la pantalla de inicio se muestra como una pantalla de presentación para informar al usuario de que la aplicación se está iniciando. Antes de iOS 8, el desarrollador tenía que incluir varios recursos de imagen Default.png para cada tipo de dispositivo, orientación y resolución de pantalla en los que se ejecutaría la aplicación. Por ejemplo, Default@2x.png, Default-Landscape@2x~ipad.png, Default-Portrait@2x~ipad.png, etc.

Teniendo en cuenta los nuevos dispositivos iPhone 6 y iPhone 6 Plus (y el próximo Apple Watch), además de todos los dispositivos iPhone y iPad existentes, esto supone la creación y el mantenimiento de una gran cantidad de recursos de imagen de pantalla de inicio Default.png de distintos tamaños, orientaciones y resoluciones. Además, estos archivos pueden ser bastante grandes y sobredimensionarán el paquete de aplicación de entrega, por lo que aumentará la cantidad de tiempo necesario para descargar la aplicación desde iTunes App Store (lo que posiblemente impida que se pueda entregar a través de una red de telefonía móvil) y también aumentará la cantidad de almacenamiento necesario en el dispositivo del usuario final.

Como novedad en iOS 8, el desarrollador puede crear un único archivo atómico .xib en Xcode que use clases de diseño y tamaño automáticos para crear una pantalla de inicio dinámica que funcione para cada dispositivo, resolución y orientación. Esto no solo disminuye la cantidad de trabajo que debe realizar el desarrollador para crear y mantener todos los recursos de imagen necesarios, sino que también reduce notablemente el tamaño del paquete instalado de la aplicación.

Las pantallas de inicio dinámicas presentan las siguientes limitaciones y aspectos que se deben tener en cuenta:

  • Use solo clases UIKit.
  • Use una sola vista raíz que sea un objeto UIView o UIViewController.
  • No realice ninguna conexión con el código de la aplicación (no agregue acciones ni salidas).
  • No agregue objetos UIWebView.
  • No use ninguna clase personalizada.
  • No use atributos en tiempo de ejecución.

Teniendo en cuenta las instrucciones anteriores, veamos cómo agregar una pantalla de inicio dinámica a un proyecto de Xamarin iOS 8 existente.

Haga lo siguiente:

  1. Abra Visual Studio para Mac y cargue la solución a la que se va a agregar la pantalla de inicio dinámica.

  2. En el Explorador de soluciones, haga clic con el botón derecho en el archivo MainStoryboard.storyboard y seleccione Abrir con>Interface Builder de Xcode:

    Open With Xcode Interface Builder

  3. En Xcode, seleccione Archivo>Nuevo>Archivo...:

    Select File / New

  4. Seleccione iOS>Interfaz de usuario>Pantalla de inicio y haga clic en el botón Siguiente:

    Select iOS / User Interface / Launch Screen

  5. Asigne un nombre al archivo LaunchScreen.xib y haga clic en el botón Crear:

    Name the file LaunchScreen.xib

  6. Edite el diseño de la pantalla de inicio agregando elementos gráficos y usando restricciones de diseño para colocarlos según los dispositivos, las orientaciones y los tamaños de pantalla dados:

    Editing the design of the launch screen

  7. Guarde los cambios en LaunchScreen.xib.

  8. Seleccione el destino de aplicaciones y la pestaña General:

    Select the Applications Target and the General tab

  9. Haga clic en el botón de Elegir info.plist, seleccione Info.plist para la aplicación de Xamarin y haga clic en el botón Elegir:

    Select the Info.plist for the Xamarin app

  10. En la sección Iconos de aplicaciones e imágenes de inicio, abra la lista desplegable Archivo de pantalla de inicio y elija el archivo LaunchScreen.xib creado anteriormente:

    Choose the LaunchScreen.xib

  11. Guarde los cambios en el archivo y vuelva a Visual Studio para Mac.

  12. Espere a que Visual Studio para Mac termine de sincronizar los cambios con Xcode.

  13. En el Explorador de soluciones, haga clic con el botón derecho en la carpeta Recurso y seleccione Agregar>Agregar archivos...:

    Select Add / Add Files...

  14. Seleccione el archivo LaunchScreen.xib creado anteriormente y haga clic en el botón Abrir:

    Select the LaunchScreen.xib file

  15. Compile la aplicación.

Prueba de la pantalla de inicio dinámica

En Visual Studio para Mac, seleccione el simulador de Retina de iPhone 4 y ejecute la aplicación. La pantalla de inicio dinámica se mostrará en el formato y la orientación correctos:

The Dynamic Launch Screen displayed in the vertical orientation

Detenga la aplicación en Visual Studio para Mac y seleccione un dispositivo iPad iOS 8. Ejecute la aplicación; la pantalla de inicio adoptará el formato correcto para este dispositivo y esta orientación:

The Dynamic Launch Screen displayed in the horizontal orientation

Vuelva a Visual Studio para Mac y detenga la ejecución de la aplicación.

Trabajo con iOS 7

Para mantener la compatibilidad con iOS 7, basta con incluir los recursos de imagen Default.png habituales de forma normal en la aplicación para iOS 8. iOS volverá al comportamiento anterior y usará esos archivos como pantalla de inicio cuando se ejecute en un dispositivo iOS 7.

Para ver una implementación de una pantalla de inicio dinámica en Xamarin, consulte la aplicación de ejemplo de pantallas de inicio dinámicas para iOS 8 adjunta a este documento.

Resumen

En este artículo se tratan brevemente las clases de tamaño y la manera en que afectan al diseño en los dispositivos iPhone y iPad. Se explica cómo funcionan los rasgos, los entornos de rasgos y las colecciones de rasgos con las clases de tamaño para crear interfaces unificadas. Se examinan brevemente los controladores de vista adaptables y su funcionamiento con las clases de tamaño en las interfaces unificadas. Se examina la implementación de clases de tamaño e interfaces unificadas íntegramente a partir de código C# en una aplicación Xamarin iOS 8.

Por último, en este artículo se tratan los conceptos básicos para crear una sola pantalla de inicio dinámica que se mostrará como pantalla de inicio en cada dispositivo iOS 8.