Condividi tramite


Librerie di binding Objective-C

Quando si usa Xamarin.iOS o Xamarin.Mac, è possibile che si verifichino casi in cui si vuole usare una libreria di terze parti Objective-C . In queste situazioni, è possibile usare progetti di associazione Xamarin per creare un'associazione C# alle librerie native Objective-C . Il progetto usa gli stessi strumenti usati per portare le API iOS e Mac in C#.

Questo documento descrive come associare Objective-C le API, se si associano solo LE API C, è consigliabile usare il meccanismo .NET standard per questo, il framework P/Invoke. Informazioni dettagliate su come collegare in modo statico una libreria C sono disponibili nella pagina Collegamento di librerie native.

Vedere la Guida di riferimento ai tipi di associazione complementare. Inoltre, se vuoi saperne di più su cosa accade sotto le quinte, consulta la pagina Panoramica del binding.

Le associazioni possono essere create sia per le librerie iOS che per Mac. Questa pagina descrive come lavorare su un'associazione iOS, tuttavia le associazioni Mac sono molto simili.

Codice di esempio per iOS

È possibile usare il progetto di esempio di binding iOS per sperimentare le associazioni.

Introduzione

Il modo più semplice per creare un'associazione consiste nel creare un progetto di associazione Xamarin.iOS. È possibile eseguire questa operazione da Visual Studio per Mac selezionando il tipo di progetto, libreria di associazioni della libreria >iOS>:

Eseguire questa operazione da Visual Studio per Mac selezionando il tipo di progetto, libreria di associazioni della libreria iOS

Il progetto generato contiene un piccolo modello che è possibile modificare, contiene due file: ApiDefinition.cs e StructsAndEnums.cs.

ApiDefinition.cs è la posizione in cui si definirà il contratto API, ovvero il file che descrive il modo in cui l'API sottostante Objective-C viene proiettata in C#. La sintassi e il contenuto di questo file sono l'argomento principale della discussione di questo documento e il relativo contenuto è limitato alle interfacce C# e alle dichiarazioni di delegati C#. Il StructsAndEnums.cs file è il file in cui immettere le definizioni richieste dalle interfacce e dai delegati. Sono inclusi i valori di enumerazione e le strutture che il codice potrebbe usare.

Associazione di un'API

Per eseguire un'associazione completa, è necessario comprendere la definizione dell'API Objective-C e acquisire familiarità con le linee guida per la progettazione di .NET Framework.

Per associare la libreria, in genere si inizierà con un file di definizione DELL'API. Un file di definizione API è semplicemente un file di origine C# che contiene interfacce C# annotate con pochi attributi che consentono di guidare l'associazione. Questo file definisce il contratto tra C# e Objective-C .

Ad esempio, si tratta di un file API semplice per una libreria:

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

L'esempio precedente definisce una classe denominata Cocos2D.Camera che deriva dal NSObject tipo di base (questo tipo deriva da Foundation.NSObject) e che definisce una proprietà statica (ZEye), due metodi che non accettano argomenti e un metodo che accetta tre argomenti.

Una descrizione approfondita del formato del file API e degli attributi che è possibile usare è illustrata nella sezione file di definizione API seguente.

Per produrre un'associazione completa, in genere si gestiscono quattro componenti:

  • File di definizione dell'API (ApiDefinition.cs nel modello).
  • Facoltativo: qualsiasi enumerazione, tipi, struct richiesti dal file di definizione dell'API (StructsAndEnums.cs nel modello).
  • Facoltativo: origini aggiuntive che potrebbero espandere l'associazione generata o fornire un'API più intuitiva C# (tutti i file C# aggiunti al progetto).
  • Libreria nativa di cui si esegue l'associazione.

Questo grafico mostra la relazione tra i file:

Questo grafico mostra la relazione tra i file

Il file di definizione DELL'API conterrà solo spazi dei nomi e definizioni di interfaccia (con tutti i membri che un'interfaccia può contenere) e non deve contenere classi, enumerazioni, delegati o struct. Il file di definizione dell'API è semplicemente il contratto che verrà usato per generare l'API.

Qualsiasi codice aggiuntivo necessario come enumerazioni o classi di supporto deve essere ospitato in un file separato, nell'esempio precedente "CameraMode" è un valore di enumerazione che non esiste nel file CS e deve essere ospitato in un file separato, ad esempio StructsAndEnums.cs:

public enum CameraMode {
    FlyOver, Back, Follow
}

Il APIDefinition.cs file viene combinato con la StructsAndEnum classe e viene usato per generare l'associazione principale della libreria. È possibile usare la libreria risultante così come è, ma in genere si vuole ottimizzare la libreria risultante per aggiungere alcune funzionalità C# a vantaggio degli utenti. Alcuni esempi includono l'implementazione di un ToString() metodo, fornire indicizzatori C#, aggiungere conversioni implicite da e verso alcuni tipi nativi o fornire versioni fortemente tipizzate di alcuni metodi. Questi miglioramenti vengono archiviati in file C# aggiuntivi. Aggiungere semplicemente i file C# al progetto e verranno inclusi in questo processo di compilazione.

Viene illustrato come implementare il codice nel Extra.cs file. Si noti che si usano classi parziali perché aumentano le classi parziali generate dalla combinazione di ApiDefinition.cs e l'associazione StructsAndEnums.cs principale:

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

La compilazione della libreria produrrà l'associazione nativa.

Per completare questa associazione, è necessario aggiungere la libreria nativa al progetto. A tale scopo, è possibile aggiungere la libreria nativa al progetto trascinando e rilasciando la libreria nativa dal Finder nel progetto in Esplora soluzioni oppure facendo clic con il pulsante destro del mouse sul progetto e scegliendo Aggiungi file> per selezionare la libreria nativa. Le librerie native per convenzione iniziano con la parola "lib" e terminano con l'estensione ".a". Quando si esegue questa operazione, Visual Studio per Mac aggiungerà due file: il file con estensione a e un file C# popolato automaticamente che contiene informazioni su ciò che la libreria nativa contiene:

