Eventos, protocolos y delegados en Xamarin.iOS

Xamarin.iOS usa controles para exponer eventos para la mayoría de las interacciones del usuario. Las aplicaciones de Xamarin.iOS consumen estos eventos de la misma manera que las aplicaciones .NET tradicionales. Por ejemplo, la clase UIButton de Xamarin.iOS tiene un evento denominado TouchUpInside y consume este evento igual que si esta clase y evento estuvieran en una aplicación .NET.

Además de este enfoque de .NET, Xamarin.iOS expone otro modelo que se puede usar para interacciones y enlaces de datos más complejos. Esta metodología usa lo que Apple llama delegados y protocolos. Los delegados son similares en concepto a los delegados de C#, pero en lugar de definir y llamar a un único método, un delegado de Objective-C es una clase completa que se ajusta a un protocolo. Un protocolo es similar a una interfaz en C#, salvo que sus métodos pueden ser opcionales. Por ejemplo, para rellenar UITableView con datos, crearía una clase de delegado que implementa los métodos definidos en el protocolo UITableViewDataSource al que UITableView llamaría para rellenarse.

En este artículo aprenderá sobre todos estos temas, de forma que tenga una base sólida para controlar escenarios de devolución de llamada en Xamarin.iOS, entre los que se incluyen:

  • Eventos: uso de eventos de .NET con controles UIKit.
  • Protocolos: aprender qué protocolos son y cómo se usan y crear un ejemplo que proporcione datos para una anotación de mapa.
  • Delegados: aprender sobre los delegados de Objective-C extendiendo el ejemplo de mapa para controlar la interacción del usuario que incluye una anotación y, a continuación, aprender la diferencia entre delegados fuertes y débiles y cuándo usar cada uno de ellos.

Para ilustrar los protocolos y los delegados, crearemos una aplicación de mapa sencilla que agrega una anotación a un mapa, como se muestra aquí:

An example of a simple map application that adds an annotation to a mapAn example annotation added to a map

Antes de abordar esta aplicación, vamos a empezar examinando los eventos de .NET en UIKit.

Eventos de .NET con UIKit

Xamarin.iOS expone eventos de .NET en controles UIKit. Por ejemplo, UIButton tiene un evento TouchUpInside, que se controla como lo haría normalmente en .NET, como se muestra en el código siguiente que usa una expresión lambda de C#:

aButton.TouchUpInside += (o,s) => {
    Console.WriteLine("button touched");
};

También puede implementar esto con un método anónimo de estilo C# 2.0 como este:

aButton.TouchUpInside += delegate {
    Console.WriteLine ("button touched");
};

El código anterior se conecta en el método ViewDidLoad de UIViewController. La variable aButton hace referencia a un botón, que puede agregar en Interface Builder de Xcode o con código.

Xamarin.iOS también admite el estilo destino-acción para conectar el código a una interacción que se produce con un control.

Para más información sobre el patrón destino-acción de iOS, consulte la sección Destino-acción de Competencias básicas de aplicaciones en iOS en la biblioteca de desarrolladores de iOS de Apple.

Para obtener más información, consulte Diseño de interfaces de usuario con Xcode.

Eventos

Si desea interceptar eventos de UIControl, tiene una variedad de opciones: desde el uso de las funciones lambdas y de delegado de C# hasta el uso de API Objective-C de bajo nivel.

En la sección siguiente se muestra cómo capturar el evento TouchDown en un botón, en función de la cantidad de control que necesite.

Estilo C#

Uso de la sintaxis de delegado:

UIButton button = MakeTheButton ();
button.TouchDown += delegate {
    Console.WriteLine ("Touched");
};

Si en cambio le gustan las expresiones lambda:

button.TouchDown += () => {
   Console.WriteLine ("Touched");
};

Si desea tener varios botones, use el mismo controlador para compartir el mismo código:

void handler (object sender, EventArgs args)
{
   if (sender == button1)
      Console.WriteLine ("button1");
   else
      Console.WriteLine ("some other button");
}

button1.TouchDown += handler;
button2.TouchDown += handler;

Supervisión de más de un tipo de evento

Los eventos de C# de las marcas UIControlEvent tienen una correspondencia uno a uno con las marcas individuales. Cuando desee que el mismo fragmento de código controle dos o más eventos, use el método UIControl.AddTarget:

button.AddTarget (handler, UIControlEvent.TouchDown | UIControlEvent.TouchCancel);

