Бөлісу құралы:


События, протоколы и делегаты в Xamarin.iOS

Xamarin.iOS использует элементы управления для предоставления событий для большинства взаимодействий с пользователем. Приложения Xamarin.iOS используют эти события так же, как и традиционные приложения .NET. Например, класс Xamarin.iOS UIButton имеет событие с именем TouchUpInside и использует это событие так же, как если бы этот класс и событие находились в приложении .NET.

Помимо этого подхода .NET, Xamarin.iOS предоставляет другую модель, которая может использоваться для более сложного взаимодействия и привязки данных. Эта методология использует то, что Apple вызывает делегатов и протоколов. Делегаты похожи на делегаты в C#, но вместо определения и вызова одного метода делегат является Objective-C целым классом, который соответствует протоколу. Протокол аналогичен интерфейсу в C#, за исключением того, что его методы могут быть необязательными. Например, чтобы заполнить UITableView данными, вы создадите класс делегата, реализующий методы, определенные в протоколе UITableViewDataSource, который uiTableView будет вызывать для заполнения себя.

В этой статье вы узнаете обо всех этих разделах, предоставляя вам надежную основу для обработки сценариев обратного вызова в Xamarin.iOS, в том числе:

  • События — использование событий .NET с элементами управления UIKit.
  • Протоколы — Обучение, какие протоколы и как они используются, а также создание примера, предоставляющего данные для заметки карты.
  • Делегаты — Обучение о Objective-C делегатах, расширяя пример карты для обработки взаимодействия с пользователем, включающего заметку, а затем изучая разницу между сильными и слабыми делегатами и когда использовать каждую из них.

Чтобы проиллюстрировать протоколы и делегаты, мы создадим простое приложение карты, которое добавляет заметку на карту, как показано ниже:

Пример простого приложения карты, добавляющего заметку к картеПример заметки, добавленной на карту

Прежде чем решать это приложение, давайте приступим к работе с событиями .NET в UIKit.

События .NET с uiKit

Xamarin.iOS предоставляет события .NET в элементах управления UIKit. Например, UIButton имеет событие TouchUpInside, которое обрабатывается как обычно в .NET, как показано в следующем коде, который использует лямбда-выражение C#:

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

Вы также можете реализовать это с помощью анонимного метода C# 2.0, как в следующем:

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

Приведенный выше код подключен к ViewDidLoad методу UIViewController. Переменная aButton ссылается на кнопку, которую можно добавить в конструктор интерфейсов Xcode или с кодом.

Xamarin.iOS также поддерживает стиль целевого действия для подключения кода к взаимодействию, которое происходит с элементом управления.

Дополнительные сведения о шаблоне целевого действия iOS см. в разделе "Основные компетенции приложений" для iOS в библиотеке разработчиков iOS Apple.

Дополнительные сведения см. в статье Проектирование пользовательских интерфейсов с помощью Xcode.

События

Если вы хотите перехватывать события из UIControl, у вас есть ряд вариантов: от использования лямбда-кодов C# и делегирования функций до использования низкоуровневых Objective-C API.

В следующем разделе показано, как записать событие TouchDown на кнопке в зависимости от того, сколько элементов управления требуется.

Стиль C#

Использование синтаксиса делегата:

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

Если вы хотите лямбда-коды вместо этого:

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

Если вы хотите использовать несколько кнопок, используйте один и тот же обработчик для совместного использования одного кода:

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

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

Мониторинг нескольких типов событий

События C# для флагов UIControlEvent имеют сопоставление "один к одному" с отдельными флагами. Если требуется иметь один и тот же фрагмент кода, обрабатывая два или более событий, используйте UIControl.AddTarget этот метод:

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

Использование лямбда-синтаксиса:

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

Если необходимо использовать низкоуровневые функции Objective-C, например подключение к конкретному экземпляру объекта и вызов определенного селектора:

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

// In some other place:

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

Обратите внимание, что при реализации метода экземпляра в наследуемом базовом классе он должен быть открытым методом.

Протоколы