Le librerie native per convenzione iniziano con la parola lib e terminano con l'estensione .a

Il contenuto del libMagicChord.linkwith.cs file contiene informazioni su come può essere usata questa libreria e indica all'IDE di creare il pacchetto binario nel file DLL risultante:

using System;
using ObjCRuntime;

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

Dettagli completi su come usare [LinkWith]l'attributo è documentato nella Guida di riferimento ai tipi di associazione.

A questo punto, quando si compila il progetto, si finirà con un MagicChords.dll file che contiene sia l'associazione che la libreria nativa. È possibile distribuire questo progetto o la DLL risultante ad altri sviluppatori per il proprio uso.

In alcuni casi è possibile che siano necessari alcuni valori di enumerazione, definizioni di delegati o altri tipi. Non inserire tali elementi nel file delle definizioni api, perché si tratta semplicemente di un contratto

File di definizione dell'API

Il file di definizione dell'API è costituito da una serie di interfacce. Le interfacce nella definizione dell'API verranno trasformate in una dichiarazione di classe e devono essere decorate con l'attributo [BaseType] per specificare la classe di base per la classe .

Ci si potrebbe chiedere perché non sono state usate classi anziché interfacce per la definizione del contratto. Sono stati selezionate interfacce perché è stato consentito scrivere il contratto per un metodo senza dover fornire un corpo del metodo nel file di definizione dell'API o dover fornire un corpo che doveva generare un'eccezione o restituire un valore significativo.

Tuttavia, poiché si usa l'interfaccia come scheletro per generare una classe, è necessario ricorrere a decorare varie parti del contratto con attributi per guidare l'associazione.

Metodi di associazione

L'associazione più semplice che è possibile eseguire consiste nell'associare un metodo. È sufficiente dichiarare un metodo nell'interfaccia con le convenzioni di denominazione C# e decorare il metodo con attributo [Export]. L'attributo [Export] è il collegamento tra il nome C# e il Objective-C nome nel runtime di Xamarin.iOS. Parametro dell'oggetto [Export] attribute è il nome del Objective-C selettore. Alcuni esempi:

// 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);

Gli esempi precedenti illustrano come associare i metodi di istanza. Per associare metodi statici, è necessario usare l'attributo [Static] , come illustrato di seguito:

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

Ciò è necessario perché il contratto fa parte di un'interfaccia e le interfacce non hanno alcuna nozione di dichiarazioni statiche e di istanza, pertanto è necessario ricorrere ancora una volta agli attributi. Se si desidera nascondere un metodo specifico dall'associazione, è possibile decorare il metodo con l'attributo [Internal] .

Il btouch-native comando introduce controlli per i parametri di riferimento che non sono Null. Se si desidera consentire valori Null per un parametro specifico, usare [NullAllowed] attributo nel parametro , come illustrato di seguito:

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

Quando si esporta un tipo riferimento, con la [Export] parola chiave è anche possibile specificare la semantica di allocazione. Ciò è necessario per garantire che non vengano persi dati.

Proprietà delle associazioni

Analogamente ai metodi, Objective-C le proprietà vengono associate usando [Export] attributo ed eseguire il mapping direttamente alle proprietà C#. Proprio come i metodi, le proprietà possono essere decorate con il [Static] E la [Internal] Attributi.

Quando si usa l'attributo [Export] in una proprietà sotto le quinte btouch-native associa effettivamente due metodi: il getter e il setter. Il nome specificato per l'esportazione è il nome di base e il setter viene calcolato anteponendo la parola "set", trasformando la prima lettera del nome base in lettere maiuscole e rendendo il selettore accettare un argomento. Ciò significa che [Export ("label")] applicato a una proprietà associa effettivamente i metodi "label" e "setLabel:". Objective-C

A volte le Objective-C proprietà non seguono il modello descritto in precedenza e il nome viene sovrascritto manualmente. In questi casi è possibile controllare la modalità di generazione dell'associazione tramite [Bind] attributo sul getter o setter, ad esempio:

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

Viene quindi associato "isMenuVisible" e "setMenuVisible:". Facoltativamente, una proprietà può essere associata usando la sintassi seguente:

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

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

Dove il getter e il setter sono definiti in modo esplicito come nelle name associazioni e setName precedenti.

Oltre al supporto per le proprietà statiche tramite [Static], è possibile decorare le proprietà statiche del thread con [IsThreadStatic], ad esempio:

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

Proprio come i metodi consentono di contrassegnare alcuni parametri con [NullAllowed], è possibile applicare [NullAllowed] a una proprietà per indicare che null è un valore valido per la proprietà, ad esempio:

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

Il [NullAllowed] parametro può anche essere specificato direttamente nel setter:

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

Avvertenze dell'associazione di controlli personalizzati

Quando si configura l'associazione per un controllo personalizzato, è necessario considerare le avvertenze seguenti:

  1. Le proprietà di associazione devono essere statiche : quando si definisce l'associazione delle proprietà, è necessario usare l'attributo [Static] .
  2. I nomi delle proprietà devono corrispondere esattamente : il nome utilizzato per associare la proprietà deve corrispondere esattamente al nome della proprietà nel controllo personalizzato.
  3. I tipi di proprietà devono corrispondere esattamente : il tipo di variabile usato per associare la proprietà deve corrispondere esattamente al tipo della proprietà nel controllo personalizzato.
  4. Punti di interruzione e getter/setter : i punti di interruzione inseriti nei metodi getter o setter della proprietà non verranno mai raggiunti.
  5. Osservare i callback: è necessario usare i callback di osservazione per ricevere una notifica delle modifiche nei valori delle proprietà dei controlli personalizzati.