Uso de la sintaxis lambda:

button.AddTarget ((sender, event)=> Console.WriteLine ("An event happened"), UIControlEvent.TouchDown | UIControlEvent.TouchCancel);

Si necesita usar características de bajo nivel de Objective-C, como conectar con una instancia de objeto determinada e invocar un selector determinado:

[Export ("MySelector")]
void MyObjectiveCHandler ()
{
    Console.WriteLine ("Hello!");
}

// In some other place:

button.AddTarget (this, new Selector ("MySelector"), UIControlEvent.TouchDown);

Tenga en cuenta que, si implementa el método de instancia en una clase base heredada, debe ser un método público.

Protocolos

Un protocolo es una característica de lenguaje de Objective-C que proporciona una lista de declaraciones de método. Sirve para un propósito similar a una interfaz en C#, la principal diferencia es que un protocolo puede tener métodos opcionales. No se llama a métodos opcionales si la clase que adopta un protocolo no las implementa. Además, una sola clase de Objective-C puede implementar varios protocolos, al igual que una clase de C# puede implementar varias interfaces.

Apple usa protocolos en todo iOS para definir contratos para las clases que se adoptan, a la vez que abstrae la clase que se implementa del autor de la llamada, de forma que funciona como una interfaz de C#. Los protocolos se usan en escenarios que no son de delegados (por ejemplo, con el MKAnnotation ejemplo que se muestra a continuación) y con delegados (como se presenta más adelante en este documento, en la sección Delegados).

Protocolos con Xamarin.iOS

Echemos un vistazo a un ejemplo en el que se usa un protocolo Objective-C de Xamarin.iOS. En este ejemplo, se usará el protocolo MKAnnotation, que forma parte del marco MapKit. MKAnnotation es un protocolo que permite que cualquier objeto que lo adopte proporcione información sobre una anotación que se puede agregar a un mapa. Por ejemplo, un objeto que implementa MKAnnotation proporciona la ubicación de la anotación y el título asociado a ella.

De esta manera, el protocolo MKAnnotation se usa para proporcionar datos pertinentes que acompañan a una anotación. La vista real de la propia anotación se crea a partir de los datos del objeto que adopta el protocolo MKAnnotation. Por ejemplo, el texto de la llamada que aparece cuando el usuario pulsa la anotación (como se muestra en la captura de pantalla siguiente) procede de la propiedad Title de la clase que implementa el protocolo:

Example text for the callout when the user taps on the annotation

Como se describe en la sección siguiente, Análisis en profundidad de los protocolos, Xamarin.iOS enlaza protocolos a clases abstractas. Para el protocolo MKAnnotation, la clase de C# enlazada se denomina MKAnnotation para imitar el nombre del protocolo y es una subclase de NSObject, la clase base raíz de CocoaTouch. El protocolo requiere que se implemente un captador y un establecedor para la coordenada; sin embargo, el título y el subtítulo son opcionales. Por lo tanto, en la clase MKAnnotation, la propiedad Coordinate es abstracta, lo que requiere que se implemente y las propiedades Title y Subtitle están marcadas como virtuales, lo que las convierte en opcionales, como se muestra a continuación:

[Register ("MKAnnotation"), Model ]
public abstract class MKAnnotation : NSObject
{
    public abstract CLLocationCoordinate2D Coordinate
    {
        [Export ("coordinate")]
        get;
        [Export ("setCoordinate:")]
        set;
    }

    public virtual string Title
    {
        [Export ("title")]
        get
        {
            throw new ModelNotImplementedException ();
        }
    }

    public virtual string Subtitle
    {
        [Export ("subtitle")]
        get
        {
            throw new ModelNotImplementedException ();
        }
    }
...
}

Cualquier clase puede proporcionar datos de anotación simplemente derivando de MKAnnotation, siempre y cuando se implemente al menos la propiedad Coordinate. Por ejemplo, esta es una clase de ejemplo que toma la coordenada en el constructor y devuelve una cadena para el título:

/// <summary>
/// Annotation class that subclasses MKAnnotation abstract class
/// MKAnnotation is bound by Xamarin.iOS to the MKAnnotation protocol
/// </summary>
public class SampleMapAnnotation : MKAnnotation
{
    string title;

    public SampleMapAnnotation (CLLocationCoordinate2D coordinate)
    {
        Coordinate = coordinate;
        title = "Sample";
    }

    public override CLLocationCoordinate2D Coordinate { get; set; }

