Zdarzenia, protokoły i delegaty w środowisku Xamarin.iOS

Platforma Xamarin.iOS używa kontrolek do uwidaczniania zdarzeń dla większości interakcji użytkownika. Aplikacje platformy Xamarin.iOS używają tych zdarzeń w taki sam sposób, jak w przypadku tradycyjnych aplikacji platformy .NET. Na przykład klasa Xamarin.iOS UIButton ma zdarzenie o nazwie TouchUpInside i korzysta z tego zdarzenia tak, jakby ta klasa i zdarzenie znajdowało się w aplikacji platformy .NET.

Oprócz tego podejścia platformy .NET platforma Xamarin.iOS uwidacznia inny model, który może służyć do bardziej złożonej interakcji i powiązania danych. Ta metodologia używa funkcji wywoływanych przez firmę Apple delegatów i protokołów. Delegaty są podobne do delegatów w języku C#, ale zamiast definiować i wywoływać jedną metodę, delegat w Objective-C systemie jest całą klasą zgodną z protokołem. Protokół jest podobny do interfejsu w języku C#, z tą różnicą, że jego metody mogą być opcjonalne. Aby na przykład wypełnić element UITableView danymi, należy utworzyć klasę delegata, która implementuje metody zdefiniowane w protokole UITableViewDataSource, który wywoła obiekt UITableView, aby wypełnić się.

W tym artykule znajdziesz informacje na temat wszystkich tych tematów, zapewniając solidną podstawę do obsługi scenariuszy wywołania zwrotnego na platformie Xamarin.iOS, w tym:

  • Zdarzenia — używanie zdarzeń platformy .NET z kontrolkami UIKit.
  • Protokoły — Edukacja, jakie protokoły są i jak są używane, oraz tworzenie przykładu zawierającego dane adnotacji mapy.
  • Delegaci — Edukacja na temat Objective-C delegatów, rozszerzając przykład mapy w celu obsługi interakcji użytkownika, która zawiera adnotację, a następnie poznając różnicę między silnymi i słabymi delegatami i kiedy należy ich używać.

Aby zilustrować protokoły i delegaty, utworzymy prostą aplikację mapy, która dodaje adnotację do mapy, jak pokazano poniżej:

Przykład prostej aplikacji mapy, która dodaje adnotację do mapyPrzykładowa adnotacja dodana do mapy

Przed rozpoczęciem pracy z tą aplikacją przyjrzyjmy się zdarzeń platformy .NET w zestawie UIKit.

Zdarzenia platformy .NET z zestawem UIKit

Środowisko Xamarin.iOS uwidacznia zdarzenia platformy .NET w kontrolkach UIKit. Na przykład funkcja UIButton ma zdarzenie TouchUpInside, które jest obsługiwane w zwykły sposób na platformie .NET, jak pokazano w poniższym kodzie używającym wyrażenia lambda języka C#:

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

Można to również zaimplementować za pomocą metody anonimowej w stylu C# 2.0, takiej jak ta:

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

Powyższy kod jest podłączony do ViewDidLoad metody UIViewController. Zmienna aButton odwołuje się do przycisku, który można dodać w narzędziu Xcode Interface Builder lub z kodem.

Platforma Xamarin.iOS obsługuje również styl akcji docelowej łączący kod z interakcją, która występuje z kontrolką.

Aby uzyskać więcej informacji na temat wzorca akcji docelowej systemu iOS, zobacz sekcję Target-Action ( Akcja docelowa) w sekcji Core Application Competencies for iOS for iOS (Podstawowe kompetencje aplikacji dla systemu iOS) w bibliotece deweloperów systemu iOS firmy Apple.

Aby uzyskać więcej informacji, zobacz Projektowanie interfejsów użytkownika za pomocą programu Xcode.

Zdarzenia

Jeśli chcesz przechwycić zdarzenia z interfejsu UIControl, masz szereg opcji: od używania lambdów języka C# i delegowania funkcji do korzystania z interfejsów API niskiego poziomu Objective-C .

W poniższej sekcji pokazano, jak przechwycić zdarzenie TouchDown na przycisku, w zależności od tego, ile kontrolki potrzebujesz.