Se non si osserva una delle avvertenze sopra elencate, l'associazione ha esito negativo in modo invisibile all'utente in fase di esecuzione.

Objective-C Modello modificabile e proprietà

Objective-C i framework usano un linguaggio in cui alcune classi non sono modificabili con una sottoclasse modificabile. Ad esempio NSString , è la versione non modificabile, mentre NSMutableString è la sottoclasse che consente la mutazione.

In queste classi è comune vedere che la classe base non modificabile contiene proprietà con un getter, ma nessun setter. E per la versione modificabile per introdurre il setter. Poiché ciò non è realmente possibile con C#, è stato necessario eseguire il mapping di questo idioma in un linguaggio che funziona con C#.

Il modo in cui viene eseguito il mapping a C# consiste nell'aggiungere sia il getter che il setter nella classe base, ma contrassegnando il setter con un attributo [NotImplemented].

Quindi, nella sottoclasse modificabile, si usa il [Override] attributo sulla proprietà per assicurarsi che la proprietà stia effettivamente sovrascrivendo il comportamento dell'elemento padre.

Esempio:

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

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

Costruttori di binding

Lo btouch-native strumento genererà automaticamente quattro costruttori nella classe, per una determinata classe Foo, che genera:

  • Foo (): costruttore predefinito (mappa al Objective-Ccostruttore "init")
  • Foo (NSCoder): costruttore usato durante la deserializzazione dei file NIB (esegue il mapping al Objective-Ccostruttore "initWithCoder:".
  • Foo (IntPtr handle): il costruttore per la creazione basata su handle, viene richiamato dal runtime quando il runtime deve esporre un oggetto gestito da un oggetto non gestito.
  • Foo (NSEmptyFlag): viene usato dalle classi derivate per impedire l'inizializzazione doppia.

Per i costruttori definiti, devono essere dichiarati usando la firma seguente all'interno della definizione dell'interfaccia: devono restituire un IntPtr valore e il nome del metodo deve essere Costruttore. Ad esempio, per associare il initWithFrame: costruttore, si tratta di ciò che si userebbe:

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

Protocolli di associazione

Come descritto nel documento di progettazione dell'API, nella sezione che illustra modelli e protocolli, Xamarin.iOS esegue il mapping dei Objective-C protocolli in classi contrassegnate con il attributo [Model]. Questo viene in genere usato quando si implementano Objective-C classi delegate.

La grande differenza tra una classe associata regolare e una classe delegato è che la classe delegato potrebbe avere uno o più metodi facoltativi.

Si consideri ad esempio la UIKit classe UIAccelerometerDelegate, come è associata in Xamarin.iOS:

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

Poiché si tratta di un metodo facoltativo nella definizione per UIAccelerometerDelegate non c'è altro da fare. Tuttavia, se è presente un metodo obbligatorio nel protocollo, è necessario aggiungere [Abstract] attributo al metodo . In questo modo l'utente dell'implementazione fornirà effettivamente un corpo per il metodo .

In generale, i protocolli vengono usati nelle classi che rispondono ai messaggi. Questa operazione viene in genere eseguita in Objective-C assegnando alla proprietà "delegato" un'istanza di un oggetto che risponde ai metodi nel protocollo.

La convenzione in Xamarin.iOS consiste nel supportare sia lo Objective-C stile ad accoppiamento libero in cui qualsiasi istanza di un NSObject oggetto può essere assegnata al delegato, sia per esporre una versione fortemente tipizzata. Per questo motivo, in genere viene fornita sia una Delegate proprietà fortemente tipizzata che un WeakDelegate oggetto tipizzato in modo libero. La versione tipizzata viene in genere associata a [Export]e viene usato l'attributo [Wrap] per fornire la versione fortemente tipizzata.

In questo modo viene illustrato come è stata associata la UIAccelerometer classe :

[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; }
}

Novità di MonoTouch 7.0

A partire da MonoTouch 7.0 è stata incorporata una funzionalità di associazione di protocollo nuova e migliorata. Questo nuovo supporto semplifica l'uso Objective-C di idiomi per l'adozione di uno o più protocolli in una determinata classe.

Per ogni definizione MyProtocol di protocollo in Objective-Cè ora disponibile un'interfaccia IMyProtocol che elenca tutti i metodi richiesti dal protocollo, nonché una classe di estensione che fornisce tutti i metodi facoltativi. L'esempio precedente, combinato con il nuovo supporto nell'editor di Xamarin Studio, consente agli sviluppatori di implementare metodi di protocollo senza dover usare le sottoclassi separate delle classi di modelli astratte precedenti.

Qualsiasi definizione che contiene l'attributo [Protocol] genererà effettivamente tre classi di supporto che migliorano notevolmente il modo in cui si utilizzano i protocolli:

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

L'implementazione della classe fornisce una classe astratta completa che è possibile eseguire l'override dei singoli metodi di e ottenere la sicurezza completa dei tipi. Tuttavia, a causa di C# che non supporta l'ereditarietà multipla, esistono scenari in cui potrebbe essere necessario avere una classe base diversa, ma si vuole comunque implementare un'interfaccia, ovvero la posizione in cui è

Viene fornita la definizione dell'interfaccia generata. Si tratta di un'interfaccia che dispone di tutti i metodi necessari dal protocollo. Ciò consente agli sviluppatori che vogliono implementare il protocollo per implementare semplicemente l'interfaccia . Il runtime registrerà automaticamente il tipo come adozione del protocollo.

