Enlace de bibliotecas de Objective-C

Cuando trabaja con Xamarin.iOS o Xamarin.Mac, es posible que encuentre casos en los que quiera usar una biblioteca de Objective-C de terceros. En esas situaciones, puede usar los proyectos de enlace de Xamarin para crear un enlace de C# a las bibliotecas nativas de Objective-C. El proyecto usa las mismas herramientas que usamos para llevar las API de iOS y Mac a C#.

En este documento se indica cómo enlazar las API de Objective-C. Si va a enlazar solo las API de C, debe usar el mecanismo estándar de .NET para esto, el marco P/Invoke. Los detalles sobre cómo vincular estáticamente una biblioteca de C están disponibles en la página Vincular bibliotecas nativas.

Consulte nuestra Guía de referencia de tipos de enlace complementarios. Además, si desea obtener más información sobre lo que sucede en segundo plano, consulte nuestra página de Información general sobre enlaces.

Los enlaces se pueden crear para las bibliotecas de iOS y Mac. En esta página se describe cómo trabajar en un enlace de iOS, pero los enlaces de Mac son muy parecidos.

Código de ejemplo de iOS

Puede usar el proyecto de ejemplo de enlace de iOS para experimentar con enlaces.

Introducción

La manera más fácil de crear un enlace es crear un proyecto de enlace de Xamarin.iOS. Puede hacerlo desde Visual Studio para Mac seleccionando el tipo de proyecto, iOS > Biblioteca > Biblioteca de enlaces:

Do this from Visual Studio for Mac by selecting the project type, iOS Library Bindings Library

El proyecto generado contiene una plantilla pequeña que se puede editar e incluye dos archivos: ApiDefinition.cs y StructsAndEnums.cs.

ApiDefinition.cs es donde definirá el contrato de API, que es el archivo que describe cómo se proyecta la API subyacente Objective-C en C#. La sintaxis y el contenido de este archivo son el tema principal de análisis de este documento y el contenido se limita a las interfaces de C# y las declaraciones de delegados de C#. El archivo StructsAndEnums.cs es el archivo donde escribirá las definiciones que requieren las interfaces y los delegados. Esto incluye los valores de enumeración y estructuras que el código podría usar.

Enlace de una API

Para realizar un enlace completo, querrá conocer la definición de API de Objective-C y familiarizarse con las directrices de diseño de .NET Framework.

Para enlazar la biblioteca, normalmente comenzará con un archivo de definición de API. Un archivo de definición de API es simplemente un archivo de código fuente de C# que contiene interfaces de C# que se han anotado con varios atributos que ayudan a controlar el enlace. Este archivo es el que define lo que es el contrato entre C# y Objective-C.

Por ejemplo, este es un archivo de API trivial para una biblioteca:

using Foundation;

namespace Cocos2D {
  [BaseType (typeof (NSObject))]
  interface Camera {
    [Static, Export ("getZEye")]
    nfloat ZEye { get; }

    [Export ("restore")]
    void Restore ();

    [Export ("locate")]
    void Locate ();

    [Export ("setEyeX:eyeY:eyeZ:")]
    void SetEyeXYZ (nfloat x, nfloat y, nfloat z);

    [Export ("setMode:")]
    void SetMode (CameraMode mode);
  }
}

En el ejemplo anterior se define una clase denominada Cocos2D.Camera que se deriva del tipo base NSObject (este tipo procede de Foundation.NSObject) y que define una propiedad estática (ZEye), dos métodos que no toman argumentos y un método que toma tres argumentos.

A continuación, en la sección Archivo de definición de API, encontrará un análisis detallado del formato del archivo de API y de los atributos que puede utilizar.

Para generar un enlace completo, normalmente tratará con cuatro componentes:

  • El archivo de definición de API (ApiDefinition.cs en la plantilla).
  • Opcional: todas las enumeraciones, tipos y estructuras necesarios para el archivo de definición de API (StructsAndEnums.cs en la plantilla).
  • Opcional: orígenes adicionales que pueden expandir el enlace generado o proporcionar una API más descriptiva de C# (cualquier archivo de C# que agregue al proyecto).
  • La biblioteca nativa que está enlazando.

Este gráfico muestra la relación entre los archivos:

This chart shows the relationship between the files

El archivo de definición de API solo contendrá espacios de nombres y definiciones de interfaz (con cualquier miembro que una interfaz pueda contener) y no debe contener clases, enumeraciones, delegados o estructuras. El archivo de definición de API es simplemente el contrato que se usará para generar la API.

Cualquier código adicional que necesite como, por ejemplo, enumeraciones o clases auxiliares, debe hospedarse en un archivo independiente. En el ejemplo anterior, "CameraMode" es un valor de enumeración que no existe en el archivo CS y debe hospedarse en un archivo independiente, por ejemplo StructsAndEnums.cs:

public enum CameraMode {
    FlyOver, Back, Follow
}

El archivo APIDefinition.cs se combina con la clase StructsAndEnum y se usa para generar el enlace principal de la biblioteca. Puede usar la biblioteca resultante tal cual, pero normalmente la ajustará para agregar algunas características de C# para beneficio de los usuarios. Algunos ejemplos incluyen la implementación de un método ToString(), proporcionar indexadores de C#, agregar conversiones implícitas a algunos tipos nativos y desde estos, o proporcionar versiones fuertemente tipadas de algunos métodos. Estas mejoras se almacenan en archivos adicionales de C#. Simplemente agregue los archivos de C# al proyecto y se incluirán en este proceso de creación.

Esto muestra cómo implementaría el código en el archivo Extra.cs. Tenga en cuenta que va a usar clases parciales, ya que estas aumentan las clases parciales que se generan a partir de la combinación del enlace principal entre ApiDefinition.cs y StructsAndEnums.cs:

public partial class Camera {
    // Provide a ToString method
    public override string ToString ()
    {
         return String.Format ("ZEye: {0}", ZEye);
    }
}

La creación de la biblioteca generará el enlace nativo.

Para completar este enlace, debe agregar la biblioteca nativa al proyecto. Para ello, agregue la biblioteca nativa al proyecto, ya sea arrastrando y colocando la biblioteca nativa de Finder sobre el proyecto en el Explorador de soluciones, o haciendo clic con el botón derecho en el proyecto y seleccionando Agregar>Agregar archivos para seleccionar la biblioteca nativa. Las bibliotecas nativas, por convención, comienzan con la palabra "lib" y terminan con la extensión ".a". Al hacerlo, Visual Studio para Mac agrega dos archivos: el archivo .a y un archivo de C# rellenado automáticamente que incluye información sobre lo que contiene la biblioteca nativa:

Native libraries by convention start with the word lib and end with the extension .a

El contenido del archivo libMagicChord.linkwith.cs tiene información sobre cómo se puede usar esta biblioteca e indica al IDE que empaquete este archivo binario en el archivo DLL resultante:

using System;
using ObjCRuntime;

[assembly: LinkWith ("libMagicChord.a", SmartLink = true, ForceLoad = true)]

Puede encontrar la información completa sobre el uso del atributo [LinkWith] en la Guía de referencia de tipos de enlace.

Ahora, al crear el proyecto, terminará con un archivo MagicChords.dll que contiene el enlace y la biblioteca nativa. Puede distribuir este proyecto o la DLL resultante a otros desarrolladores para su propio uso.

A veces, es posible que necesite algunos valores de enumeración, definiciones de delegado u otros tipos. No los coloque en el archivo de definiciones de API, ya que esto es simplemente un contrato

Archivo de definición de API

El archivo de definición de API consta de una serie de interfaces. Las interfaces de la definición de API se convertirán en una declaración de clase y se deben decorar con el atributo [BaseType] para especificar la clase base de la clase.

