Eventos, protocolos e delegados no Xamarin.iOS

O Xamarin.iOS usa controles para expor eventos para a maioria das interações do usuário. Os aplicativos Xamarin.iOS consomem esses eventos da mesma forma que os aplicativos .NET tradicionais. Por exemplo, a classe UIButton do Xamarin.iOS tem um evento chamado TouchUpInside e consome esse evento como se essa classe e evento estivessem em um aplicativo .NET.

Além dessa abordagem do .NET, o Xamarin.iOS expõe outro modelo que pode ser usado para interação e associação de dados mais complexas. Essa metodologia usa o que a Apple chama de delegados e protocolos. Os delegados são semelhantes em conceito aos delegados em C#, mas em vez de definir e chamar um único método, um delegado em Objective-C é uma classe inteira que está em conformidade com um protocolo. Um protocolo é semelhante a uma interface em C#, exceto que seus métodos podem ser opcionais. Portanto, por exemplo, para preencher um UITableView com dados, você criaria uma classe delegada que implementa os métodos definidos no protocolo UITableViewDataSource que o UITableView chamaria para se popular.

Neste artigo, você aprenderá sobre todos esses tópicos, fornecendo uma base sólida para lidar com cenários de retorno de chamada no Xamarin.iOS, incluindo:

  • Eventos – usando eventos .NET com controles UIKit.
  • Protocolos – aprender quais são os protocolos e como eles são usados e criar um exemplo que fornece dados para uma anotação de mapa.
  • Delegados – aprendendo sobre Objective-C delegados estendendo o exemplo de mapa para lidar com a interação do usuário que inclui uma anotação e, em seguida, aprendendo a diferença entre delegados fortes e fracos e quando usar cada um deles.

Para ilustrar protocolos e delegados, criaremos um aplicativo de mapa simples que adiciona uma anotação a um mapa, conforme mostrado aqui:

Um exemplo de um aplicativo de mapa simples que adiciona uma anotação a um mapaUma anotação de exemplo adicionada a um mapa

Antes de lidar com esse aplicativo, vamos começar examinando os eventos do .NET no UIKit.

Eventos do .NET com UIKit

O Xamarin.iOS expõe eventos .NET em controles UIKit. Por exemplo, UIButton tem um evento TouchUpInside, que você manipula como faria normalmente no .NET, conforme mostrado no código a seguir que usa uma expressão lambda C#:

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

Você também pode implementar isso com um método anônimo no estilo C# 2.0 como este:

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

O código anterior é conectado no ViewDidLoad método do UIViewController. A aButton variável faz referência a um botão, que você pode adicionar no Construtor de Interface xcode ou com código.

O Xamarin.iOS também dá suporte ao estilo de ação de destino de conectar seu código a uma interação que ocorre com um controle .

Para obter mais detalhes sobre o padrão de ação de destino do iOS, consulte a seção Target-Action de Competências de Aplicativos Principais para iOS na Biblioteca de Desenvolvedores do iOS da Apple.

Para obter mais informações, consulte Criando interfaces do usuário com Xcode.

Eventos

Se você quiser interceptar eventos de UIControl, terá uma variedade de opções: desde usar as funções lambdas e delegar do C# até o uso das APIs de baixo nível Objective-C .

A seção a seguir mostra como você capturaria o evento TouchDown em um botão, dependendo de quanto controle você precisa.

Estilo C#

Usando a sintaxe de delegado:

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

Se você gosta de lambdas em vez disso:

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

Se você quiser ter vários botões, use o mesmo manipulador para compartilhar o mesmo 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;

Monitorando mais de um tipo de evento

Os eventos C# para sinalizadores UIControlEvent têm um mapeamento um-para-um para sinalizadores individuais. Quando você quiser ter a mesma parte do código que manipula dois ou mais eventos, use o UIControl.AddTarget método :

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

Usando a sintaxe lambda:

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

Se você precisar usar recursos de baixo nível do Objective-C, como conectar-se a uma instância de objeto específica e invocar um seletor específico:

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

// In some other place:

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

Observe que, se você implementar o método de instância em uma classe base herdada, ele deverá ser um método público.

Protocolos