    public override string Title {
        get {
            return title;
        }
    }
}

Mediante el protocolo al que está enlazada, cualquier clase que herede de la clase MKAnnotation puede proporcionar datos relevantes que se usarán en el mapa cuando se cree la vista de la anotación. Para agregar una anotación a un mapa, simplemente llame al método AddAnnotation de una instancia MKMapView, como se muestra en el código siguiente:

//an arbitrary coordinate used for demonstration here
var sampleCoordinate =
    new CLLocationCoordinate2D (42.3467512, -71.0969456); // Boston

//create an annotation and add it to the map
map.AddAnnotation (new SampleMapAnnotation (sampleCoordinate));

La variable de mapa aquí es una instancia de MKMapView, que es la clase que representa el propio mapa. MKMapView usará los datos de Coordinate derivados de la instancia SampleMapAnnotation para colocar la vista de anotación en el mapa.

El protocolo MKAnnotation proporciona un conjunto conocido de funcionalidades en todos los objetos que lo implementan, sin que el consumidor (el mapa en este caso) necesite conocer los detalles de implementación. Esto simplifica la adición de una variedad de anotaciones posibles a un mapa.

Análisis en profundidad de los protocolos

Dado que las interfaces de C# no admiten métodos opcionales, Xamarin.iOS asigna protocolos a clases abstractas. Por lo tanto, la adopción de un protocolo en Objective-C se realiza en Xamarin.iOS derivando de la clase abstracta que está enlazada al protocolo e implementando los métodos necesarios. Estos métodos se exponen como métodos abstractos en la clase. Los métodos opcionales del protocolo se enlazarán a métodos virtuales de la clase de C#.

Por ejemplo, esta es una parte del protocolo UITableViewDataSource como enlazada en Xamarin.iOS:

public abstract class UITableViewDataSource : NSObject
{
    [Export ("tableView:cellForRowAtIndexPath:")]
    public abstract UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath);
    [Export ("numberOfSectionsInTableView:")]
    public virtual int NumberOfSections (UITableView tableView){...}
...
}