Si noti che l'interfaccia elenca solo i metodi richiesti ed espone i metodi facoltativi. Ciò significa che le classi che adottano il protocollo otterranno il controllo completo dei tipi per i metodi richiesti, ma dovranno ricorrere alla digitazione debole (usando [Export] manualmente gli attributi e la corrispondenza della firma) per i metodi di protocollo facoltativi.

Per semplificare l'utilizzo di un'API che usa protocolli, lo strumento di associazione produrrà anche una classe di metodi di estensione che espone tutti i metodi facoltativi. Ciò significa che, purché si stia usando un'API, sarà possibile considerare i protocolli come tutti i metodi.

Se si vogliono usare le definizioni di protocollo nell'API, è necessario scrivere le interfacce vuote nella definizione dell'API. Se si vuole usare MyProtocol in un'API, è necessario eseguire questa operazione:

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

Il codice precedente è necessario perché in fase di associazione l'oggetto IMyProtocol non esiste, ecco perché è necessario fornire un'interfaccia vuota.

Adozione di interfacce generate dal protocollo

Ogni volta che si implementa una delle interfacce generate per i protocolli, come illustrato di seguito:

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

L'implementazione per i metodi di interfaccia necessari viene esportata con il nome corretto, pertanto equivale a questo:

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

Questo funzionerà per tutti i membri del protocollo necessari, ma esiste un caso speciale con selettori facoltativi di cui tenere conto. I membri del protocollo facoltativi vengono trattati in modo identico quando si usa la classe base:

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

ma quando si usa l'interfaccia del protocollo è necessario aggiungere [Export]. L'IDE lo aggiungerà tramite completamento automatico quando lo si aggiunge a partire dall'override.

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

Esiste una leggera differenza di comportamento tra i due in fase di esecuzione.

  • Gli utenti della classe base (NSUrlSessionDownloadDelegate, ad esempio) forniscono tutti i selettori obbligatori e facoltativi, restituendo valori predefiniti ragionevoli.
  • Gli utenti dell'interfaccia (INSUrlSessionDownloadDelegate in esempio) rispondono solo ai selettori esatti forniti.

Alcune classi rare possono comportarsi in modo diverso qui. In quasi tutti i casi, tuttavia, è sicuro usarlo.

Estensioni della classe di associazione

In Objective-C è possibile estendere le classi con nuovi metodi, simili in spirito ai metodi di estensione di C#. Quando uno di questi metodi è presente, è possibile usare [BaseType] attributo per contrassegnare il metodo come destinatario del Objective-C messaggio.

Ad esempio, in Xamarin.iOS sono stati associati i metodi di estensione definiti NSString in quando UIKit vengono importati come metodi in NSStringDrawingExtensions, come illustrato di seguito:

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

Elenchi di argomenti di associazione Objective-C

Objective-C supporta argomenti variadic. Ad esempio:

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

Per richiamare questo metodo da C# è necessario creare una firma simile alla seguente:

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

Questo dichiara il metodo come interno, nascondendo l'API precedente dagli utenti, ma esponendola alla libreria. È quindi possibile scrivere un metodo simile al seguente:

public void AppendWorkers(params Worker[] workers)
{
    if (workers is 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);
}

Campi di associazione

In alcuni casi è necessario accedere ai campi pubblici dichiarati in una libreria.

In genere questi campi contengono stringhe o valori interi a cui è necessario fare riferimento. Vengono comunemente usati come stringa che rappresentano una notifica specifica e come chiavi nei dizionari.

Per associare un campo, aggiungere una proprietà al file di definizione dell'interfaccia e decorare la proprietà con l'attributo [Field] . Questo attributo accetta un parametro: il nome C del simbolo da cercare. Ad esempio:

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

Se si desidera eseguire il wrapping di vari campi in una classe statica che non deriva da NSObject, è possibile usare [Static] attributo nella classe , come illustrato di seguito:

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

Il codice precedente genererà un oggetto LonelyClass che non deriva da NSObject e conterrà un'associazione all'oggetto NSSomeEventNotificationNSString esposto come NSString.

L'attributo [Field] può essere applicato ai tipi di dati seguenti:

  • NSString riferimenti (solo proprietà di sola lettura)
  • NSArray riferimenti (solo proprietà di sola lettura)
  • Int a 32 bit (System.Int32)
  • Ints a 64 bit (System.Int64)
  • Float a 32 bit (System.Single)
  • Float a 64 bit (System.Double)
  • System.Drawing.SizeF
  • CGSize

Oltre al nome del campo nativo, è possibile specificare il nome della libreria in cui si trova il campo passando il nome della libreria:

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

Se si esegue il collegamento statico, non esiste alcuna libreria a cui eseguire l'associazione, quindi è necessario usare il __Internal nome:

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

Enumerazioni di binding

È possibile aggiungere enum direttamente nei file di associazione per semplificare l'uso all'interno delle definizioni API, senza usare un file di origine diverso (che deve essere compilato sia nelle associazioni che nel progetto finale).

Esempio:

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

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

È anche possibile creare enumerazioni personalizzate per sostituire NSString le costanti. In questo caso, il generatore creerà automaticamente i metodi per convertire automaticamente i valori di enumerazione e le costanti NSString.

Esempio:

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

Nell'esempio precedente è possibile decidere di decorare void Perform (NSString mode); con un [Internal] attributo . In questo modo l'API basata su costante verrà nascosta ai consumer di binding.

Tuttavia, questo limiterebbe la sottoclasse del tipo come alternativa api più bella usa un [Wrap] attributo . Questi metodi generati non virtualsono , ad esempio non sarà possibile eseguirne l'override, che potrebbe, o no, essere una scelta valida.