Es posible que se pregunte por qué no hemos usado clases en lugar de interfaces para la definición del contrato. Hemos seleccionado interfaces porque esto nos permitió escribir el contrato para un método sin tener que proporcionar un cuerpo del método en el archivo de definición de API, ni tener que proporcionar un cuerpo que tuviera que producir una excepción o devolver un valor significativo.

Pero dado que estamos usando la interfaz como esqueleto para generar una clase, tuvimos que recurrir al ajuste de varias partes del contrato con atributos para controlar el enlace.

Métodos de enlace

El enlace más simple que puede hacer es enlazar un método. Simplemente declare un método en la interfaz con las convenciones de nomenclatura de C# y decore el método con el [Export]Atributo . El atributo [Export] es lo que vincula el nombre de C# con el nombre Objective-C en el entorno de ejecución de Xamarin.iOS. El parámetro del atributo [Export] es el nombre del selector Objective-C. He aquí algunos ejemplos:

// A method, that takes no arguments
[Export ("refresh")]
void Refresh ();

// A method that takes two arguments and return the result
[Export ("add:and:")]
nint Add (nint a, nint b);

// A method that takes a string
[Export ("draw:atColumn:andRow:")]
void Draw (string text, nint column, nint row);

Los ejemplos anteriores muestran cómo puede enlazar métodos de instancia. Para enlazar métodos estáticos, debe usar el atributo [Static], de la siguiente manera:

// A static method, that takes no arguments
[Static, Export ("beep")]
void Beep ();

Esto es necesario porque el contrato forma parte de una interfaz y las interfaces no tienen ninguna noción de declaraciones estáticas frente a declaraciones de instancias, por lo que es necesario volver a recurrir a atributos. Si desea ocultar un método determinado del enlace, puede decorar el método con el atributo [Internal].

El comando btouch-native incluirá comprobaciones para que los parámetros de referencia no sean null. Si desea permitir valores null para un parámetro determinado, use el atributo [NullAllowed] en el parámetro, de la siguiente manera:

[Export ("setText:")]
string SetText ([NullAllowed] string text);

Al exportar un tipo de referencia, con la palabra clave [Export] también puede especificar la semántica de asignación. Esto es necesario para asegurarse de que no se filtren datos.

Propiedades de los enlaces

Al igual que los métodos, las propiedades Objective-C se enlazan mediante el atributo [Export] y se asignan directamente a las propiedades de C#. Al igual que los métodos, las propiedades se pueden decorar con los atributos [Static] y [Internal].

Cuando se usa el atributo [Export] en una propiedad en segundo plano, btouch-native realmente enlaza dos métodos: el captador y el establecedor. El nombre que se proporciona para exportar es el nombre base y el establecedor se calcula anteponiendo la palabra "set", poniendo la primera letra del nombre base en mayúsculas y haciendo que el selector tome un argumento. Esto significa que [Export ("label")] aplicado en una propiedad enlaza realmente los métodos "label" y "setLabel:" de Objective-C.

A veces, las propiedades de Objective-C no siguen el patrón descrito anteriormente y el nombre se sobrescribe manualmente. En esos casos, puede controlar la forma en que se genera el enlace mediante el atributo [Bind] en el método captador o establecedor, por ejemplo:

[Export ("menuVisible")]
bool MenuVisible { [Bind ("isMenuVisible")] get; set; }

A continuación, esto enlaza "isMenuVisible" y "setMenuVisible:". Opcionalmente, se puede enlazar una propiedad mediante la sintaxis siguiente:

[Category, BaseType(typeof(UIView))]
interface UIView_MyIn
{
  [Export ("name")]
  string Name();

  [Export("setName:")]
  void SetName(string name);
}

Donde el captador y el establecedor se definen explícitamente como en los enlaces name y setName anteriores.

Además de admitir propiedades estáticas mediante [Static], puede decorar propiedades estáticas para subprocesos con [IsThreadStatic], por ejemplo:

[Export ("currentRunLoop")][Static][IsThreadStatic]
NSRunLoop Current { get; }

Al igual que los métodos permiten marcar algunos parámetros con [NullAllowed], puede aplicar [NullAllowed] a una propiedad para indicar que null es un valor válido para la propiedad, por ejemplo:

[Export ("text"), NullAllowed]
string Text { get; set; }

El parámetro [NullAllowed] también se puede especificar directamente en el método establecedor:

[Export ("text")]
string Text { get; [NullAllowed] set; }

Advertencias sobre el enlace de controles personalizados

Se deben tener en cuenta las advertencias siguientes al configurar el enlace de un control personalizado:

  1. Las propiedades de enlace deben ser estáticas: al definir el enlace de propiedades, se debe usar el atributo [Static].
  2. Los nombres de propiedad deben coincidir exactamente: el nombre usado para enlazar la propiedad debe coincidir exactamente con el nombre de la propiedad en el control personalizado.
  3. Los tipos de propiedad deben coincidir exactamente: el tipo de variable usado para enlazar la propiedad debe coincidir exactamente con el tipo de la propiedad en el control personalizado.
  4. Puntos de interrupción y los métodos captador y establecedor: los puntos de interrupción colocados en el método captador o establecedor de la propiedad nunca se alcanzarán.
  5. Devoluciones de llamada de observación: deberá usar devoluciones de llamada de observación para recibir notificaciones de cambios en los valores de propiedad de los controles personalizados.

Si no se observa alguna de las advertencias enumeradas anteriormente, se puede producir un error silencioso en el enlace en tiempo de ejecución.

Patrones y propiedades mutables de Objective-C

Los marcos de Objective-C usan una expresión en la que algunas clases son inmutables con una subclase mutable. Por ejemplo, NSString es la versión inmutable, mientras que NSMutableString es la subclase que permite la mutación.

En esas clases es habitual ver que la clase base inmutable contiene propiedades con un método captador, pero ninguno establecedor. Y para que la versión mutable introduzca el establecedor. Puesto que esto no es realmente posible con C#, tuvimos que asignar esta expresión a una expresión que funcionara con C#.

La forma en que se asigna a C# es agregando el método captador y el establecedor en la clase base, pero marcando el establecedor con un [NotImplemented]Atributo .

A continuación, en la subclase mutable, use el atributo [Override] en la propiedad para asegurarse de que esta invalida realmente el comportamiento del elemento primario.

Ejemplo:

[BaseType (typeof (NSObject))]
interface MyTree {
    string Name { get; [NotImplemented] set; }
}

[BaseType (typeof (MyTree))]
interface MyMutableTree {
    [Override]
    string Name { get; set; }
}

Enlace de constructores