Tenga en cuenta que la clase es abstracta. Xamarin.iOS hace que la clase sea abstracta para admitir métodos opcionales u obligatorios en los protocolos. Sin embargo, a diferencia de los protocolos Objective-C (o interfaces de C#), las clases de C# no admite herencia múltiple. Esto afecta al diseño del código de C# que usa protocolos y normalmente conduce a clases anidadas. Más adelante en este documento se trata este tema con más detalle, en la sección Delegados.

GetCell(…) es un método abstracto, enlazado al Objective-Cselector,tableView:cellForRowAtIndexPath:, que es un método obligatorio del protocolo UITableViewDataSource. Selector es el término de Objective-C con el que se conoce al método. Para aplicar el método según sea necesario, Xamarin.iOS lo declara como abstracto. El otro método, NumberOfSections(…), está enlazado a numberOfSectionsInTableview:. Este método es opcional en el protocolo, por lo que Xamarin.iOS lo declara como virtual, lo que lo convierte en opcional para invalidar en C#.

Xamarin.iOS se encarga de todos los enlaces de iOS automáticamente. Sin embargo, si alguna vez necesita enlazar un protocolo manualmente desde Objective-C, puede hacerlo decorando una clase con ExportAttribute. Este es el mismo método que usa el propio Xamarin.iOS.

Para más información sobre cómo enlazar tipos de Objective-C en Xamarin.iOS, consulte el artículo Enlace de tipos de Objective-C.

Aún no hemos terminado con los protocolos. También se usan en iOS como base para los delegados de Objective-C, que es el tema de la sección siguiente.

Delegados

iOS usa delegados de Objective-C para implementar el patrón de delegación, en el que un objeto pasa trabajo a otro. El objeto que realiza el trabajo es el delegado del primer objeto. Un objeto indica a su delegado que realice el trabajo mediante el envío de mensajes después de que ocurran ciertas cosas. Enviar un mensaje como este en Objective-C es funcionalmente equivalente a llamar a un método en C#. Un delegado implementa métodos en respuesta a estas llamadas y, por tanto, proporciona funcionalidad a la aplicación.

Los delegados permiten extender el comportamiento de las clases sin necesidad de crear subclases. Las aplicaciones de iOS suelen usar delegados cuando una clase llama a otra después de que se produzca una acción importante. Por ejemplo, la clase MKMapView llama de nuevo a su delegado cuando el usuario pulsa una anotación en un mapa, lo que proporciona al autor de la clase de delegado la oportunidad de responder dentro de la aplicación. Puede trabajar con un ejemplo de este tipo de uso de delegado más adelante en este artículo, en ”Ejemplo de uso de un delegado con Xamarin.iOS”.

En este punto, es posible que se pregunte cómo una clase determina qué métodos llamar en su delegado. Este es otro lugar donde se usan protocolos. Normalmente, los métodos disponibles para un delegado proceden de los protocolos que adoptan.

Uso de los protocolos con delegados

Hemos visto anteriormente cómo se usan los protocolos para permitir la adición de anotaciones a un mapa. Los protocolos también se usan para proporcionar un conjunto conocido de métodos a los que las clases llamen después de que se produzcan determinados eventos, como después de que el usuario pulse una anotación en un mapa o seleccione una celda de una tabla. Las clases que implementan estos métodos se conocen como delegados de las clases que las llaman.

Las clases que admiten la delegación lo hacen exponiendo una propiedad Delegate a la que se asigna una clase que implementa el delegado. Los métodos que implemente para el delegado dependerán del protocolo que adopte el delegado concreto. Si para el método UITableView implementa el protocolo UITableViewDelegate, para el método UIAccelerometer implementaría UIAccelerometerDelegate, y así sucesivamente con otras clases de iOS en las que desee exponer un delegado.

La clase MKMapView que vimos en nuestro ejemplo anterior también tiene una propiedad denominada Delegate, a la que llamará después de que se produzcan varios eventos. El delegado de MKMapView es de tipo MKMapViewDelegate. Lo usará en breve en un ejemplo para responder a la anotación después de seleccionarla, pero primero vamos a ver la diferencia entre delegados fuertes y débiles.

Delegados fuertes frente a débiles

Los delegados que hemos visto hasta ahora son delegados fuertes, lo que significa que están fuertemente tipados. Los enlaces de Xamarin.iOS se envían con una clase fuertemente tipada para cada protocolo de delegado de iOS. Sin embargo, iOS también tiene el concepto de delegado débil. En lugar de heredar de una clase enlazada al protocolo Objective-C en un delegado en particular, en iOS también puede elegir enlazar los métodos del protocolo usted mismo en cualquier clase que desee que derive de NSObject. Para ello, decore los métodos con el atributo ExportAttribute y, luego, proporcione los selectores apropiados. Al adoptar este enfoque, asigna una instancia de la clase a la propiedad WeakDelegate en lugar de a la propiedad Delegate. Un delegado débil le ofrece la flexibilidad de llevar la clase de delegado hasta una jerarquía de herencia diferente. Echemos un vistazo a un ejemplo de Xamarin.iOS que usa delegados fuertes y débiles.

Ejemplo de uso de un delegado con Xamarin.iOS

Para ejecutar código en respuesta al usuario que pulsa la anotación en nuestro ejemplo, podemos heredar de MKMapViewDelegate y asignar una instancia a la propiedad Delegate de MKMapView. El protocolo MKMapViewDelegate solo contiene métodos opcionales. Por lo tanto, todos los métodos que están enlazados a este protocolo en la clase MKMapViewDelegate de Xamarin.iOS son virtuales. Cuando el usuario selecciona una anotación, la instancia de MKMapView enviará el mensaje mapView:didSelectAnnotationView: a su delegado. Para controlar esto en Xamarin.iOS, es necesario invalidar el método DidSelectAnnotationView (MKMapView mapView, MKAnnotationView annotationView) en la subclase MKMapViewDelegate de la siguiente manera:

public class SampleMapDelegate : MKMapViewDelegate
{
    public override void DidSelectAnnotationView (
        MKMapView mapView, MKAnnotationView annotationView)
    {
        var sampleAnnotation =
            annotationView.Annotation as SampleMapAnnotation;

        if (sampleAnnotation != null) {

            //demo accessing the coordinate of the selected annotation to
            //zoom in on it
            mapView.Region = MKCoordinateRegion.FromDistance(
                sampleAnnotation.Coordinate, 500, 500);

            //demo accessing the title of the selected annotation
            Console.WriteLine ("{0} was tapped", sampleAnnotation.Title);
        }
    }
}

La clase SampleMapDelegate mostrada anteriormente se implementa como una clase anidada en el controlador que contiene la instancia de MKMapView. En Objective-C, a menudo verá que el controlador adopta varios protocolos directamente dentro de la clase. Sin embargo, dado que los protocolos están enlazados a clases de Xamarin.iOS, las clases que implementan delegados fuertemente tipados normalmente se incluyen como clases anidadas.

Con la implementación de la clase de delegado ya realizada, solo tiene que crear una instancia del delegado en el controlador y asignarle la propiedad Delegate de MKMapView, como se muestra aquí:

public partial class Protocols_Delegates_EventsViewController : UIViewController
{
    SampleMapDelegate _mapDelegate;
    ...
    public override void ViewDidLoad ()
    {
        base.ViewDidLoad ();

        //set the map's delegate
        _mapDelegate = new SampleMapDelegate ();
        map.Delegate = _mapDelegate;
        ...
    }
    class SampleMapDelegate : MKMapViewDelegate
    {
        ...
    }
}

Para usar un delegado débil para lograr lo mismo, debe enlazar el método usted mismo en cualquier clase que derive de NSObject y asignarlo a la propiedad WeakDelegate de MKMapView. Puesto que la clase UIViewController deriva finalmente de NSObject (como todas las clases Objective-C de CocoaTouch), simplemente podemos implementar un método enlazado a mapView:didSelectAnnotationView: directamente en el controlador y asignar el controlador a WeakDelegate de MKMapView, lo que evita la necesidad de la clase anidada adicional. El código siguiente muestra este enfoque:

public partial class Protocols_Delegates_EventsViewController : UIViewController
{
    ...
    public override void ViewDidLoad ()
    {
        base.ViewDidLoad ();
        //assign the controller directly to the weak delegate
        map.WeakDelegate = this;
    }
    //bind to the Objective-C selector mapView:didSelectAnnotationView:
    [Export("mapView:didSelectAnnotationView:")]
    public void DidSelectAnnotationView (MKMapView mapView,
        MKAnnotationView annotationView)
    {
        ...
    }
}

Al ejecutar este código, la aplicación se comporta exactamente igual que cuando se ejecuta la versión del delegado fuertemente tipada. La ventaja de este código es que el delegado débil no requiere la creación de la clase adicional que se creó cuando usamos el delegado fuertemente tipado. Sin embargo, esto se hace a expensas de la seguridad de tipos. Si fuera a cometer un error en el selector que se pasó a ExportAttribute, no lo descubriría hasta el momento de la ejecución.

Eventos y delegados

Los delegados se usan con las devoluciones de llamada en iOS de forma similar a como se usan los eventos en .NET. Para que las API de iOS y la forma en que usan delegados de Objective-C se parezcan más a .NET, Xamarin.iOS expone eventos de .NET en muchos lugares donde se usan delegados en iOS.

Por ejemplo, la implementación anterior en la que MKMapViewDelegate respondió a una anotación seleccionada también se podría implementar en Xamarin.iOS mediante un evento de .NET. En ese caso, el evento se definiría en MKMapView y se llamaría DidSelectAnnotationView. Tendría una subclase EventArgs de tipo MKMapViewAnnotationEventsArgs. La propiedad View de MKMapViewAnnotationEventsArgs le proporcionaría una referencia a la vista de la anotación, desde la que podría continuar con la misma implementación que tenía anteriormente, como se muestra aquí:

map.DidSelectAnnotationView += (s,e) => {
    var sampleAnnotation = e.View.Annotation as SampleMapAnnotation;
    if (sampleAnnotation != null) {
        //demo accessing the coordinate of the selected annotation to
        //zoom in on it
        mapView.Region = MKCoordinateRegion.FromDistance (
            sampleAnnotation.Coordinate, 500, 500);

        //demo accessing the title of the selected annotation
        Console.WriteLine ("{0} was tapped", sampleAnnotation.Title);
    }
};

Resumen

En este artículo se describe cómo usar eventos, protocolos y delegados en Xamarin.iOS. Hemos visto cómo Xamarin.iOS expone eventos de estilo .NET normales para los controles. A continuación, hemos aprendido sobre los protocolos de Objective-C, entre otras cosas, cómo difieren de las interfaces de C# y cómo se usan en Xamarin.iOS. Por último, hemos examinado delegados de Objective-C desde una perspectiva de Xamarin.iOS. Hemos visto cómo Xamarin.iOS admite delegados tanto débil como fuertemente tipados, y cómo enlazar eventos .NET a métodos de delegado.