Un'alternativa consiste nel contrassegnare la definizione originale, NSStringbasata su , come [Protected]. Ciò consentirà il funzionamento della sottoclasse, quando necessario e la versione wrap'ed funzionerà comunque e chiamerà il metodo di override.

Associazione NSValuedi , NSNumbere NSString a un tipo migliore

L'attributo consente l'associazione [BindAs] NSNumberdi e NSValue (enumerazioni NSString) in tipi C# più accurati. L'attributo può essere usato per creare un'API .NET più accurata e più accurata tramite l'API nativa.

È possibile decorare i metodi (sul valore restituito), i parametri e le proprietà con [BindAs]. L'unica restrizione è che il membro non deve trovarsi all'interno di un [Protocol] o [Model] interfaccia.

Ad esempio:

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

Restituirebbe:

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

Internamente faremo le bool?conversioni ->NSNumber e CGRect<->NSValue .<

[BindAs] supporta anche matrici di NSNumber NSValue e NSString(enumerazioni).

Ad esempio:

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

Restituirebbe:

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

CAScroll è un'enumerazione NSString supportata, recupereremo il valore corretto NSString e gestiremo la conversione del tipo.

Vedere la [BindAs] documentazione per visualizzare i tipi di conversione supportati.

Notifiche di associazione

Le notifiche sono messaggi inviati a NSNotificationCenter.DefaultCenter e vengono usati come meccanismo per trasmettere messaggi da una parte dell'applicazione a un'altra. Gli sviluppatori sottoscrivono in genere le notifiche usando il metodo AddObserver di NSNotificationCenter. Quando un'applicazione invia un messaggio al Centro notifiche, in genere contiene un payload archiviato nel dizionario NSNotification.UserInfo . Questo dizionario viene digitato in modo debole e le informazioni sono soggette a errori, perché gli utenti in genere devono leggere nella documentazione le chiavi disponibili nel dizionario e i tipi di valori che possono essere archiviati nel dizionario. La presenza di chiavi a volte viene usata anche come valore booleano.

Il generatore di associazioni Xamarin.iOS fornisce supporto per gli sviluppatori per associare le notifiche. A tale scopo, impostare [Notification] attributo su una proprietà contrassegnata anche con un oggetto [Field] proprietà (può essere pubblica o privata).

Questo attributo può essere usato senza argomenti per le notifiche che non contengono payload oppure è possibile specificare un System.Type oggetto che fa riferimento a un'altra interfaccia nella definizione dell'API, in genere con il nome che termina con "EventArgs". Il generatore trasformerà l'interfaccia in una classe che sottoclassi EventArgs e includerà tutte le proprietà elencate. L'attributo [Export] deve essere usato nella classe EventArgs per elencare il nome della chiave usata per cercare il Objective-C dizionario per recuperare il valore.

Ad esempio:

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

Il codice precedente genererà una classe MyClass.Notifications annidata con i metodi seguenti:

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

Gli utenti del codice possono quindi sottoscrivere facilmente le notifiche inviate a NSDefaultCenter usando codice simile al seguente:

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

Il valore restituito da ObserveDidStart può essere usato per interrompere facilmente la ricezione di notifiche, come illustrato di seguito:

token.Dispose ();

In alternativa, è possibile chiamare NSNotification.DefaultCenter.RemoveObserver e passare il token. Se la notifica contiene parametri, è necessario specificare un'interfaccia helper EventArgs , come illustrato di seguito:

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

Il codice precedente genererà una MyScreenChangedEventArgs classe con le ScreenX proprietà e ScreenY che recupererà i dati dal dizionario NSNotification.UserInfo usando rispettivamente le chiavi "ScreenXKey" e "ScreenYKey" e applicherà le conversioni appropriate. L'attributo [ProbePresence] viene usato per il generatore per eseguire il probe se la chiave è impostata in UserInfo, anziché tentare di estrarre il valore. Viene usato per i casi in cui la presenza della chiave è il valore (in genere per i valori booleani).

In questo modo è possibile scrivere codice simile al seguente:

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

Categorie di associazione

Le categorie sono un Objective-C meccanismo usato per estendere il set di metodi e proprietà disponibili in una classe. In pratica, vengono usati per estendere la funzionalità di una classe base (ad esempio ) quando un framework specifico è collegato in (ad esempio NSObjectUIKit), rendendo disponibili i relativi metodi, ma solo se il nuovo framework è collegato. In altri casi, vengono usati per organizzare le funzionalità in una classe in base alle funzionalità. Sono simili ai metodi di estensione C#. Questa è l'aspetto di una categoria in Objective-C:

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

L'esempio precedente se trovato in una libreria estende le istanze di UIView con il metodo makeBackgroundRed.

Per associarli, è possibile usare l'attributo [Category] in una definizione di interfaccia. Quando si usa [Category] attributo, il significato del [BaseType] modifiche dell'attributo da usare per specificare la classe di base da estendere, in modo che sia il tipo da estendere.

Di seguito viene illustrato come le UIView estensioni sono associate e trasformate in metodi di estensione C#:

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

Il codice precedente creerà una MyUIViewExtension classe che contiene il MakeBackgroundRed metodo di estensione. Ciò significa che è ora possibile chiamare "MakeBackgroundRed" in qualsiasi UIView sottoclasse, offrendo la stessa funzionalità che si otterrebbe su Objective-C. In altri casi, le categorie vengono usate non per estendere una classe di sistema, ma per organizzare le funzionalità, esclusivamente a scopo decorativo. nel modo seguente:

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

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

Anche se è possibile usare [Category] attributo anche per questo stile di decorazione di dichiarazioni, è anche possibile aggiungerli tutti alla definizione della classe. Entrambe le operazioni otterrebbero lo stesso risultato:

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

In questi casi è più breve unire le categorie:

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

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

Blocchi di associazione