La herramienta btouch-native generará automáticamente cuatro constructores en la clase. Para una clase Foodeterminada, generará:

  • Foo (): el constructor predeterminado (se asigna al constructor "init" de Objective-C)
  • Foo (NSCoder): el constructor que se usa durante la deserialización de archivos NIB (se asigna al constructor "initWithCoder:" de Objective-C.
  • Foo (IntPtr handle): el constructor para la creación basada en identificadores. Este constructor lo invoca el entorno de ejecución cuando este tiene que exponer un objeto administrado desde un objeto no administrado.
  • Foo (NSEmptyFlag): se usa en las clases derivadas para evitar la inicialización doble.

En el caso de los constructores que define, tienen que declararse mediante la siguiente signatura dentro de la definición de interfaz: deben devolver un valor IntPtr y el nombre del método debe ser Constructor. Por ejemplo, para enlazar el constructor initWithFrame:, esto es lo que debe usar:

[Export ("initWithFrame:")]
IntPtr Constructor (CGRect frame);

Protocolos de enlace

Como se describe en el documento de diseño de API, en la sección Análisis de modelos y protocolos, Xamarin.iOS asigna los protocolos de Objective-C a clases que se han marcado con el [Model]Atributo . Esto se usa normalmente al implementar clases delegadas de Objective-C.

La gran diferencia entre una clase enlazada normal y una clase delegada es que esta última puede tener uno o varios métodos opcionales.

Por ejemplo, considere la clase UIAccelerometerDelegate de UIKit, así es como está enlazada en Xamarin.iOS:

[BaseType (typeof (NSObject))]
[Model][Protocol]
interface UIAccelerometerDelegate {
        [Export ("accelerometer:didAccelerate:")]
        void DidAccelerate (UIAccelerometer accelerometer, UIAcceleration acceleration);
}

Dado que se trata de un método opcional en la definición de UIAccelerometerDelegate ya no se puede hacer nada más. Pero si hubiera un método obligatorio en el protocolo, debería agregar el atributo [Abstract] al método. Esto obligará al usuario de la implementación a proporcionar realmente un cuerpo para el método.

En general, los protocolos se usan en clases que responden a los mensajes. Normalmente, esto se hace en Objective-C mediante la asignación a la propiedad "delegate" de una instancia de un objeto que responde a los métodos del protocolo.

La convención de Xamarin.iOS es, por una parte, admitir el estilo de acoplamiento flexible de Objective-C en el que se puede asignar cualquier instancia de un objeto NSObject al delegado y, por otra, exponer también una versión fuertemente tipada de él. Por este motivo, normalmente se proporciona una propiedad Delegate fuertemente tipada y una WeakDelegate débilmente tipada. Normalmente enlazamos la versión débilmente tipada con [Export] y usamos el atributo [Wrap] para proporcionar la versión fuertemente tipada.

Esto muestra cómo enlazamos la clase UIAccelerometer:

[BaseType (typeof (NSObject))]
interface UIAccelerometer {
        [Static] [Export ("sharedAccelerometer")]
        UIAccelerometer SharedAccelerometer { get; }

        [Export ("updateInterval")]
        double UpdateInterval { get; set; }

        [Wrap ("WeakDelegate")]
        UIAccelerometerDelegate Delegate { get; set; }

        [Export ("delegate", ArgumentSemantic.Assign)][NullAllowed]
        NSObject WeakDelegate { get; set; }
}

Novedades de MonoTouch 7.0

A partir de MonoTouch 7.0 se ha incorporado una nueva y mejorada funcionalidad de enlace de protocolos. Esta nueva compatibilidad facilita el uso de expresiones de Objective-C para adoptar uno o varios protocolos en una clase determinada.

Por cada definición de protocolo MyProtocol en Objective-C, ahora hay una interfaz IMyProtocol que enumera todos los métodos obligatorios del protocolo, así como una clase de extensión que proporciona todos los métodos opcionales. Lo anterior, combinado con la nueva compatibilidad del editor de Xamarin Studio, permite a los desarrolladores implementar métodos de protocolo sin tener que usar las subclases independientes de las clases de modelo abstractas anteriores.

Cualquier definición que contenga [Protocol] el atributo generará en realidad tres clases de apoyo que mejoran enormemente la forma de consumir protocolos:

// Full method implementation, contains all methods
class MyProtocol : IMyProtocol {
    public void Say (string msg);
    public void Listen (string msg);
}

// Interface that contains only the required methods
interface IMyProtocol: INativeObject, IDisposable {
    [Export ("say:")]
    void Say (string msg);
}

// Extension methods
static class IMyProtocol_Extensions {
    public static void Optional (this IMyProtocol this, string msg);
    }
}

La implementación de la clase proporciona una clase abstracta completa de la que puede sobrescribir métodos individuales y obtener una seguridad de tipo completa. Pero debido a que C# no admite varias herencias, hay escenarios en los que puede que necesite tener una clase base diferente, pero aún así quiera implementar una interfaz, ahí es donde

la definición de interfaz generada entra en acción. Es una interfaz que tiene todos los métodos necesarios del protocolo. Esto permite a los desarrolladores que quieran implementar el protocolo simplemente para implementar la interfaz. El tiempo de ejecución registrará automáticamente el tipo al adoptar el protocolo.

Observe que la interfaz solo enumera los métodos necesarios y expone los métodos opcionales. Esto significa que las clases que adopten el protocolo obtendrán una comprobación de tipos completa para los métodos obligatorios, pero tendrán que utilizar un establecimiento débil de tipos (utilizando manualmente los atributos [Export] y comparando la firma) para los métodos opcionales del protocolo.

Para que sea conveniente consumir una API que use protocolos, la herramienta de enlace también generará una clase de método de extensiones que expone todos los métodos opcionales. Esto significa que, siempre que consuma una API, podrá tratar los protocolos como tener todos los métodos.

Si quiere usar las definiciones de protocolo en la API, deberá escribir interfaces vacías de esqueleto en la definición de API. Si desea usar MyProtocol en una API, deberá hacerlo:

[BaseType (typeof (NSObject))]
[Model, Protocol]
interface MyProtocol {
    // Use [Abstract] when the method is defined in the @required section
    // of the protocol definition in Objective-C
    [Abstract]
    [Export ("say:")]
    void Say (string msg);

    [Export ("listen")]
    void Listen ();
}

interface IMyProtocol {}

[BaseType (typeof(NSObject))]
interface MyTool {
    [Export ("getProtocol")]
    IMyProtocol GetProtocol ();
}

Lo anterior es necesario porque en el momento del enlace no IMyProtocol existiría, por eso es necesario proporcionar una interfaz vacía.

Adopción de interfaces generadas por protocolos

Siempre que implemente una de las interfaces generadas para los protocolos, de la siguiente manera:

class MyDelegate : NSObject, IUITableViewDelegate {
    nint IUITableViewDelegate.GetRowHeight (nint row) {
        return 1;
    }
}

La implementación de los métodos de interfaz necesarios se exporta con el nombre adecuado, por lo que es equivalente a esto:

class MyDelegate : NSObject, IUITableViewDelegate {
    [Export ("getRowHeight:")]
    nint IUITableViewDelegate.GetRowHeight (nint row) {
        return 1;
    }
}

Esto funcionará para todos los miembros obligatorios del protocolo, pero hay que tener en cuenta un caso especial con los selectores opcionales. Los miembros de protocolo opcionales se tratan de forma idéntica al usar la clase base:

public class UrlSessionDelegate : NSUrlSessionDownloadDelegate {
	public override void DidWriteData (NSUrlSession session, NSUrlSessionDownloadTask downloadTask, long bytesWritten, long totalBytesWritten, long totalBytesExpectedToWrite)

pero cuando se usa la interfaz de protocolo, es necesario agregar [Export]. El IDE lo agregará a través de autocompletar al agregarlo a partir de la invalidación.

public class UrlSessionDelegate : NSObject, INSUrlSessionDownloadDelegate {
	[Export ("URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:")]
	public void DidWriteData (NSUrlSession session, NSUrlSessionDownloadTask downloadTask, long bytesWritten, long totalBytesWritten, long totalBytesExpectedToWrite)

Hay una ligera diferencia de comportamiento entre los dos en tiempo de ejecución.