Um protocolo é um Objective-C recurso de linguagem que fornece uma lista de declarações de método. Ele tem uma finalidade semelhante a uma interface em C#, a main diferença é que um protocolo pode ter métodos opcionais. Métodos opcionais não serão chamados se a classe que adota um protocolo não os implementar. Além disso, uma única classe em Objective-C pode implementar vários protocolos, assim como uma classe C# pode implementar várias interfaces.

A Apple usa protocolos em todo o iOS para definir contratos para que as classes adotem, ao mesmo tempo em que abstrai a classe de implementação do chamador, operando assim como uma interface C#. Os protocolos são usados em cenários não delegados (como com o exemplo mostrado a MKAnnotation seguir) e com delegados (conforme apresentado posteriormente neste documento, na seção Delegados).

Protocolos com Xamarin.ios

Vamos dar uma olhada em um exemplo usando um Objective-C protocolo do Xamarin.iOS. Para este exemplo, usaremos o MKAnnotation protocolo , que faz parte da MapKit estrutura . MKAnnotation é um protocolo que permite que qualquer objeto que o adote forneça informações sobre uma anotação que pode ser adicionada a um mapa. Por exemplo, um objeto que implementa MKAnnotation fornece o local da anotação e o título associado a ela.

Dessa forma, o MKAnnotation protocolo é usado para fornecer dados pertinentes que acompanham uma anotação. A exibição real da anotação em si é criada a partir dos dados no objeto que adota o MKAnnotation protocolo. Por exemplo, o texto do texto explicativo que aparece quando o usuário toca na anotação (conforme mostrado na captura de tela abaixo) vem da Title propriedade na classe que implementa o protocolo:

Texto de exemplo para o texto explicativo quando o usuário toca na anotação

Conforme descrito na próxima seção, Protocols Deep Dive, o Xamarin.iOS associa protocolos a classes abstratas. Para o MKAnnotation protocolo, a classe C# associada é nomeada MKAnnotation para imitar o nome do protocolo e é uma subclasse de NSObject, a classe base raiz para CocoaTouch. O protocolo requer que um getter e setter sejam implementados para a coordenada; no entanto, um título e um subtítulo são opcionais. Portanto, na MKAnnotation classe , a Coordinate propriedade é abstrata, exigindo que ela seja implementada e as Title propriedades e Subtitle sejam marcadas como virtuais, tornando-as opcionais, conforme mostrado abaixo:

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

Qualquer classe pode fornecer dados de anotação simplesmente derivando de MKAnnotation, desde que pelo menos a Coordinate propriedade seja implementada. Por exemplo, aqui está uma classe de exemplo que usa a coordenada no construtor e retorna uma cadeia de caracteres para o 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;
        }
    }
}

Por meio do protocolo ao qual ele está associado, qualquer classe que subclasse MKAnnotation pode fornecer dados relevantes que serão usados pelo mapa ao criar a exibição da anotação. Para adicionar uma anotação a um mapa, basta chamar o AddAnnotation método de uma MKMapView instância, conforme mostrado no seguinte código:

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

A variável de mapa aqui é uma instância de um MKMapView, que é a classe que representa o próprio mapa. O MKMapView usará os Coordinate dados derivados da SampleMapAnnotation instância para posicionar a exibição de anotação no mapa.

O MKAnnotation protocolo fornece um conjunto conhecido de recursos em todos os objetos que o implementam, sem que o consumidor (o mapa neste caso) precise saber mais sobre os detalhes da implementação. Isso simplifica a adição de uma variedade de anotações possíveis a um mapa.

Aprofundamento dos protocolos

Como as interfaces C# não dão suporte a métodos opcionais, o Xamarin.iOS mapeia protocolos para classes abstratas. Portanto, a adoção de um protocolo no Objective-C é realizada no Xamarin.iOS derivando da classe abstrata associada ao protocolo e implementando os métodos necessários. Esses métodos serão expostos como métodos abstratos na classe . Os métodos opcionais do protocolo serão associados a métodos virtuais da classe C#.

Por exemplo, aqui está uma parte do UITableViewDataSource protocolo como associada no 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){...}
...
}