Styl języka C#

Przy użyciu składni delegata:

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

Jeśli zamiast tego lubisz lambdy:

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

Jeśli chcesz mieć wiele przycisków, użyj tej samej procedury obsługi, aby udostępnić ten sam kod:

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

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

Monitorowanie więcej niż jednego rodzaju zdarzenia

Zdarzenia języka C# dla flag UIControlEvent mają mapowanie "jeden do jednego" do poszczególnych flag. Jeśli chcesz, aby ten sam fragment kodu obsługiwał co najmniej dwa zdarzenia, użyj UIControl.AddTarget metody :

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

Przy użyciu składni lambda:

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

Jeśli musisz użyć funkcji Objective-Cniskiego poziomu , takich jak podłączanie do określonego wystąpienia obiektu i wywoływanie określonego selektora:

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

// In some other place:

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

Należy pamiętać, że jeśli implementujesz metodę wystąpienia w dziedziczonej klasie bazowej, musi to być metoda publiczna.

Protokoły

Protokół to Objective-C funkcja języka, która udostępnia listę deklaracji metod. Służy on do podobnego celu do interfejsu w języku C#, główną różnicą jest to, że protokół może mieć opcjonalne metody. Metody opcjonalne nie są wywoływane, jeśli klasa, która przyjmuje protokół, nie implementuje ich. Ponadto pojedyncza klasa w Objective-C programie może implementować wiele protokołów, podobnie jak klasa języka C# może implementować wiele interfejsów.

Firma Apple używa protokołów w całym systemie iOS do definiowania kontraktów dla klas do wdrożenia, jednocześnie abstrakcjąc klasę implementającą z obiektu wywołującego, tak aby działały tak samo jak interfejs języka C#. Protokoły są używane zarówno w scenariuszach niedelegacyjnych (na przykład w przykładzie MKAnnotation pokazanym obok), jak i delegatów (jak przedstawiono w dalszej części tego dokumentu, w sekcji Delegaty).

Protokoły z platformą Xamarin.ios

Przyjrzyjmy się przykładowi przy użyciu Objective-C protokołu z platformy Xamarin.iOS. W tym przykładzie MKAnnotation użyjemy protokołu, który jest częścią MapKit struktury. MKAnnotation to protokół, który umożliwia każdemu obiektowi, który go przyjmuje w celu udostępnienia informacji na temat adnotacji, które można dodać do mapy. Na przykład implementacja MKAnnotation obiektu udostępnia lokalizację adnotacji i skojarzony z nim tytuł.

W ten sposób MKAnnotation protokół jest używany do dostarczania istotnych danych, które towarzyszą adnotacji. Rzeczywisty widok samej adnotacji jest tworzony na podstawie danych w obiekcie, który przyjmuje MKAnnotation protokół. Na przykład tekst objaśnienia, który pojawia się, gdy użytkownik naciśnie adnotację (jak pokazano na poniższym zrzucie ekranu) pochodzi z Title właściwości w klasie implementujące protokół:

Przykładowy tekst objaśnienia po naciśnięciu adnotacji przez użytkownika

Zgodnie z opisem w następnej sekcji protokoły Szczegółowe omówienie, Xamarin.iOS wiąże protokoły z klasami abstrakcyjnymi. MKAnnotation Dla protokołu powiązana klasa języka C# nosi nazwę MKAnnotation , aby naśladować nazwę protokołu i jest podklasą NSObject, główną klasą bazową dla cocoaTouch. Protokół wymaga implementacji elementu getter i ustawiania dla współrzędnych; jednak tytuł i podtytuł są opcjonalne. W związku z tym MKAnnotation w klasie Coordinate właściwość jest abstrakcyjna, wymagając jej zaimplementowania, a Title właściwości i Subtitle są oznaczone jako wirtualne, co czyni je opcjonalnymi, jak pokazano poniżej:

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

Każda klasa może dostarczać dane adnotacji, po prostu wyprowadzając z MKAnnotationklasy , o ile co najmniej Coordinate właściwość jest implementowana. Oto przykładowa klasa, która przyjmuje współrzędną w konstruktorze i zwraca ciąg tytułu:

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