Протокол — это Objective-C функция языка, которая предоставляет список объявлений методов. Он служит аналогичной цели интерфейсу в C#, главное отличие заключается в том, что протокол может иметь необязательные методы. Необязательные методы не вызываются, если класс, который принимает протокол, не реализует их. Кроме того, один класс может Objective-C реализовать несколько протоколов, так же как класс C# может реализовать несколько интерфейсов.

Apple использует протоколы по всему iOS для определения контрактов для принятия классов, а абстрагирование от вызывающего класса, поэтому работает так же, как интерфейс C#. Протоколы используются как в сценариях, не являющихся делегатами (например MKAnnotation , в примере ниже), так и с делегатами (как представлено далее в этом документе в разделе "Делегаты").

Протоколы с помощью Xamarin.ios

Рассмотрим пример использования Objective-C протокола из Xamarin.iOS. В этом примере мы будем использовать MKAnnotation протокол, который является частью MapKit платформы. MKAnnotation — это протокол, позволяющий любому объекту, который принимает его для предоставления сведений о заметке, которую можно добавить на карту. Например, объект, реализующий MKAnnotation объект, предоставляет расположение заметки и название, связанное с ним.

Таким образом, MKAnnotation протокол используется для предоставления соответствующих данных, сопровождающих заметку. Фактическое представление самой заметки создается из данных в объекте, который принимает MKAnnotation протокол. Например, текст выноски, который отображается при нажатии пользователя на заметку (как показано на снимке экрана ниже), поступает из Title свойства в классе, реализующем протокол:

Пример текста выноски при нажатии пользователя на заметку

Как описано в следующем разделе, Протоколы глубокого погружения, Xamarin.iOS привязывает протоколы к абстрактным классам. MKAnnotation Для протокола привязанный класс C# называется MKAnnotation для имитации имени протокола, и это подкласс NSObjectкорневого базового класса для CocoaTouch. Протокол требует реализации метода получения и задания для координаты; однако заголовок и подзаголовок являются необязательными. Таким образом, Coordinate в MKAnnotation классе свойство является абстрактным, требуя его реализации, и TitleSubtitle свойства помечены виртуальными, что делает их необязательными, как показано ниже:

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

Любой класс может предоставлять данные заметки, просто исходя MKAnnotationиз , если по крайней мере Coordinate свойство реализовано. Например, вот пример класса, который принимает координату в конструкторе и возвращает строку для заголовка:

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

Через протокол, к которому он привязан, любой класс, к которому подклассы MKAnnotation могут предоставлять соответствующие данные, которые будут использоваться картой при создании представления заметки. Чтобы добавить заметку на карту, просто вызовите AddAnnotation метод экземпляра MKMapView , как показано в следующем коде:

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

Переменная карты здесь представляет собой экземпляр класса MKMapView, представляющего саму карту. Данные MKMapView , производные Coordinate от экземпляра SampleMapAnnotation , будут использоваться для размещения представления заметок на карте.

Протокол MKAnnotation предоставляет известный набор возможностей для любых объектов, реализующих его, без потребителя (карты в данном случае), необходимо знать о деталях реализации. Это упрощает добавление различных возможных заметок на карту.

Глубокое погружение по протоколам

Так как интерфейсы C# не поддерживают необязательные методы, протоколы Xamarin.iOS сопоставляют с абстрактными классами. Поэтому принятие протокола Objective-C выполняется в Xamarin.iOS путем производных от абстрактного класса, привязанного к протоколу и реализующего необходимые методы. Эти методы будут предоставляться как абстрактные методы в классе. Необязательные методы из протокола будут привязаны к виртуальным методам класса C#.

Например, ниже приведена часть протокола, привязанная UITableViewDataSource к 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){...}
...
}