Observe que a classe é abstrata. O Xamarin.iOS torna a classe abstrata para dar suporte a métodos opcionais/necessários em protocolos. No entanto, ao contrário dos Objective-C protocolos (ou interfaces C#), as classes C# não dão suporte a várias heranças. Isso afeta o design do código C# que usa protocolos e normalmente leva a classes aninhadas. Mais informações sobre esse problema serão abordadas posteriormente neste documento, na seção Delegados.

GetCell(…) é um método abstrato, associado ao Objective-Cseletor, tableView:cellForRowAtIndexPath:, que é um método necessário do UITableViewDataSource protocolo. Seletor é o termo para o Objective-C nome do método. Para impor o método conforme necessário, o Xamarin.iOS o declara como abstrato. O outro método, NumberOfSections(…), está associado a numberOfSectionsInTableview:. Esse método é opcional no protocolo, portanto, o Xamarin.iOS o declara como virtual, tornando-o opcional para substituir em C#.

O Xamarin.iOS cuida de toda a associação do iOS para você. No entanto, se você precisar associar um protocolo manualmente Objective-C , poderá fazer isso decorando uma classe com o ExportAttribute. Esse é o mesmo método usado pelo próprio Xamarin.iOS.

Para obter mais informações sobre como associar Objective-C tipos no Xamarin.iOS, consulte o artigo Tipos de associaçãoObjective-C.

No entanto, ainda não acabamos com protocolos. Eles também são usados no iOS como base para Objective-C delegados, que é o tópico da próxima seção.

Delegados

O iOS usa delegados Objective-C para implementar o padrão de delegação, no qual um objeto passa o trabalho para outro. O objeto que está fazendo o trabalho é o delegado do primeiro objeto. Um objeto instrui seu representante a fazer o trabalho enviando-lhe mensagens depois que determinadas coisas acontecem. Enviar uma mensagem como essa em Objective-C é funcionalmente equivalente a chamar um método em C#. Um delegado implementa métodos em resposta a essas chamadas e, portanto, fornece funcionalidade para o aplicativo.

Os delegados permitem estender o comportamento das classes sem a necessidade de criar subclasses. Os aplicativos no iOS geralmente usam delegados quando uma classe chama de volta para outra depois que ocorre uma ação importante. Por exemplo, a MKMapView classe chama de volta para seu delegado quando o usuário toca em uma anotação em um mapa, dando ao autor da classe delegada a oportunidade de responder dentro do aplicativo. Você pode trabalhar com um exemplo desse tipo de uso de delegado posteriormente neste artigo, em Exemplo usando um delegado com Xamarin.iOS.

Neste ponto, você pode estar se perguntando como uma classe determina quais métodos chamar em seu delegado. Esse é outro lugar onde você usa protocolos. Normalmente, os métodos disponíveis para um delegado vêm dos protocolos que eles adotam.

Como os protocolos são usados com delegados

Vimos anteriormente como os protocolos são usados para dar suporte à adição de anotações a um mapa. Os protocolos também são usados para fornecer um conjunto conhecido de métodos para que as classes chamem depois que determinados eventos ocorrem, como depois que o usuário toca em uma anotação em um mapa ou seleciona uma célula em uma tabela. As classes que implementam esses métodos são conhecidas como os delegados das classes que os chamam.

Classes que dão suporte à delegação fazem isso expondo uma propriedade Delegate, à qual uma classe que implementa o delegado é atribuída. Os métodos implementados para o delegado dependerão do protocolo que o delegado específico adota. Para o UITableView método , você implementa o UITableViewDelegate protocolo , para o UIAccelerometer método , você implementaria UIAccelerometerDelegatee assim por diante para quaisquer outras classes em todo o iOS para as quais você gostaria de expor um delegado.

A MKMapView classe que vimos em nosso exemplo anterior também tem uma propriedade chamada Delegate, que chamará depois que vários eventos ocorrerem. O Delegado para MKMapView é do tipo MKMapViewDelegate. Você usará isso em breve em um exemplo para responder à anotação depois que ela for selecionada, mas primeiro vamos discutir a diferença entre delegados fortes e fracos.

Delegados fortes versus delegados fracos

Os delegados que analisamos até agora são delegados fortes, o que significa que eles são fortemente tipado. As associações do Xamarin.iOS são fornecidas com uma classe fortemente tipada para cada protocolo delegado no iOS. No entanto, o iOS também tem o conceito de um delegado fraco. Em vez de subclasse de uma classe associada ao Objective-C protocolo para um delegado específico, o iOS também permite que você opte por associar os métodos de protocolo por conta própria em qualquer classe que você goste que deriva de NSObject, decorando seus métodos com ExportAttribute e fornecendo os seletores apropriados. Ao adotar essa abordagem, você atribui uma instância da classe à propriedade WeakDelegate em vez de à propriedade Delegate. Um delegado fraco oferece a flexibilidade de colocar sua classe delegada em uma hierarquia de herança diferente. Vamos examinar um exemplo de Xamarin.iOS que usa delegados fortes e fracos.

Exemplo usando um delegado com Xamarin.iOS

Para executar o código em resposta ao usuário tocando na anotação em nosso exemplo, podemos subclasse MKMapViewDelegate e atribuir uma instância à MKMapViewpropriedade do Delegate . O MKMapViewDelegate protocolo contém apenas métodos opcionais. Portanto, todos os métodos são virtuais associados a esse protocolo na classe Xamarin.iOS MKMapViewDelegate . Quando o usuário selecionar uma anotação, a MKMapView instância enviará a mapView:didSelectAnnotationView: mensagem ao delegado. Para lidar com isso no Xamarin.iOS, precisamos substituir o DidSelectAnnotationView (MKMapView mapView, MKAnnotationView annotationView) método na subclasse MKMapViewDelegate desta forma:

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

A classe SampleMapDelegate mostrada acima é implementada como uma classe aninhada no controlador que contém a MKMapView instância . No Objective-C, você geralmente verá o controlador adotar vários protocolos diretamente dentro da classe . No entanto, como os protocolos são associados a classes no Xamarin.iOS, as classes que implementam delegados fortemente tipados geralmente são incluídas como classes aninhadas.

Com a implementação da classe delegada em vigor, você só precisa instanciar uma instância do delegado no controlador e atribuí-la à MKMapViewpropriedade do Delegate , conforme mostrado aqui:

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 um delegado fraco para realizar a mesma coisa, você precisa associar o método por conta própria em qualquer classe derivada de NSObject e atribuí-lo à WeakDelegate propriedade do MKMapView. Como a UIViewController classe deriva de NSObject (como todas as Objective-C classes em CocoaTouch), podemos simplesmente implementar um método associado diretamente no mapView:didSelectAnnotationView: controlador e atribuir o controlador a MKMapView, WeakDelegateevitando a necessidade da classe aninhada extra. O código a seguir demonstra essa abordagem:

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)
    {
        ...
    }
}