Za pośrednictwem protokołu, z jakim jest powiązany, każda klasa, która podklasy MKAnnotation może dostarczyć odpowiednie dane, które będą używane przez mapę podczas tworzenia widoku adnotacji. Aby dodać adnotację do mapy, po prostu wywołaj AddAnnotation metodę MKMapView wystąpienia, jak pokazano w poniższym kodzie:

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

Zmienna mapy jest tutaj wystąpieniem MKMapViewklasy , która reprezentuje samą mapę. Użyje MKMapViewCoordinate danych pochodzących z SampleMapAnnotation wystąpienia, aby ustawić widok adnotacji na mapie.

Protokół MKAnnotation udostępnia znany zestaw możliwości we wszystkich obiektach, które go implementują, bez konsumenta (w tym przypadku mapy) wymagającego znajomości szczegółów implementacji. Usprawnia to dodawanie różnych możliwych adnotacji do mapy.

Szczegółowe omówienie protokołów

Ponieważ interfejsy języka C# nie obsługują opcjonalnych metod, platforma Xamarin.iOS mapuje protokoły na klasy abstrakcyjne. W związku z tym wdrożenie protokołu w Objective-C programie jest realizowane w środowisku Xamarin.iOS przez wyprowadzenie z klasy abstrakcyjnej powiązanej z protokołem i implementowanie wymaganych metod. Te metody zostaną uwidocznione jako metody abstrakcyjne w klasie. Opcjonalne metody z protokołu będą powiązane z metodami wirtualnymi klasy C#.

Na przykład poniżej znajduje się część UITableViewDataSource protokołu powiązana z platformą 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){...}
...
}