Обратите внимание, что класс является абстрактным. Xamarin.iOS делает класс абстрактным для поддержки необязательных или обязательных методов в протоколах. Однако в отличие от Objective-C протоколов (или интерфейсов C#), классы C# не поддерживают несколько наследования. Это влияет на проектирование кода C#, использующего протоколы, и обычно приводит к вложенным классам. Дополнительные сведения об этой проблеме рассматриваются далее в этом документе в разделе "Делегаты".

GetCell(…) — абстрактный метод, привязанный к Objective-Cселектору, tableView:cellForRowAtIndexPath:который является обязательным методом UITableViewDataSource протокола. Селектор — это Objective-C термин для имени метода. Чтобы применить метод по мере необходимости, Xamarin.iOS объявляет его абстрактным. Другой метод NumberOfSections(…), привязан к numberOfSectionsInTableview:. Этот метод является необязательным в протоколе, поэтому Xamarin.iOS объявляет его виртуальным, что делает его необязательным для переопределения в C#.

Xamarin.iOS заботится обо всех привязках iOS. Однако если вы когда-либо хотите привязать протокол вручнуюObjective-C, это можно сделать, декорируя класс с помощью .ExportAttribute Это тот же метод, используемый самим Xamarin.iOS.

Дополнительные сведения о привязке Objective-C типов в Xamarin.iOS см. в статье "Типы привязкиObjective-C".

Мы еще не через протоколы, хотя. Они также используются в iOS в качестве основы для Objective-C делегатов, который является темой следующего раздела.

Делегаты

iOS использует Objective-C делегаты для реализации шаблона делегирования, в котором один объект передает работу другому. Объект, выполняя работу, является делегатом первого объекта. Объект сообщает делегату выполнить работу, отправив ему сообщения после выполнения определенных действий. Отправка сообщения, подобного этому, Objective-C функционально эквивалентна вызову метода в C#. Делегат реализует методы в ответ на эти вызовы и поэтому предоставляет функциональные возможности для приложения.

Делегаты позволяют расширить поведение классов без необходимости создавать подклассы. Приложения в iOS часто используют делегаты, когда один класс возвращается к другому после выполнения важного действия. Например, класс возвращается к своему делегату, MKMapView когда пользователь нажимает заметку на карте, что дает автору класса делегата возможность ответить в приложении. Пример использования делегатов можно выполнить далее в этой статье в разделе "Пример использования делегата с Xamarin.iOS".

На этом этапе может возникнуть вопрос о том, как класс определяет методы для вызова делегата. Это еще одно место, в котором используются протоколы. Обычно методы, доступные делегату, приходят из принятых протоколов.

Использование протоколов с делегатами

Ранее мы видели, как протоколы используются для поддержки добавления заметок на карту. Протоколы также используются для предоставления известного набора методов для вызова классов после возникновения определенных событий, таких как после того, как пользователь нажимает заметку на карте или выбирает ячейку в таблице. Классы, реализующие эти методы, называются делегатами классов, вызывающих их.

Классы, поддерживающие делегирование, предоставляя свойство Делегата, которому назначается класс, реализующий делегат. Методы, которые вы реализуете для делегата, будут зависеть от протокола, который принимает конкретный делегат. UITableView Для метода вы реализуете UITableViewDelegate протокол, для UIAccelerometer метода будет реализовано UIAccelerometerDelegateи т. д. для любых других классов в iOS, для которых требуется предоставить делегат.

Класс, MKMapView который мы видели в предыдущем примере, также имеет свойство с именем "Делегат", которое будет вызываться после различных событий. MKMapView Делегат для типаMKMapViewDelegate. Вы будете использовать это вскоре в примере для реагирования на заметку после выбора, но сначала рассмотрим разницу между сильными и слабыми делегатами.

Сильные делегаты и слабые делегаты

Делегаты, которые мы рассмотрели до сих пор являются сильными делегатами, т. е. они строго типизированы. Привязки Xamarin.iOS передаются с строго типизированным классом для каждого протокола делегата в iOS. Однако iOS также имеет концепцию слабого делегата. Вместо подкласса класса, привязанного к Objective-C протоколу для определенного делегата, iOS также позволяет привязать методы протокола самостоятельно в любом классе, который вы хотите получить от NSObject, декорируя методы с помощью ExportAttribute, а затем предоставлять соответствующие селекторы. При таком подходе экземпляр класса назначается свойству WeakDelegate вместо свойства Delegate. Слабый делегат предлагает гибкость, чтобы перевести класс делегата в другую иерархию наследования. Рассмотрим пример Xamarin.iOS, использующий как сильные, так и слабые делегаты.

Пример использования делегата с Xamarin.iOS

Чтобы выполнить код в ответ на касание заметки пользователем в нашем примере, можно подкласс MKMapViewDelegate и назначить экземпляр свойствуMKMapViewDelegate. Протокол MKMapViewDelegate содержит только необязательные методы. Поэтому все методы являются виртуальными, привязанными к этому протоколу в классе Xamarin.iOS MKMapViewDelegate . Когда пользователь выбирает заметку, MKMapView экземпляр отправит mapView:didSelectAnnotationView: сообщение своему делегату. Для этого в Xamarin.iOS необходимо переопределить DidSelectAnnotationView (MKMapView mapView, MKAnnotationView annotationView) метод в подклассе MKMapViewDelegate следующим образом:

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

Приведенный выше класс SampleMapDelegate реализуется как вложенный класс в контроллере, который содержит MKMapView экземпляр. В Objective-Cэтом случае контроллер часто будет принимать несколько протоколов непосредственно в классе. Однако, так как протоколы привязаны к классам в Xamarin.iOS, классы, реализующие строго типизированные делегаты, обычно включаются в качестве вложенных классов.

При реализации класса делегата необходимо создать экземпляр делегата в контроллере и назначить его MKMapViewDelegate свойству, как показано здесь:

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

Чтобы использовать слабый делегат для выполнения той же задачи, необходимо привязать метод самостоятельно в любом классе, наследуемом от NSObject него, и назначить его свойству WeakDelegate объекта MKMapView. UIViewController Так как класс в конечном счете является производным от NSObject (как и каждого Objective-C класса в CocoaTouch), мы можем просто реализовать метод, привязанный непосредственно к mapView:didSelectAnnotationView: контроллеру, и назначить контроллеру MKMapViewзначение 'sWeakDelegate, избегая необходимости вложенного класса. Приведенный ниже код демонстрирует такой подход:

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

При выполнении этого кода приложение ведет себя точно так же, как и при выполнении строго типизированной версии делегата. Преимуществом этого кода является то, что слабый делегат не требует создания дополнительного класса, созданного при использовании строго типизированного делегата. Однако это происходит за счет безопасности типа. Если бы вы сделали ошибку в селекторе, который был передан в, ExportAttributeвы не узнаете, пока среда выполнения не будет выполнена.

События и делегаты

Делегаты используются для обратных вызовов в iOS аналогично тому, как .NET использует события. Чтобы сделать API iOS и способ использования Objective-C делегатов, кажется, более похожими на .NET, Xamarin.iOS предоставляет события .NET во многих местах, где делегаты используются в iOS.

Например, более ранняя реализация, в которой MKMapViewDelegate была реализована выбранная заметка, также может быть реализована в Xamarin.iOS с помощью события .NET. В этом случае событие будет определено MKMapView и вызывается DidSelectAnnotationView. Он будет иметь EventArgs подкласс типа MKMapViewAnnotationEventsArgs. Свойство View дает ссылку на представление заметки MKMapViewAnnotationEventsArgs , из которого можно продолжить с той же реализацией, что и ранее, как показано здесь:

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

Итоги

В этой статье описано, как использовать события, протоколы и делегаты в Xamarin.iOS. Мы видели, как Xamarin.iOS предоставляет обычные события стиля .NET для элементов управления. Далее мы узнали о Objective-C протоколах, в том числе о том, как они отличаются от интерфейсов C# и как они используются Xamarin.iOS. Наконец, мы рассмотрели Objective-C делегатов с точки зрения Xamarin.iOS. Мы узнали, как Xamarin.iOS поддерживает как строго, так и слабо типизированные делегаты, а также как привязать события .NET к делегированию методов.