  • Los usuarios de la clase base (NSUrlSessionDownloadDelegate en el ejemplo) proporcionan todos los selectores obligatorios y opcionales, devolviendo valores predeterminados razonables.
  • Los usuarios de la interfaz (INSUrlSessionDownloadDelegate en el ejemplo) solo responden a los selectores exactos proporcionados.

Algunas clases raras pueden comportarse de forma diferente aquí. En casi todos los casos, sin embargo, es seguro usar cualquiera de los dos.

Extensiones de clase de enlace

En Objective-C es posible extender las clases con nuevos métodos, similares en esencia a los métodos de extensión de C#. Si uno de estos métodos está presente, puede usar el atributo [BaseType] para marcar el método como receptor del mensaje de Objective-C.

Por ejemplo, en Xamarin.iOS enlazamos los métodos de extensión definidos en NSString cuando UIKit se importa como método en NSStringDrawingExtensions, de la siguiente manera:

[Category, BaseType (typeof (NSString))]
interface NSStringDrawingExtensions {
    [Export ("drawAtPoint:withFont:")]
    CGSize DrawString (CGPoint point, UIFont font);
}

Enlace de las listas de argumentos de Objective-C

Objective-C admite argumentos variádicos. Por ejemplo:

- (void) appendWorkers:(XWorker *) firstWorker, ...
  NS_REQUIRES_NIL_TERMINATION ;

Para invocar este método desde C#, querrá crear una firma como esta:

[Export ("appendWorkers"), Internal]
void AppendWorkers (Worker firstWorker, IntPtr workersPtr)

Esto declara el método como interno, ocultando la API anterior a los usuarios, pero exponiéndola a la biblioteca. A continuación, puede escribir un método similar al siguiente:

public void AppendWorkers(params Worker[] workers)
{
    if (workers == null)
         throw new ArgumentNullException ("workers");

    var pNativeArr = Marshal.AllocHGlobal(workers.Length * IntPtr.Size);
    for (int i = 1; i < workers.Length; ++i)
        Marshal.WriteIntPtr (pNativeArr, (i - 1) * IntPtr.Size, workers[i].Handle);

    // Null termination
    Marshal.WriteIntPtr (pNativeArr, (workers.Length - 1) * IntPtr.Size, IntPtr.Zero);

    // the signature for this method has gone from (IntPtr, IntPtr) to (Worker, IntPtr)
    WorkerManager.AppendWorkers(workers[0], pNativeArr);
    Marshal.FreeHGlobal(pNativeArr);
}

Enlace de campos

A veces, querrá tener acceso a los campos públicos declarados en una biblioteca.

Normalmente, estos campos contienen cadenas o valores enteros a los que se debe hacer referencia. Normalmente se usan como cadena que representa una notificación específica y como claves en diccionarios.

Para enlazar un campo, agregue una propiedad al archivo de definición de interfaz y decore la propiedad con el atributo [Field]. Este atributo toma un parámetro: el nombre C del símbolo que se va a buscar. Por ejemplo:

[Field ("NSSomeEventNotification")]
NSString NSSomeEventNotification { get; }

Si desea encapsular varios campos en una clase estática que no se deriva de NSObject, puede usar el atributo [Static] en la clase de la siguiente manera:

[Static]
interface LonelyClass {
    [Field ("NSSomeEventNotification")]
    NSString NSSomeEventNotification { get; }
}

Lo anterior generará una clase LonelyClass que no se deriva de NSObject y contendrá un enlace al elemento NSString de NSSomeEventNotificationexpuesto como NSString.

El atributo [Field] se puede aplicar a los siguientes tipos de datos:

  • Referencias NSString (solo propiedades de solo lectura)
  • Referencias NSArray (solo propiedades de solo lectura)
  • Enteros de 32 bits (System.Int32)
  • Enteros de 64 bits (System.Int64)
  • Valores flotantes de 32 bits (System.Single)
  • Valores flotantes de 64 bits (System.Double)
  • System.Drawing.SizeF
  • CGSize

Además del nombre del campo nativo, puede especificar el nombre de la biblioteca donde se encuentra el campo; para ello, pase el nombre de la biblioteca:

[Static]
interface LonelyClass {
    [Field ("SomeSharedLibrarySymbol", "SomeSharedLibrary")]
    NSString SomeSharedLibrarySymbol { get; }
}

Si está vinculando de manera estática, no hay ninguna biblioteca a la que enlazar, por lo que debe usar el nombre __Internal:

[Static]
interface LonelyClass {
    [Field ("MyFieldFromALibrary", "__Internal")]
    NSString MyFieldFromALibrary { get; }
}

Enlace de enumeraciones

Puede agregar enum directamente en los archivos de enlace para facilitar su uso dentro de las definiciones de API, sin usar un archivo de origen diferente (que debe compilarse tanto en los enlaces como en el proyecto final).

Ejemplo:

[Native] // needed for enums defined as NSInteger in ObjC
enum MyEnum {}

interface MyType {
    [Export ("initWithEnum:")]
    IntPtr Constructor (MyEnum value);
}

También es posible crear sus propias enumeraciones para reemplazar las constantes NSString. En este caso, el generador creará automáticamente los métodos para convertir los valores de enumeración y las constantes NSString.

Ejemplo:

enum NSRunLoopMode {

    [DefaultEnumValue]
    [Field ("NSDefaultRunLoopMode")]
    Default,

    [Field ("NSRunLoopCommonModes")]
    Common,

    [Field (null)]
    Other = 1000
}

interface MyType {
    [Export ("performForMode:")]
    void Perform (NSString mode);