I blocchi sono un nuovo costrutto introdotto da Apple per portare l'equivalente funzionale dei metodi anonimi C# a Objective-C. Ad esempio, la NSSet classe espone ora questo metodo:

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

La descrizione precedente dichiara un metodo denominato enumerateObjectsUsingBlock: che accetta un argomento denominato block. Questo blocco è simile a un metodo anonimo C# in quanto supporta l'acquisizione dell'ambiente corrente (il puntatore "this", l'accesso a variabili e parametri locali). Il metodo precedente in NSSet richiama il blocco con due parametri ( NSObject la id obj parte) e un puntatore a un valore booleano (la BOOL *stopparte ).

Per associare questo tipo di API con btouch, è prima necessario dichiarare la firma del tipo di blocco come delegato C# e quindi farvi riferimento da un punto di ingresso dell'API, come illustrato di seguito:

// 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)

Ora il codice può chiamare la funzione da 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);
});

È anche possibile usare espressioni lambda se si preferisce:

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

Metodi asincroni

Il generatore di associazioni può trasformare una determinata classe di metodi in metodi asincroni (metodi che restituiscono un oggetto Task o Task<T>).

È possibile usare il [Async] attributo sui metodi che restituiscono void e l'ultimo argomento è un callback. Quando si applica a un metodo, il generatore di associazioni genererà una versione di tale metodo con il suffisso Async. Se il callback non accetta parametri, il valore restituito sarà , Taskse il callback accetta un parametro, il risultato sarà .Task<T> Se il callback accetta più parametri, è necessario impostare ResultType o ResultTypeName per specificare il nome desiderato del tipo generato che conterrà tutte le proprietà.

Esempio:

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

Il codice precedente genererà sia il metodo LoadFile che:

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

Visualizzazione di tipi sicuri per i parametri NSDictionary deboli

In molte posizioni nell'API Objective-C , i parametri vengono passati come API con tipizzati deboli NSDictionary con chiavi e valori specifici, ma questi sono soggetti a errori (è possibile passare chiavi non valide e non ricevere avvisi; è possibile passare valori non validi e non ricevere avvisi) e frustrante da usare perché richiedono più viaggi alla documentazione per cercare i possibili nomi e valori di chiave.

La soluzione consiste nel fornire una versione fortemente tipizzata che fornisce la versione fortemente tipizzata dell'API e dietro le quinte esegue il mapping delle varie chiavi e valori sottostanti.

Ad esempio, se l'API Objective-C ha accettato un NSDictionary oggetto e viene documentato come accettare la chiave XyzVolumeKey che accetta un NSNumber oggetto con un valore di volume compreso tra 0,0 e 1.0 e che XyzCaptionKey accetta una stringa, si vuole che gli utenti abbiano un'API interessante simile alla seguente:

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

La Volume proprietà è definita come float nullable, perché la convenzione in Objective-C non richiede che questi dizionari abbiano il valore, quindi esistono scenari in cui il valore potrebbe non essere impostato.

A tale scopo, è necessario eseguire alcune operazioni:

  • Creare una classe fortemente tipizzata, che sottoclassi DictionaryContainer e fornisce i vari getter e setter per ogni proprietà.
  • Dichiara gli overload per i metodi che accettano NSDictionary la nuova versione fortemente tipizzata.

È possibile creare manualmente la classe fortemente tipizzata oppure usare il generatore per eseguire automaticamente il lavoro. Si esaminerà prima di tutto come eseguire questa operazione manualmente, in modo da comprendere cosa sta succedendo e quindi l'approccio automatico.

È necessario creare un file di supporto per questo, ma non viene inserito nell'API del contratto. Questo è ciò che è necessario scrivere per creare la classe 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
}

È quindi necessario fornire un metodo wrapper che espone l'API di alto livello, oltre all'API di basso livello.

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

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

Se l'API non deve essere sovrascritta, è possibile nascondere in modo sicuro l'API basata su NSDictionary usando attributo [Internal].

Come si può notare, usiamo il [Wrap] attributo per visualizzare un nuovo punto di ingresso DELL'API ed è stato illustrato usando la classe fortemente tipizzata XyzOptions . Il metodo wrapper consente anche di passare null.

Ora, una cosa che non abbiamo menzionato è dove provengono i XyzOptionsKeys valori. In genere si raggruppano le chiavi visualizzate da un'API in una classe statica come XyzOptionsKeys, come illustrato di seguito:

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

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

Esaminiamo il supporto automatico per la creazione di questi dizionari fortemente tipizzato. In questo modo si evita un sacco di boilerplate ed è possibile definire il dizionario direttamente nel contratto API, invece di usare un file esterno.

Per creare un dizionario fortemente tipizzato, introdurre un'interfaccia nell'API e decorarla con l'attributo StrongDictionary . In questo modo, il generatore deve creare una classe con lo stesso nome dell'interfaccia da cui derivare DictionaryContainer e fornirà funzioni di accesso fortemente tipizzate.

L'attributo [StrongDictionary] accetta un parametro, ovvero il nome della classe statica che contiene le chiavi del dizionario. Ogni proprietà dell'interfaccia diventerà quindi una funzione di accesso fortemente tipizzata. Per impostazione predefinita, il codice userà il nome della proprietà con il suffisso "Key" nella classe statica per creare la funzione di accesso.

Ciò significa che la creazione della funzione di accesso fortemente tipizzata non richiede più un file esterno, né la necessità di creare manualmente getter e setter per ogni proprietà, né dover cercare manualmente le chiavi.

L'intera associazione avrà un aspetto simile al seguente:

[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?.Dictionary")]
    void Playback (string fileName, XyzOptions options);
}