Ao executar esse código, o aplicativo se comporta exatamente como ao executar a versão delegada fortemente tipada. O benefício desse código é que o delegado fraco não requer a criação da classe extra que foi criada quando usamos o delegado fortemente tipado. No entanto, isso ocorre às custas do tipo segurança. Se você cometesse um erro no seletor que foi passado para o ExportAttribute, não descobriria até o runtime.

Eventos e delegados

Os delegados são usados para retornos de chamada no iOS da mesma forma que o .NET usa eventos. Para tornar as APIs do iOS e a maneira como elas usam Objective-C delegados parecem mais com o .NET, o Xamarin.iOS expõe eventos do .NET em muitos lugares em que os delegados são usados no iOS.

Por exemplo, a implementação anterior em que o MKMapViewDelegate respondeu a uma anotação selecionada também poderia ser implementada no Xamarin.iOS usando um evento .NET. Nesse caso, o evento seria definido em MKMapView e chamado DidSelectAnnotationViewde . Ele teria uma EventArgs subclasse do tipo MKMapViewAnnotationEventsArgs. A View propriedade de MKMapViewAnnotationEventsArgs lhe daria uma referência à exibição de anotação, da qual você poderia prosseguir com a mesma implementação que tinha anteriormente, conforme ilustrado aqui:

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

Resumo

Este artigo abordou como usar eventos, protocolos e delegados no Xamarin.iOS. Vimos como o Xamarin.iOS expõe eventos de estilo .NET normais para controles. Em seguida, aprendemos sobre Objective-C protocolos, incluindo como eles são diferentes das interfaces C# e como o Xamarin.iOS os usa. Por fim, examinamos Objective-C delegados de uma perspectiva do Xamarin.iOS. Vimos como o Xamarin.iOS dá suporte a delegados forte e fracamente tipados e como associar eventos .NET a métodos delegados.