Należy pamiętać, że klasa jest abstrakcyjna. Platforma Xamarin.iOS sprawia, że klasa jest abstrakcyjna do obsługi opcjonalnych/wymaganych metod w protokołach. Jednak w przeciwieństwie do Objective-C protokołów (lub interfejsów języka C#) klasy języka C# nie obsługują wielu dziedziczenia. Ma to wpływ na projekt kodu w języku C#, który używa protokołów, i zwykle prowadzi do zagnieżdżonych klas. Więcej informacji na temat tego problemu opisano w dalszej części tego dokumentu w sekcji Delegaci.

GetCell(…) to metoda abstrakcyjna powiązana z Objective-Cselektorem , tableView:cellForRowAtIndexPath:która jest wymaganą metodą UITableViewDataSource protokołu. Selektor to Objective-C termin nazwy metody. Aby wymusić metodę zgodnie z wymaganiami, platforma Xamarin.iOS deklaruje ją jako abstrakcyjną. Druga metoda, NumberOfSections(…), jest powiązana z numberOfSectionsInTableview:. Ta metoda jest opcjonalna w protokole, więc platforma Xamarin.iOS deklaruje ją jako wirtualną, co czyni ją opcjonalną do zastąpienia w języku C#.

Środowisko Xamarin.iOS zajmuje się wszystkimi powiązaniami systemu iOS. Jeśli jednak kiedykolwiek musisz powiązać protokół z Objective-C ręcznie, możesz to zrobić, dekorując klasę za pomocą klasy ExportAttribute. Jest to ta sama metoda używana przez samą platformę Xamarin.iOS.

Aby uzyskać więcej informacji na temat tworzenia powiązań Objective-C typów na platformie Xamarin.iOS, zobacz artykuł Typy powiązańObjective-C.

Nie korzystamy jeszcze z protokołów. Są one również używane w systemie iOS jako podstawy dla Objective-C delegatów, który jest tematem następnej sekcji.

Delegaci

System iOS używa Objective-C delegatów do implementowania wzorca delegowania, w którym jeden obiekt przechodzi pracę do innego. Obiekt wykonujący pracę jest delegatem pierwszego obiektu. Obiekt nakazuje delegatowi pracę, wysyłając komunikaty po wystąpieniu pewnych rzeczy. Wysyłanie komunikatu takiego w pliku Objective-C jest funkcjonalnie równoważne wywołaniu metody w języku C#. Delegat implementuje metody w odpowiedzi na te wywołania, a więc zapewnia funkcjonalność aplikacji.

Delegaci umożliwiają rozszerzanie zachowania klas bez konieczności tworzenia podklas. Aplikacje w systemie iOS często używają delegatów, gdy jedna klasa wywołuje z powrotem do innej po wystąpieniu ważnej akcji. Na przykład MKMapView klasa wywołuje delegata, gdy użytkownik naciśnie adnotację na mapie, dając autorowi klasy delegata możliwość odpowiedzi w aplikacji. W dalszej części tego artykułu możesz zapoznać się z przykładem użycia delegata tego typu, w temacie Example Using a Delegate with Xamarin.iOS (Przykład używanie delegata z platformą Xamarin.iOS).

W tym momencie możesz się zastanawiać, w jaki sposób klasa określa, jakie metody wywołać na jego delegatu. Jest to inne miejsce, w którym są używane protokoły. Zazwyczaj metody dostępne dla delegata pochodzą z wdrażanych protokołów.

Jak protokoły są używane z delegatami

Widzieliśmy wcześniej, jak protokoły są używane do obsługi dodawania adnotacji do mapy. Protokoły są również używane do udostępniania znanego zestawu metod wywoływania klas po wystąpieniu niektórych zdarzeń, takich jak po naciśnięciu adnotacji przez użytkownika na mapie lub wybraniu komórki w tabeli. Klasy implementujące te metody są nazywane delegatami klas, które je nazywają.

Klasy, które obsługują delegowanie, wykonują to przez uwidacznianie właściwości Delegat, do której jest przypisywana klasa implementująca delegata. Metody zaimplementowane dla delegata będą zależeć od protokołu, który dany delegat przyjmuje. W przypadku UITableView metody należy zaimplementować UITableViewDelegateUIAccelerometerDelegateprotokół dla UIAccelerometer metody , a tak dalej dla innych klas w systemie iOS, dla których chcesz uwidocznić delegata.

Klasa MKMapView , którą widzieliśmy we wcześniejszym przykładzie, ma również właściwość o nazwie Delegate, która będzie wywoływana po wystąpieniu różnych zdarzeń. Delegat dla MKMapView jest typu MKMapViewDelegate. Użyjesz tego wkrótce w przykładzie, aby odpowiedzieć na adnotację po jej wybraniu, ale najpierw omówimy różnicę między silnymi i słabymi delegatami.

Silni delegaci a słabi delegaci

Delegaci, na których przyjrzeliśmy się do tej pory, są silnymi delegatami, co oznacza, że są silnie typizowane. Powiązania platformy Xamarin.iOS są dostarczane z silnie typizowanym typem klasy dla każdego protokołu delegata w systemie iOS. Jednak system iOS ma również koncepcję słabego delegata. Zamiast podklasować klasę powiązaną z Objective-C protokołem dla określonego delegata, system iOS umożliwia również samodzielne powiązanie metod protokołu w dowolnej klasie, która pochodzi z klasy NSObject, dekorując metody za pomocą atrybutu ExportAttribute, a następnie podając odpowiednie selektory. W przypadku tego podejścia przypiszesz wystąpienie klasy do właściwości WeakDelegate zamiast do właściwości Delegate. Słaby delegat zapewnia elastyczność przechodzenia do klasy delegata w dół innej hierarchii dziedziczenia. Przyjrzyjmy się przykładowi platformy Xamarin.iOS, który używa zarówno silnych, jak i słabych delegatów.

Przykład użycia delegata z platformą Xamarin.iOS

Aby wykonać kod w odpowiedzi na naciśnięcie adnotacji przez użytkownika w naszym przykładzie, możemy podklasy MKMapViewDelegate i przypisać wystąpienie do MKMapViewwłaściwości .s Delegate . Protokół MKMapViewDelegate zawiera tylko metody opcjonalne. W związku z tym wszystkie metody są wirtualne, które są powiązane z tym protokołem w klasie Xamarin.iOS MKMapViewDelegate . Gdy użytkownik wybierze adnotację, MKMapView wystąpienie wyśle komunikat do swojego delegata mapView:didSelectAnnotationView: . Aby to obsłużyć w środowisku Xamarin.iOS, musimy zastąpić metodę DidSelectAnnotationView (MKMapView mapView, MKAnnotationView annotationView) w podklasie MKMapViewDelegate w następujący sposób:

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

Klasa SampleMapDelegate pokazana powyżej jest implementowana jako zagnieżdżona klasa w kontrolerze zawierającym MKMapView wystąpienie. W Objective-Csystemie często kontroler przyjmuje wiele protokołów bezpośrednio w klasie . Jednak ponieważ protokoły są powiązane z klasami w środowisku Xamarin.iOS, klasy implementujące silnie typizowane delegaty są zwykle uwzględniane jako klasy zagnieżdżone.

Po wdrożeniu implementacji klasy delegata wystarczy utworzyć wystąpienie delegata w kontrolerze i przypisać je do MKMapViewwłaściwości "s Delegate ", jak pokazano poniżej:

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

Aby użyć słabego delegata, aby wykonać to samo, musisz powiązać metodę samodzielnie w dowolnej klasie, która pochodzi z NSObject klasy i przypisać ją do WeakDelegate właściwości MKMapView. UIViewController Ponieważ klasa ostatecznie pochodzi z NSObject (podobnie jak każda Objective-C klasa w cocoaTouch), możemy po prostu zaimplementować metodę powiązaną mapView:didSelectAnnotationView: bezpośrednio z kontrolerem i przypisać kontroler do MKMapViewWeakDelegateklasy , unikając potrzeby dodatkowej klasy zagnieżdżonej. Poniższy kod przedstawia następujące podejście:

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

Podczas uruchamiania tego kodu aplikacja zachowuje się dokładnie tak, jak w przypadku uruchamiania silnie typizowanej wersji delegata. Zaletą tego kodu jest to, że słaby delegat nie wymaga utworzenia dodatkowej klasy, która została utworzona podczas korzystania z silnie typizowanego delegata. Jednak wiąże się to z kosztem bezpieczeństwa typu. Jeśli popełnisz błąd w selektorze ExportAttribute, który został przekazany do elementu , nie dowiesz się, aż do czasu wykonania.

Zdarzenia i delegaty

Delegaty są używane do wywołania zwrotnego w systemie iOS podobnie jak w przypadku korzystania ze zdarzeń platformy .NET. Aby interfejsy API systemu iOS i sposób korzystania z Objective-C delegatów wyglądały bardziej jak .NET, platforma Xamarin.iOS uwidacznia zdarzenia platformy .NET w wielu miejscach, w których delegaty są używane w systemie iOS.

Na przykład wcześniejsza implementacja, w której MKMapViewDelegate odpowiedź na wybraną adnotację może być również zaimplementowana w środowisku Xamarin.iOS przy użyciu zdarzenia platformy .NET. W takim przypadku zdarzenie zostanie zdefiniowane w MKMapView pliku i o nazwie DidSelectAnnotationView. Ma ona podklasę EventArgs typu MKMapViewAnnotationEventsArgs. Właściwość View polecenia MKMapViewAnnotationEventsArgs daje odwołanie do widoku adnotacji, z którego można kontynuować tę samą implementację, którą wcześniej wykonano, jak pokazano poniżej:

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

Podsumowanie

W tym artykule opisano sposób używania zdarzeń, protokołów i delegatów na platformie Xamarin.iOS. Zobaczyliśmy, jak platforma Xamarin.iOS uwidacznia normalne zdarzenia stylu platformy .NET dla kontrolek. Następnie dowiedzieliśmy się o Objective-C protokołach, w tym o tym, jak różnią się one od interfejsów języka C# i jak są używane przez platformę Xamarin.iOS. Na koniec zbadaliśmy Objective-C delegatów z perspektywy platformy Xamarin.iOS. Zobaczyliśmy, jak platforma Xamarin.iOS obsługuje delegatów silnie i słabo wpisanych oraz jak powiązać zdarzenia platformy .NET z metodami delegowanymi.