Nel caso in cui sia necessario fare riferimento nei XyzOption membri un campo diverso (che non è il nome della proprietà con il suffisso Key), è possibile decorare la proprietà con un [Export] attributo con il nome che si vuole usare.

Mapping dei tipi

Questa sezione illustra come Objective-C viene eseguito il mapping dei tipi ai tipi C#.

Tipi semplici

La tabella seguente illustra come eseguire il Objective-C mapping dei tipi dal mondo CocoaTouch a Xamarin.iOS:

Objective-C nome del tipo Tipo di API unificata Xamarin.iOS
BOOL, GLboolean bool
NSInteger nint
NSUInteger nuint
CFTimeInterval / NSTimeInterval double
NSString (altre informazioni sull'associazione NSString) string
char * string (vedere anche: [PlainString])
CGRect CGRect
CGPoint CGPoint
CGSize CGSize
CGFloat, GLfloat nfloat
Tipi CoreFoundation (CF*) CoreFoundation.CF*
GLint nint
GLfloat nfloat
Tipi di base (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

Matrici

Il runtime di Xamarin.iOS si occupa automaticamente della conversione delle matrici C# in NSArrays e dell'esecuzione della conversione, quindi, ad esempio, il metodo immaginario Objective-C che restituisce un NSArray di UIViews:

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

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

È associato come segue:

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

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

L'idea consiste nell'usare una matrice C# fortemente tipizzata, in quanto ciò consentirà all'IDE di fornire il completamento corretto del codice con il tipo effettivo senza forzare l'utente a indovinare o cercare la documentazione per individuare il tipo effettivo degli oggetti contenuti nella matrice.

Nei casi in cui non è possibile rilevare il tipo più derivato effettivo contenuto nella matrice, è possibile usare NSObject [] come valore restituito.

Selettori

I selettori vengono visualizzati nell'API Objective-C come tipo SELspeciale . Quando si associa un selettore, si esegue il mapping del tipo a ObjCRuntime.Selector. In genere, i selettori vengono esposti in un'API con un oggetto, l'oggetto di destinazione e un selettore da richiamare nell'oggetto di destinazione. Fornire entrambi questi elementi corrisponde fondamentalmente al delegato C#: un elemento che incapsula sia il metodo da richiamare che l'oggetto in cui richiamare il metodo.

Questo è l'aspetto dell'associazione:

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

Questo è il modo in cui il metodo viene in genere usato in un'applicazione:

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

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

Per rendere più adatta l'associazione agli sviluppatori C#, in genere si fornirà un metodo che accetta un NSAction parametro, che consente l'uso di delegati e espressioni lambda C# anziché .Target+Selector A tale scopo, in genere si nasconde il SetTarget metodo contrassegnandolo con un [Internal] attributo e quindi si espone un nuovo metodo helper, come illustrato di seguito:

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

Ora il codice utente può essere scritto come segue:

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

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

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

Stringhe

Quando si associa un metodo che accetta , NSStringè possibile sostituirlo con un tipo stringa C#, sia per i tipi restituiti che per i parametri.

L'unico caso in cui si potrebbe voler usare direttamente è NSString quando la stringa viene usata come token. Per altre informazioni sulle stringhe e NSString, vedere il documento Progettazione API in NSString .

In alcuni casi rari, un'API potrebbe esporre una stringa simile a C (char *) anziché una Objective-C stringa (NSString *). In questi casi, è possibile annotare il parametro con attributo [PlainString].

parametri out/ref

Alcune API restituiscono valori nei relativi parametri o passano parametri per riferimento.

In genere la firma è simile alla seguente:

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

Il primo esempio mostra un linguaggio comune Objective-C per restituire i codici di errore, un puntatore a un puntatore a un NSError puntatore e, al momento della restituzione, viene impostato il valore. Il secondo metodo mostra come un Objective-C metodo può accettare un oggetto e modificarne il contenuto. Si tratta di un passaggio per riferimento, anziché di un valore di output puro.

Il binding sarà simile al seguente:

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

Attributi di gestione della memoria

Quando si usa l'attributo [Export] e si passano dati che verranno conservati dal metodo chiamato, è possibile specificare la semantica dell'argomento passandola come secondo parametro, ad esempio:

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

L'oggetto precedente contrassegna il valore come con la semantica "Mantieni". La semantica disponibile è:

  • Assegnazione
  • Copia
  • Mantenere

Linee guida per lo stile

Uso di [interno]

È possibile usare il [Internal] attributo per nascondere un metodo dall'API pubblica. È possibile eseguire questa operazione nei casi in cui l'API esposta è troppo bassa e si vuole fornire un'implementazione di alto livello in un file separato basato su questo metodo.

È anche possibile usarlo quando si verificano limitazioni nel generatore di associazioni, ad esempio alcuni scenari avanzati potrebbero esporre tipi non associati e si vuole eseguire il binding in modo personalizzato e si vuole eseguire il wrapping di tali tipi in modo personalizzato.

Gestori eventi e callback

Objective-C le classi in genere trasmettono notifiche o richiedono informazioni inviando un messaggio su una classe delegato (Objective-C delegato).

Questo modello, sebbene completamente supportato e risolto da Xamarin.iOS può talvolta risultare complesso. Xamarin.iOS espone il modello di evento C# e un sistema di callback del metodo nella classe che può essere usata in queste situazioni. In questo modo è possibile eseguire codice simile al seguente:

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

Il generatore di associazioni è in grado di ridurre la quantità di tipizzazione necessaria per eseguire il mapping del Objective-C modello nel modello C#.

A partire da Xamarin.iOS 1.4 sarà anche possibile indicare al generatore di produrre associazioni per un delegato specifico Objective-C ed esporre il delegato come eventi e proprietà C# nel tipo host.

Esistono due classi coinvolte in questo processo, la classe host che sarà quella che attualmente genera eventi e li invia nella Delegate classe delegato o WeakDelegate e nella classe delegato effettiva.

Considerando la configurazione seguente:

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

Per eseguire il wrapping della classe è necessario:

  • Nella classe host aggiungere al [BaseType]
    dichiarazione del tipo che funge da delegato e del nome C# esposto. Nell'esempio precedente sono typeof (MyClassDelegate) e WeakDelegate rispettivamente.
  • Nella classe del delegato, in ogni metodo con più di due parametri, è necessario specificare il tipo da usare per la classe EventArgs generata automaticamente.

Il generatore di associazioni non è limitato al wrapping solo di una singola destinazione evento, è possibile che alcune Objective-C classi emettano messaggi a più delegati, quindi è necessario fornire matrici per supportare questa configurazione. La maggior parte delle configurazioni non sarà necessaria, ma il generatore è pronto per supportare tali casi.

Il codice risultante sarà:

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

Viene EventArgs utilizzato per specificare il nome della EventArgs classe da generare. È consigliabile usare uno per firma (in questo esempio, EventArgs conterrà una With proprietà di tipo nint).

Con le definizioni precedenti, il generatore produrrà l'evento seguente nella classe MyClass generata:

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

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

È quindi possibile usare il codice come segue:

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

I callback sono esattamente come le chiamate agli eventi, la differenza è che invece di avere più sottoscrittori potenziali (ad esempio, più metodi possono eseguire l'hook in un Clicked evento o un DownloadFinished evento) i callback possono avere un solo sottoscrittore.

Il processo è identico, l'unica differenza è che invece di esporre il nome della EventArgs classe che verrà generata, eventArgs viene effettivamente usato per assegnare un nome al delegato C# risultante.

Se il metodo nella classe delegate restituisce un valore, il generatore di associazioni eseguirà il mapping di questo oggetto in un metodo delegato nella classe padre anziché in un evento. In questi casi è necessario specificare il valore predefinito che deve essere restituito dal metodo se l'utente non si collega al delegato. A tale scopo, usare [DefaultValue] o [DefaultValueFromArgument] attributi.

[DefaultValue] hardcodederà un valore restituito, mentre [DefaultValueFromArgument] viene utilizzato per specificare quale argomento di input verrà restituito.

Enumerazioni e tipi di base

È anche possibile fare riferimento a enumerazioni o tipi di base non direttamente supportati dal sistema di definizione dell'interfaccia btouch. A tale scopo, inserire le enumerazioni e i tipi di core in un file separato e includerlo come parte di uno dei file aggiuntivi forniti per il ritocco.

Collegamento delle dipendenze

Se si associano API che non fanno parte dell'applicazione, è necessario assicurarsi che il file eseguibile sia collegato a queste librerie.

È necessario informare Xamarin.iOS su come collegare le librerie, questa operazione può essere eseguita modificando la configurazione della compilazione per richiamare il mtouch comando con alcuni argomenti di compilazione aggiuntivi che specificano come collegare le nuove librerie usando l'opzione "-gcc_flags", seguita da una stringa tra virgolette che contiene tutte le librerie aggiuntive necessarie per il programma. Così:

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

L'esempio precedente collega libMyLibrary.ae libSystemLibrary.dylib la libreria framework all'eseguibile CFNetwork finale.

In alternativa, è possibile sfruttare il livello [LinkWithAttribute]di assembly , che è possibile incorporare nei file di contratto , ad esempio AssemblyInfo.cs. Quando si usa [LinkWithAttribute], sarà necessario avere la libreria nativa disponibile al momento dell'associazione, in quanto incorporare la libreria nativa con l'applicazione. Ad esempio:

// 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)]

Ci si potrebbe chiedere, perché è necessario -force_load il comando e il motivo è che il flag -ObjC anche se compila il codice in, non mantiene i metadati necessari per supportare le categorie (l'eliminazione del linker/compilatore dead code strips) che è necessario in fase di esecuzione per Xamarin.iOS.

Riferimenti assistito

Alcuni oggetti temporanei come fogli di azione e caselle di avviso sono complessi da tenere traccia per gli sviluppatori e il generatore di associazioni può aiutare un po 'qui.

Ad esempio, se si dispone di una classe che mostrava un messaggio e quindi generava un Done evento, il modo tradizionale di gestire questo comportamento sarebbe:

class Demo {
    MessageBox box;

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

Nello scenario precedente lo sviluppatore deve mantenere il riferimento all'oggetto stesso e perdere o cancellare attivamente il riferimento per box autonomamente. Mentre il codice di associazione, il generatore supporta tenere traccia del riferimento e cancellarlo quando viene richiamato un metodo speciale, il codice precedente diventa quindi:

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

Si noti che non è più necessario mantenere la variabile in un'istanza di , che funziona con una variabile locale e che non è necessario cancellare il riferimento quando l'oggetto muore.

Per sfruttare questo vantaggio, la classe deve avere una proprietà Events impostata nella [BaseType] dichiarazione e anche la KeepUntilRef variabile impostata sul nome del metodo richiamato quando l'oggetto ha completato il suo lavoro, come illustrato di seguito:

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

Ereditarietà dei protocolli

A partire da Xamarin.iOS v3.2, è supportato l'ereditarietà dai protocolli contrassegnati con la [Model] proprietà . Ciò è utile in determinati modelli DI API, ad esempio in MapKit cui il MKOverlay protocollo eredita dal MKAnnotation protocollo e viene adottato da una serie di classi che ereditano da NSObject.

Storicamente era necessario copiare il protocollo in ogni implementazione, ma in questi casi ora è possibile avere la MKShape classe eredita dal MKOverlay protocollo e genererà automaticamente tutti i metodi necessari.