Trabajo con tipos nativos en aplicaciones multiplataforma

En este artículo se describe el uso de los nuevos tipos nativos de Unified API para iOS (nint, nuint y nfloat) en una aplicación multiplataforma en la que el código se comparte con dispositivos que no son iOS, como sistemas operativos Android o Windows Phone.

Los tipos nativos de 64 bits funcionan con las API de iOS y Mac. Si está escribiendo código compartido que también se ejecuta en Android o Windows, deberá administrar la conversión de los tipos de Unified en tipos de .NET normales que pueda compartir.

En este documento se describen diferentes formas de interoperar con Unified API a partir del código compartido o común.

Cuándo usar los tipos nativos

Las Unified API de Xamarin.iOS y Xamarin.Mac todavía incluyen los tipos de datos int, uint y float, así como los tipos RectangleF, SizeF y PointF. Estos tipos de datos existentes deben seguir usándose en cualquier código compartido y multiplataforma. Los nuevos tipos de datos nativos solo deben usarse al realizar una llamada a una API de Mac o iOS en la que se requiera compatibilidad con tipos dependientes de la arquitectura.

En función de la naturaleza del código que se comparte, puede haber ocasiones en las que el código multiplataforma tenga que tratar con los tipos de datos nint, nuint y nfloat. Por ejemplo: una biblioteca que controla las transformaciones en datos rectangulares y que anteriormente usaba System.Drawing.RectangleF para compartir la funcionalidad entre las versiones de Xamarin.iOS y Xamarin.Android de una aplicación, tendría que actualizarse para controlar tipos nativos en iOS.

La forma en que se controlan estos cambios depende del tamaño y la complejidad de la aplicación, así como de la forma de uso compartido de código que se ha usado, como veremos en las secciones siguientes.

Consideraciones sobre el uso compartido de código

Como se indica en el documento Opciones de uso compartido de código , hay dos formas principales de compartir código entre proyectos multiplataforma: proyectos compartidos y bibliotecas de clases portables. El uso de uno de estos dos tipos limitará las opciones que tenemos al controlar los tipos de datos nativos en código multiplataforma.

Proyectos de biblioteca de clases portable

Una biblioteca de clases portable (PCL) le permite tener como destino las plataformas que quiere admitir y usar interfaces para proporcionar funcionalidad específica de la plataforma.

Dado que el tipo de proyecto de PCL se compila en un archivo .DLL independiente de Unified API, se verá obligado a seguir usando los tipos de datos existentes (int, uint y float) en el código fuente de PCL y convertir los tipos de las llamadas a las clases y métodos de PCL en las aplicaciones de front-end. Por ejemplo:

using NativePCL;
...

CGRect rect = new CGRect (0, 0, 200, 200);
Console.WriteLine ("Rectangle Area: {0}", Transformations.CalculateArea ((RectangleF)rect));

Proyectos compartidos

El tipo de proyecto de recurso compartido permite organizar el código fuente en un proyecto independiente que, después, se incluye y compila en las aplicaciones de front-end individuales específicas de la plataforma. También permite usar directivas #if del compilador según sea necesario para administrar los requisitos específicos de la plataforma.

El tamaño y la complejidad de las aplicaciones móviles de front-end que consumen código compartido, junto con el tamaño y la complejidad del código que se comparte, deben tenerse en cuenta al elegir el método de compatibilidad para los tipos de datos nativos en un proyecto de recursos compartidos multiplataforma.

En función de estos factores, se pueden implementar los siguientes tipos de soluciones mediante las directivas if __UNIFIED__ ... #endif del compilador para controlar los cambios específicos de Unified API en el código.

Uso de métodos duplicados

Tome el ejemplo de una biblioteca que realiza transformaciones en los datos rectangulares indicados anteriormente. Si la biblioteca solo contiene uno o dos métodos muy sencillos, puede optar por crear versiones duplicadas de esos métodos para Xamarin.iOS y Xamarin.Android. Por ejemplo:

using System;
using System.Drawing;

#if __UNIFIED__
using CoreGraphics;
#endif

namespace NativeShared
{
    public class Transformations
    {
        #region Constructors
        public Transformations ()
        {
        }
        #endregion

        #region Public Methods
        #if __UNIFIED__
            public static nfloat CalculateArea(CGRect rect) {

                // Calculate area...
                return (rect.Width * rect.Height);

            }
        #else
            public static float CalculateArea(RectangleF rect) {

                // Calculate area...
                return (rect.Width * rect.Height);

            }
        #endif
        #endregion
    }
}

En el código anterior, dado que la rutina CalculateArea es muy sencilla, hemos usado la compilación condicional y hemos creado una versión independiente de la Unified API del método. Por otro lado, en caso de que la biblioteca incluyera muchas rutinas o varias rutinas complejas, esta solución no sería factible, ya que presentaría un problema al mantener sincronizados todos los métodos para modificaciones o correcciones de errores.

Uso de sobrecargas de métodos

En ese caso, la solución podría ser crear una versión de sobrecarga de los métodos usando tipos de datos de 32 bits para que ahora tomen CGRect como un parámetro o un valor devuelto, convertir ese valor en RectangleF (sabiendo que la conversión de nfloat a float es una conversión con pérdidas) y llamar a la versión original de la rutina para realizar el trabajo real. Por ejemplo:

using System;
using System.Drawing;

#if __UNIFIED__
using CoreGraphics;
#endif

namespace NativeShared
{
    public class Transformations
    {
        #region Constructors
        public Transformations ()
        {
        }
        #endregion

        #region Public Methods
        #if __UNIFIED__
            public static nfloat CalculateArea(CGRect rect) {

                // Call original routine to calculate area
                return (nfloat)CalculateArea((RectangleF)rect);

            }
        #endif

        public static float CalculateArea(RectangleF rect) {

            // Calculate area...
            return (rect.Width * rect.Height);

        }

        #endregion
    }
}

De nuevo, se trata de una buena solución siempre que la pérdida de precisión no afecte a los resultados para las necesidades específicas de la aplicación.

Uso de directivas de alias

En el caso de las áreas en las que la pérdida de precisión es un problema, otra posible solución consiste en usar directivas using para crear un alias para los tipos de datos nativos y de CoreGraphics incluyendo el código siguiente en la parte superior de los archivos de código fuente compartidos y convirtiendo los valores int, uint o float necesarios en nint, nuint o nfloat:

#if __UNIFIED__
    // Mappings Unified CoreGraphic classes to MonoTouch classes
    using RectangleF = global::CoreGraphics.CGRect;
    using SizeF = global::CoreGraphics.CGSize;
    using PointF = global::CoreGraphics.CGPoint;
#else
    // Mappings Unified types to MonoTouch types
    using nfloat = global::System.Single;
    using nint = global::System.Int32;
    using nuint = global::System.UInt32;
#endif

De este modo, el código de ejemplo se convierte en:

using System;
using System.Drawing;

#if __UNIFIED__
    // Map Unified CoreGraphic classes to MonoTouch classes
    using RectangleF = global::CoreGraphics.CGRect;
    using SizeF = global::CoreGraphics.CGSize;
    using PointF = global::CoreGraphics.CGPoint;
#else
    // Map Unified types to MonoTouch types
    using nfloat = global::System.Single;
    using nint = global::System.Int32;
    using nuint = global::System.UInt32;
#endif

namespace NativeShared
{

    public class Transformations
    {
        #region Constructors
        public Transformations ()
        {
        }
        #endregion

        #region Public Methods
        public static nfloat CalculateArea(RectangleF rect) {

            // Calculate area...
            return (rect.Width * rect.Height);

        }
        #endregion
    }
}

Tenga en cuenta que aquí hemos cambiado el método CalculateArea para que devuelva nfloat en lugar del estándar float. Esto se ha hecho para no obtener un error de compilación al intentar convertir implícitamente el resultado nfloat de nuestro cálculo (ya que ambos valores multiplicados son de tipo nfloat) en un valor devuelto float.

Si el código se compila y se ejecuta en un dispositivo que no usa Unified API, using nfloat = global::System.Single; asigna el objeto nfloat a un objeto Single que se convertirá implícitamente en float para permitir que la aplicación de front-end de consumo llame al método CalculateArea sin modificaciones.

Uso de conversiones de tipos en la aplicación de front-end

En caso de que las aplicaciones de front-end solo realicen unas pocas llamadas a la biblioteca de código compartido, otra solución podría ser dejar la biblioteca sin cambios y realizar la conversión de tipos en la aplicación Xamarin.iOS o Xamarin.Mac al llamar a la rutina existente. Por ejemplo:

using NativeShared;
...

CGRect rect = new CGRect (0, 0, 200, 200);
Console.WriteLine ("Rectangle Area: {0}", Transformations.CalculateArea ((RectangleF)rect));

Si la aplicación de consumo realiza cientos de llamadas a la biblioteca de código compartido, es posible que esto no sea una buena solución.

En función de la arquitectura de la aplicación, es posible que terminemos usando una o varias de las soluciones descritas anteriormente para admitir tipos de datos nativos (cuando sea necesario) en nuestro código multiplataforma.

Aplicaciones de Xamarin.Forms

Se requiere lo siguiente para usar Xamarin.Forms para las interfaces de usuario multiplataforma que también se compartirán con una aplicación que usa Unified API:

  • Toda la solución debe usar la versión 1.3.1 (o una versión posterior) del paquete NuGet de Xamarin.Forms.
  • Para las representaciones personalizadas de Xamarin.iOS, use los mismos tipos de soluciones presentados anteriormente en función de cómo se ha compartido el código de la interfaz de usuario (proyecto compartido o PCL).

Igual que en una aplicación multiplataforma estándar, los tipos de datos de 32 bits existentes deben usarse en cualquier código multiplataforma compartido para la mayoría de las situaciones. Los nuevos tipos de datos nativos solo deben usarse al realizar una llamada a una API de Mac o iOS en la que se requiera compatibilidad con tipos dependientes de la arquitectura.

Para obtener más información, consulte el artículo Actualización de aplicaciones existentes de Xamarin.Forms.

Resumen

En este artículo hemos visto cuándo usar los tipos de datos nativos en una aplicación de Unified API y sus implicaciones multiplataforma. Hemos presentado varias soluciones para abordar las situaciones en las que se deben usar los nuevos tipos de datos nativos en bibliotecas multiplataforma. Además, hemos visto una guía rápida para admitir Unified API en aplicaciones multiplataforma de Xamarin.Forms.