    [Wrap ("Perform (mode.GetConstant ())")]
    void Perform (NSRunLoopMode mode);
}

En el ejemplo anterior, podría decidir decorar void Perform (NSString mode); con un atributo [Internal]. Esto ocultará la API basada en constantes de los consumidores de enlaces.

Sin embargo, esto limitaría la creación de subclases del tipo, ya que la alternativa de API más agradable usa un atributo [Wrap]. Esos métodos generados no son virtual, es decir, no podrá invalidarlos, lo que puede, o no, ser una buena opción.

Una alternativa es marcar la definición original, basada en NSString, como [Protected]. Esto permitirá que la creación de subclases funcione, cuando sea necesario, y la versión encapsulada seguirá funcionando y llamará al método invalidado.

Enlace de NSValue, NSNumber y NSString a un tipo mejor

El atributo [BindAs] permite enlazar NSNumber, NSValue y NSString(enumeraciones) en tipos de C# más precisos. El atributo se puede usar para crear una API de .NET mejor, más precisa y precisa a través de la API nativa.

Puede añadir métodos (en valor devuelto), parámetros y propiedades con [BindAs]. La única restricción es que el miembro no debe estar dentro de una interfaz [Protocol] o [Model].

Por ejemplo:

[return: BindAs (typeof (bool?))]
[Export ("shouldDrawAt:")]
NSNumber ShouldDraw ([BindAs (typeof (CGRect))] NSValue rect);

Se generaría lo siguiente:

[Export ("shouldDrawAt:")]
bool? ShouldDraw (CGRect rect) { ... }

Internamente haremos las bool?<->NSNumber y CGRect<->NSValueconversiones.

[BindAs] también admite matrices de NSNumber, NSValue y NSString(enumeraciones).

Por ejemplo:

[BindAs (typeof (CAScroll []))]
[Export ("supportedScrollModes")]
NSString [] SupportedScrollModes { get; set; }

Se generaría lo siguiente:

[Export ("supportedScrollModes")]
CAScroll [] SupportedScrollModes { get; set; }

CAScroll es una enumeración respaldada de NSString, capturaremos el valor correcto de NSString y controlaremos la conversión de tipos.

Consulte la documentación de [BindAs] para ver los tipos de conversión admitidos.

Enlace de notificaciones

Las notificaciones son mensajes que se publican en NSNotificationCenter.DefaultCenter y se usan como mecanismo para transmitir mensajes de una parte de la aplicación a otra. Los desarrolladores se suscriben a las notificaciones normalmente mediante el método AddObserver de NSNotificationCenter. Cuando una aplicación publica un mensaje en el centro de notificaciones, normalmente contiene una carga almacenada en el diccionario NSNotification.UserInfo. Este diccionario está débilmente tipado y obtener información de él tiende a producir a errores, ya que los usuarios normalmente necesitan leer en la documentación qué claves están disponibles en el diccionario y los tipos de los valores que se pueden almacenar en el diccionario. La presencia de claves a veces también se usa como un valor booleano.

El generador de enlaces de Xamarin.iOS proporciona compatibilidad para que los desarrolladores enlacen notificaciones. Para ello, establezca el atributo [Notification] en una propiedad que también se haya etiquetado con una propiedad [Field] (puede ser pública o privada).

Este atributo se puede utilizar sin argumentos para las notificaciones que no llevan carga útil, o se puede especificar una System.Type que haga referencia a otra interfaz en la definición de la API, normalmente con el nombre terminado en "EventArgs". El generador convertirá la interfaz en una clase que subclase EventArgs e incluirá todas las propiedades enumeradas allí. El atributo [Export] debe usarse en la clase EventArgs para enumerar el nombre de la clave utilizada para buscar en el diccionario Objective-C para capturar el valor.

Por ejemplo:

interface MyClass {
    [Notification]
    [Field ("MyClassDidStartNotification")]
    NSString DidStartNotification { get; }
}

El código anterior generará una clase anidada MyClass.Notifications con los métodos siguientes:

public class MyClass {
   [..]
   public Notifications {
      public static NSObject ObserveDidStart (EventHandler<NSNotificationEventArgs> handler)
   }
}

Los usuarios del código pueden suscribirse fácilmente a las notificaciones publicadas en NSDefaultCenter mediante código similar al siguiente:

var token = MyClass.Notifications.ObserverDidStart ((notification) => {
    Console.WriteLine ("Observed the 'DidStart' event!");
});

El valor devuelto de ObserveDidStart se puede usar para dejar fácilmente de recibir notificaciones, de la siguiente manera:

token.Dispose ();

O bien, puede llamar a NSNotification.DefaultCenter.RemoveObserver y pasar el token. Si la notificación contiene parámetros, debe especificar una interfaz auxiliar, EventArgs de la siguiente manera:

interface MyClass {
    [Notification (typeof (MyScreenChangedEventArgs)]
    [Field ("MyClassScreenChangedNotification")]
    NSString ScreenChangedNotification { get; }
}

// The helper EventArgs declaration
interface MyScreenChangedEventArgs {
    [Export ("ScreenXKey")]
    nint ScreenX { get; set; }

    [Export ("ScreenYKey")]
    nint ScreenY { get; set; }

    [Export ("DidGoOffKey")]
    [ProbePresence]
    bool DidGoOff { get; }
}

Lo anterior generará una clase MyScreenChangedEventArgs con las propiedades ScreenX y ScreenY que capturarán los datos del diccionario NSNotification.UserInfo mediante las claves "ScreenXKey" y "ScreenYKey" respectivamente, y aplicarán las conversiones adecuadas. El [ProbePresence] atributo se usa para que el generador sondee si la clave está establecida en UserInfo, en lugar de intentar extraer el valor. Esto se usa para los casos en los que la presencia de la clave es el valor (normalmente para valores booleanos).

Esto le permite escribir código similar al siguiente:

var token = MyClass.NotificationsObserveScreenChanged ((notification) => {
    Console.WriteLine ("The new screen dimensions are {0},{1}", notification.ScreenX, notification.ScreenY);
});

Enlace de categorías

Las categorías son un Objective-C mecanismo que se usa para ampliar el conjunto de métodos y propiedades disponibles en una clase. En la práctica, se usan para ampliar la funcionalidad de una clase base (por ejemplo NSObject) cuando un marco específico está vinculado en (por ejemplo UIKit), haciendo que sus métodos estén disponibles, pero solo si el nuevo marco está vinculado. En otros casos, se usan para organizar las características de una clase por funcionalidad. Son similares en esencia a los métodos de extensión de C#. Este es el aspecto de una categoría en Objective-C:

@interface UIView (MyUIViewExtension)
-(void) makeBackgroundRed;
@end

El ejemplo anterior se encuentra en una biblioteca que extendería instancias de UIView con el método makeBackgroundRed.

Para enlazarlos, puede usar el [Category] atributo en una definición de interfaz. Cuando usa el atributo [Category], el significado del atributo [BaseType] cambia de usarse para especificar la clase base que se va a extender, a usarse para especificar el tipo que se va a extender.

A continuación se muestra cómo UIView se enlazan las extensiones y se convierten en métodos de extensión de C#:

[BaseType (typeof (UIView))]
[Category]
interface MyUIViewExtension {
    [Export ("makeBackgroundRed")]
    void MakeBackgroundRed ();
}

Lo anterior creará una MyUIViewExtension clase que contenga el método de MakeBackgroundRed extensión. Esto significa que ahora puede llamar a "MakeBackgroundRed" en cualquier subclase UIView, lo que le proporciona la misma funcionalidad que obtendría en Objective-C. En algunos otros casos, las categorías se usan no para extender una clase del sistema, sino para organizar la funcionalidad, exclusivamente con fines de decoración. Por ejemplo:

@interface SocialNetworking (Twitter)
- (void) postToTwitter:(Message *) message;
@end

@interface SocialNetworking (Facebook)
- (void) postToFacebook:(Message *) message andPicture: (UIImage*)
picture;
@end

Aunque puede usar el atributo [Category] también para este estilo de decoración de declaraciones, también puede agregarlos todos a la definición de clase. Ambos enfoques lograrían lo mismo:

[BaseType (typeof (NSObject))]
interface SocialNetworking {
}

[Category]
[BaseType (typeof (SocialNetworking))]
interface Twitter {
    [Export ("postToTwitter:")]
    void PostToTwitter (Message message);
}

[Category]
[BaseType (typeof (SocialNetworking))]
interface Facebook {
    [Export ("postToFacebook:andPicture:")]
    void PostToFacebook (Message message, UIImage picture);
}

En estos casos, es más sencillo combinar las categorías:

[BaseType (typeof (NSObject))]
interface SocialNetworking {
    [Export ("postToTwitter:")]
    void PostToTwitter (Message message);

    [Export ("postToFacebook:andPicture:")]
    void PostToFacebook (Message message, UIImage picture);
}

Enlace de bloques

Los bloques son una nueva construcción presentada por Apple para llevar el equivalente funcional de los métodos anónimos de C# a Objective-C. Por ejemplo, la clase NSSet expone ahora este método:

- (void) enumerateObjectsUsingBlock:(void (^)(id obj, BOOL *stop) block

La descripción anterior declara un método denominado enumerateObjectsUsingBlock: que toma un argumento denominado block. Este bloque se parece a un método anónimo de C# en que tiene compatibilidad para capturar el entorno actual (el puntero "this", el acceso a variables y parámetros locales). El método anterior de NSSet invoca el bloque con dos parámetros en NSObject (la parte id obj) y un puntero a un elemento booleano (la parte BOOL *stop).

Para enlazar este tipo de API con btouch, primero debe declarar la firma del tipo de bloque como delegada de C# y, a continuación, hacer referencia a ella desde un punto de entrada de API, de la siguiente manera:

// This declares the callback signature for the block:
delegate void NSSetEnumerator (NSObject obj, ref bool stop)

// Later, inside your definition, do this:
[Export ("enumerateObjectUsingBlock:")]
void Enumerate (NSSetEnumerator enum)

Y ahora el código puede llamar a la función desde C#:

var myset = new NSMutableSet ();
myset.Add (new NSString ("Foo"));

s.Enumerate (delegate (NSObject obj, ref bool stop){
    Console.WriteLine ("The first object is: {0} and stop is: {1}", obj, stop);
});

También puede usar expresiones lambda si lo prefiere:

var myset = new NSMutableSet ();
mySet.Add (new NSString ("Foo"));

s.Enumerate ((obj, stop) => {
    Console.WriteLine ("The first object is: {0} and stop is: {1}", obj, stop);
});

Métodos asincrónicos

El generador de enlaces puede convertir una determinada clase de métodos en métodos descriptivos asincrónicos (métodos que devuelven una tarea o Tarea<T>).

Puede usar el Puede usar el atributo [Async] en métodos que devuelven void y cuyo último argumento es una devolución de llamada. Cuando se aplica a un método, el generador de enlaces generará una versión de ese método con el sufijo Async. Si la devolución de llamada no toma parámetros, el valor devuelto será Task, si la devolución de llamada toma un parámetro, el resultado será Task<T>. Si la devolución de llamada toma varios parámetros, debe establecer ResultType o ResultTypeName para especificar el nombre deseado del tipo generado que contendrá todas las propiedades.

Ejemplo:

[Export ("loadfile:completed:")]
[Async]
void LoadFile (string file, Action<string> completed);

El código anterior generará el método LoadFile, así como:

[Export ("loadfile:completed:")]
Task<string> LoadFileAsync (string file);

Exposición de tipos seguros para parámetros NSDictionary débiles

En muchos lugares de la API Objective-C, los parámetros se pasan como API de NSDictionary débilmente tipados con claves y valores específicos, pero son propensos a errores (puede pasar claves no válidas o valores no válidos y no recibir ninguna advertencia) y complicados de usar, ya que requieren varios desplazamientos a la documentación para buscar los posibles nombres y valores de clave.

La solución consiste en proporcionar una versión fuertemente tipada de la API y, en segundo plano, asignar las distintas claves y valores subyacentes.

Por ejemplo, si la API de Objective-C aceptó un NSDictionary y se documenta como tomar la clave XyzVolumeKey que toma un elemento NSNumber con un valor comprendido entre 0,0 y 1,0 y un elemento XyzCaptionKey que toma una cadena, seguramente deseará que los usuarios tengan una API sencilla parecida a esta:

public class  XyzOptions {
    public nfloat? Volume { get; set; }
    public string Caption { get; set; }
}

La propiedad Volume se define como un valor float que admite un valor NULL, ya que la convención de Objective-C no requiere que estos diccionarios tengan el valor, por lo que hay escenarios en los que es posible que el valor no se establezca.

Para eso, tiene que hacer algunas cosas:

  • Crear una clase fuertemente tipada, que cree subclases DictionaryContainer y proporcione los distintos métodos captadores y establecedores de cada propiedad.
  • Declarar sobrecargas para los métodos que toman NSDictionary para usar la nueva versión fuertemente tipada.

Puede crear la clase fuertemente tipada manualmente o usar el generador para lo haga automáticamente. En primer lugar, vamos a explicar cómo hacerlo manualmente para comprender lo que está ocurriendo y, posteriormente, usaremos el enfoque automático.

Debe crear un archivo auxiliar para esto, no está incluido en la API de contrato. Esto es lo que tendría que escribir para crear la clase XyzOptions:

public class XyzOptions : DictionaryContainer {
# if !COREBUILD
    public XyzOptions () : base (new NSMutableDictionary ()) {}
    public XyzOptions (NSDictionary dictionary) : base (dictionary){}

    public nfloat? Volume {
       get { return GetFloatValue (XyzOptionsKeys.VolumeKey); }
       set { SetNumberValue (XyzOptionsKeys.VolumeKey, value); }
    }
    public string Caption {
       get { return GetStringValue (XyzOptionsKeys.CaptionKey); }
       set { SetStringValue (XyzOptionsKeys.CaptionKey, value); }
    }
# endif
}

A continuación, debe proporcionar un método contenedor que exponga la API de alto nivel, sobre la API de bajo nivel.

[BaseType (typeof (NSObject))]
interface XyzPanel {
    [Export ("playback:withOptions:")]
    void Playback (string fileName, [NullAllowed] NSDictionary options);

    [Wrap ("Playback (fileName, options == null ? null : options.Dictionary")]
    void Playback (string fileName, XyzOptions options);
}

Si no es necesario sobrescribir la API, puede ocultar de forma segura la API basada en NSDictionary mediante el uso de [Internal]Atributo .

Como puede ver, se usa el atributo [Wrap] para exponer un nuevo punto de entrada de API y lo exponemos mediante nuestra clase XyzOptions fuertemente tipada. El método contenedor también permite pasar valores null.

Ahora, una cosa que no hemos mencionado es de dónde proceden los valores XyzOptionsKeys. Lo normal sería agrupar las claves que una API expone en una clase estática como XyzOptionsKeys, de la siguiente manera:

[Static]
class XyzOptionKeys {
    [Field ("kXyzVolumeKey")]
    NSString VolumeKey { get; }

    [Field ("kXyzCaptionKey")]
    NSString CaptionKey { get; }
}

Echemos un vistazo a la compatibilidad automática para crear estos diccionarios fuertemente tipados. Esto evita una gran cantidad de texto reutilizable, con lo que puede definir el diccionario directamente en el contrato de API, en lugar de usar un archivo externo.

Para crear un diccionario fuertemente tipado, especifique una interfaz en la API y decórela con el atributo StrongDictionary. Esto indica al generador que debe crear una clase con el mismo nombre que la interfaz que se derivará de DictionaryContainer y proporcionará descriptores de acceso fuertemente tipados para ella.

El atributo [StrongDictionary] toma un parámetro que es el nombre de la clase estática que contiene las claves de diccionario. A continuación, cada propiedad de la interfaz se convertirá en un descriptor de acceso fuertemente tipado. De forma predeterminada, el código usará el nombre de la propiedad con el sufijo "Key" en la clase estática para crear el descriptor de acceso.

Esto significa que la creación de su descriptor de acceso fuertemente tipado ya no requiere un archivo externo, ni tener que crear manualmente métodos captadores y establecedores para cada propiedad, ni tener que buscar las claves manualmente.

Este es el aspecto que tendría el enlace completo:

[Static]
class XyzOptionKeys {
    [Field ("kXyzVolumeKey")]
    NSString VolumeKey { get; }

    [Field ("kXyzCaptionKey")]
    NSString CaptionKey { get; }
}
[StrongDictionary ("XyzOptionKeys")]
interface XyzOptions {
    nfloat Volume { get; set; }
    string Caption { get; set; }
}

[BaseType (typeof (NSObject))]
interface XyzPanel {
    [Export ("playback:withOptions:")]
    void Playback (string fileName, [NullAllowed] NSDictionary options);

    [Wrap ("Playback (fileName, options == null ? null : options.Dictionary")]
    void Playback (string fileName, XyzOptions options);
}

En caso de que necesite que los miembros de XyzOption hagan referencia a un campo diferente (que no sea el nombre de la propiedad con el sufijo Key), puede decorar la propiedad con un atributo [Export] con el nombre que desee usar.

Asignaciones de tipos

En esta sección se explica cómo los tipos de Objective-C se asignan a los tipos de C#.

Tipos simples

En la tabla siguiente se muestra cómo debe asignar tipos del entorno de Objective-C y de CocoaTouch al entorno de Xamarin.iOS:

Nombre de tipo de Objective-C Tipo de Unified API para Xamarin.iOS
BOOL, GLboolean bool
NSInteger nint
NSUInteger nuint
CFTimeInterval / NSTimeInterval double
NSString (más información sobre el enlace de NSString) string
char * string (consulte también: [PlainString])
CGRect CGRect
CGPoint CGPoint
CGSize CGSize
CGFloat, GLfloat nfloat
Tipos de CoreFoundation (CF*) CoreFoundation.CF*
GLint nint
GLfloat nfloat
Tipos de Foundation (NS*) Foundation.NS*
id Foundation.NSObject
NSGlyph nint
NSSize CGSize
NSTextAlignment UITextAlignment
SEL ObjCRuntime.Selector
dispatch_queue_t CoreFoundation.DispatchQueue
CFTimeInterval double
CFIndex nint
NSGlyph nuint

Matrices

El entorno de ejecución de Xamarin.iOS se encarga automáticamente de convertir matrices de C# en NSArrays y a la inversa, como, por ejemplo, en el método imaginario Objective-C que devuelve un elemento NSArray de UIViews:

// Get the peer views - untyped
- (NSArray *)getPeerViews ();

// Set the views for this container
- (void) setViews:(NSArray *) views

Está enlazado de esta manera:

[Export ("getPeerViews")]
UIView [] GetPeerViews ();

[Export ("setViews:")]
void SetViews (UIView [] views);

La idea es usar una matriz de C# fuertemente tipada, ya que esto permitirá al IDE proporcionar una finalización de código adecuada con el tipo real sin forzar al usuario a adivinar, o buscar la documentación para averiguar el tipo real de los objetos contenidos en la matriz.

En los casos en los que no se puede realizar un seguimiento del tipo real más derivado incluido en la matriz, puede usar NSObject [] como valor devuelto.

Selectores

Los selectores aparecen en la API de Objective-C como el tipo especial SEL. Al enlazar un selector, debería asignar el tipo a ObjCRuntime.Selector. Normalmente, los selectores se exponen en una API con un objeto, el objeto de destino y el selector que se va a invocar en el objeto de destino. Proporcionar ambos elementos básicamente corresponde al delegado de C#: algo que encapsula tanto el método que se va a invocar como el objeto en el que se va a invocar el método.

Este es el aspecto del enlace:

interface Button {
   [Export ("setTarget:selector:")]
   void SetTarget (NSObject target, Selector sel);
}

Y así es como el método se usaría normalmente en una aplicación:

class DialogPrint : UIViewController {
    void HookPrintButton (Button b)
    {
        b.SetTarget (this, new Selector ("print"));
    }

    [Export ("print")]
    void ThePrintMethod ()
    {
       // This does the printing
    }
}

Para que el enlace sea más adecuado para los desarrolladores de C#, normalmente proporcionará un método que toma un parámetro NSAction, lo que permite usar delegados de C# y expresiones lambda en lugar de Target+Selector. Para ello, lo normal es ocultar el método SetTarget marcándolo con un atributo [Internal] y, a continuación, exponer un nuevo método auxiliar, de la siguiente manera:

// API.cs
interface Button {
   [Export ("setTarget:selector:"), Internal]
   void SetTarget (NSObject target, Selector sel);
}

// Extensions.cs
public partial class Button {
     public void SetTarget (NSAction callback)
     {
         SetTarget (new NSActionDispatcher (callback), NSActionDispatcher.Selector);
     }
}

Por lo tanto, ahora el código de usuario se puede escribir de la siguiente manera:

class DialogPrint : UIViewController {
    void HookPrintButton (Button b)
    {
        // First Style
        b.SetTarget (ThePrintMethod);

        // Lambda style
        b.SetTarget (() => {  /* print here */ });
    }

    void ThePrintMethod ()
    {
       // This does the printing
    }
}

Cadenas

Cuando se enlaza un método que toma un elemento NSString, puede reemplazarlo por un tipo de cadena de C#, tanto en los tipos devueltos como en los parámetros.

El único caso en el que es posible que quiera usar directamente NSString es cuando la cadena se usa como token. Para más información sobre las cadenas y NSString, lea el documento Diseño de API en NSString.

En algunos casos poco frecuentes, una API podría exponer una cadena similar a C (char *) en lugar de una cadena de Objective-C (NSString *). En esos casos, puede anotar el parámetro con los [PlainString]Atributo .

parámetros out y ref

Algunas API devuelven valores en sus parámetros o pasan parámetros por referencia.

Normalmente, el aspecto de la signatura es el siguiente:

- (void) someting:(int) foo withError:(NSError **) retError
- (void) someString:(NSObject **)byref

En el primer ejemplo se muestra una expresión Objective-C común para que devuelva códigos de error, se pasa un puntero a un puntero NSError y, tras devolver el valor, se establece dicho valor. El segundo método muestra cómo un método Objective-C puede tomar un objeto y modificar su contenido. Se trata de un paso por referencia, en lugar de un valor de salida puro.

El enlace tendría este aspecto:

[Export ("something:withError:")]
void Something (nint foo, out NSError error);
[Export ("someString:")]
void SomeString (ref NSObject byref);

Atributos de administración de memoria

Cuando se usa el atributo [Export] y se pasan los datos que conservará el método llamado, puede especificar la semántica del argumento pasándolo como segundo parámetro, por ejemplo:

[Export ("method", ArgumentSemantic.Retain)]

En el caso anterior se marcaría el valor como que tiene la semántica "Retain". Las semánticas disponible son:

  • Assignar
  • Copiar
  • Retain

Guía de estilo

Uso de [Internal]

Puede usar el Puede usar el atributo [Internal] para ocultar un método a la API pública. Es posible que quiera hacerlo en casos en los que la API expuesta sea de un nivel demasiado bajo y quiera proporcionar una implementación de alto nivel en un archivo independiente basado en este método.

También puede usarlo cuando se produzcan limitaciones en el generador de enlaces, por ejemplo, en algunos escenarios avanzados se podrían exponer tipos que no están enlazados y que desea enlazar por cuenta propia, y desea encapsular esos tipos a su manera.

Controladores de eventos y devoluciones de llamada

Las clases de Objective-C suelen transmitir notificaciones o solicitar información mediante el envío de un mensaje a una clase delegada (delegada de Objective-C).

Este modelo, aunque es totalmente compatible con Xamarin.iOS y este lo puede exponer, a veces puede resultar complejo. Xamarin.iOS expone el patrón de eventos de C# y un sistema de devolución de llamada de método en la clase que se puede usar en estas situaciones. Esto permite que se ejecute código como este:

button.Clicked += delegate {
    Console.WriteLine ("I was clicked");
};

El generador de enlaces es capaz de reducir la cantidad de escritura necesaria para asignar el patrón de Objective-C al patrón de C#.

A partir de Xamarin.iOS 1.4, también es posible indicar al generador que produzca enlaces para delegados específicos de Objective-C y que exponga el delegado como eventos y propiedades de C# en el tipo de host.

Hay dos clases implicadas en este proceso, la clase host que es la que emite eventos y los envía actualmente a la clase Delegate o WeakDelegate, y la clase delegada real.

Teniendo en cuenta la siguiente configuración:

[BaseType (typeof (NSObject))]
interface MyClass {
    [Export ("delegate", ArgumentSemantic.Assign)][NullAllowed]
    NSObject WeakDelegate { get; set; }

    [Wrap ("WeakDelegate")][NullAllowed]
    MyClassDelegate Delegate { get; set; }
}

[BaseType (typeof (NSObject))]
interface MyClassDelegate {
    [Export ("loaded:bytes:")]
    void Loaded (MyClass sender, int bytes);
}

Para encapsular la clase, debe:

  • En la clase host, agregar a la declaración [BaseType]
    el tipo que actúa como delegado y el nombre de C# que ha expuesto. En nuestro ejemplo anterior, estos son typeof (MyClassDelegate) y WeakDelegate respectivamente.
  • En la clase delegada, en cada método que tenga más de dos parámetros, deberá especificar el tipo que desea usar para la clase EventArgs generada automáticamente.

El generador de enlaces no se limita a encapsular solo un destino de evento, es posible que algunas clases de Objective-C emitan mensajes a más de un delegado, por lo que tendrá que proporcionar matrices para admitir esta configuración. La mayoría de las configuraciones no lo necesitarán, pero el generador está preparado para admitir esos casos.

El código resultante será:

[BaseType (typeof (NSObject),
    Delegates=new string [] {"WeakDelegate"},
    Events=new Type [] { typeof (MyClassDelegate) })]
interface MyClass {
        [Export ("delegate", ArgumentSemantic.Assign)][NullAllowed]
        NSObject WeakDelegate { get; set; }

        [Wrap ("WeakDelegate")][NullAllowed]
        MyClassDelegate Delegate { get; set; }
}

[BaseType (typeof (NSObject))]
interface MyClassDelegate {
        [Export ("loaded:bytes:"), EventArgs ("MyClassLoaded")]
        void Loaded (MyClass sender, int bytes);
}

EventArgs se usa para especificar el nombre de la clase EventArgs que se va a generar. Debe usar una por signatura (en este ejemplo, la clase EventArgs contendrá una propiedad With de tipo nint).

Con las definiciones anteriores, el generador generará el siguiente evento en la clase MyClass generada:

public MyClassLoadedEventArgs : EventArgs {
        public MyClassLoadedEventArgs (nint bytes);
        public nint Bytes { get; set; }
}

public event EventHandler<MyClassLoadedEventArgs> Loaded {
        add; remove;
}

Por lo tanto, ahora puede usar el código de la siguiente manera:

MyClass c = new MyClass ();
c.Loaded += delegate (sender, args){
        Console.WriteLine ("Loaded event with {0} bytes", args.Bytes);
};

Las devoluciones de llamada son iguales a las invocaciones de eventos, la diferencia es que, en lugar de tener varios suscriptores potenciales (por ejemplo, varios métodos que pueden enlazarse a un evento Clicked o a un evento DownloadFinished) las devoluciones de llamada solo pueden tener un único suscriptor.

El proceso es idéntico, la única diferencia es que, en lugar de exponer el nombre de la clase EventArgs que se va a generar, EventArgs realmente se usa para asignar un nombre al delegado de C# resultante.

Si el método de la clase delegada devuelve un valor, el generador de enlaces lo asignará a un método delegado en la clase primaria en lugar de a un evento. En estos casos, debe proporcionar el valor predeterminado que debe devolver el método si el usuario no se enlaza al delegado. Puede hacerlo mediante los atributos [DefaultValue] o [DefaultValueFromArgument].

[DefaultValue] codificará de forma rígida un valor devuelto, mientras que [DefaultValueFromArgument] se usa para especificar qué argumento de entrada se devolverá.

Enumeraciones y tipos base

También puede hacer referencia a enumeraciones o tipos base que no son compatibles directamente con el sistema de definición de interfaz btouch. Para ello, coloque las enumeraciones y los tipos principales en un archivo independiente e incluya este como parte de uno de los archivos adicionales que proporcione a btouch.

Vinculación de las dependencias

Si va a enlazar API que no forman parte de la aplicación, debe asegurarse de que el archivo ejecutable esté vinculado a estas bibliotecas.

Tiene que informar a Xamarin.iOS de cómo vincular las bibliotecas; esto se puede hacer modificando la configuración de compilación para que invoque el comando mtouch con algunos argumentos de compilación adicionales que especifican cómo vincular con las nuevas bibliotecas mediante la opción "-gcc_flags", seguida de una cadena entre comillas que contiene todas las bibliotecas adicionales necesarias para el programa, de forma parecida a esta:

-gcc_flags "-L$(MSBuildProjectDirectory) -lMylibrary -force_load -lSystemLibrary -framework CFNetwork -ObjC"

En el ejemplo anterior se vinculará libMyLibrary.a, libSystemLibrary.dylib y la biblioteca de marcos CFNetwork en el archivo ejecutable final.

O bien, puede aprovechar el nivel de ensamblado [LinkWithAttribute], que puede insertar en los archivos de contrato (como AssemblyInfo.cs). Cuando use [LinkWithAttribute], tendrá que tener la biblioteca nativa disponible en el momento en que haga el enlace, ya que esto insertará la biblioteca nativa con la aplicación. Por ejemplo:

// Specify only the library name as a constructor argument and specify everything else with properties:
[assembly: LinkWith ("libMyLibrary.a", LinkTarget = LinkTarget.ArmV6 | LinkTarget.ArmV7 | LinkTarget.Simulator, ForceLoad = true, IsCxx = true)]

// Or you can specify library name *and* link target as constructor arguments:
[assembly: LinkWith ("libMyLibrary.a", LinkTarget.ArmV6 | LinkTarget.ArmV7 | LinkTarget.Simulator, ForceLoad = true, IsCxx = true)]

Es posible que se pregunte, por qué necesita el comando -force_load y el motivo es que la marca -ObjC aunque compila el código, no conserva los metadatos necesarios para admitir las categorías (el enlazador o compilador elimina las franjas de eliminación de código no alcanzado) que necesita en el entorno de ejecución de Xamarin.iOS.

Referencias asistidas

Algunos objetos transitorios como las hojas de acciones y los cuadros de alerta son difíciles de rastrear para los desarrolladores y el generador de enlaces puede ayudar un poco aquí.

Por ejemplo, si tuviera una clase que mostrara un mensaje y, a continuación, generara un evento Done, la manera tradicional de controlar esto sería:

class Demo {
    MessageBox box;

    void ShowError (string msg)
    {
        box = new MessageBox (msg);
        box.Done += { box = null; ... };
    }
}

En el escenario anterior, el desarrollador debe mantener la referencia al propio objeto y filtrar o borrar activamente la referencia del cuadro por su cuenta. Mientras se enlaza el código, el generador permite realizar un seguimiento de la referencia y borrarla cuando se invoca un método especial, en cuyo caso el código anterior se convertirá en:

class Demo {
    void ShowError (string msg)
    {
        var box = new MessageBox (msg);
        box.Done += { ... };
    }
}

Observe cómo ya no es necesario mantener la variable en una instancia, que funciona con una variable local y que no es necesario borrar la referencia cuando el objeto muere.

Para aprovechar esto, la clase debe tener una propiedad Events establecida en la declaración [BaseType] y también la variable KeepUntilRef establecida en el nombre del método que se invoca cuando el objeto ha completado su trabajo, de la siguiente manera:

[BaseType (typeof (NSObject), KeepUntilRef="Dismiss"), Delegates=new string [] { "WeakDelegate" }, Events=new Type [] { typeof (SomeDelegate) }) ]
class Demo {
    [Export ("show")]
    void Show (string message);
}

Heredar protocolos

A partir de Xamarin.iOS v3.2, se admite la herencia de protocolos marcados con la propiedad [Model]. Esto resulta útil en determinados patrones de API, como en MapKit donde el protocolo MKOverlay se hereda del protocolo MKAnnotation y se adopta mediante una serie de clases que se heredan de NSObject.

Históricamente, era obligatorio copiar el protocolo en cada implementación, pero en estos casos ahora podemos hacer que la clase MKShape se herede del protocolo MKOverlay y generará todos los métodos obligatorios automáticamente.