Xamarin.iOS のイベント、プロトコル、およびデリゲート

Xamarin.iOS では、コントロールを使用して、ほとんどのユーザー操作のイベントを公開します。 Xamarin.iOS アプリケーションは、従来の .NET アプリケーションとほとんど同じ方法でこれらのイベントを使用します。 たとえば、Xamarin.iOS UIButton クラスには TouchUpInside というイベントがあり、このクラスとイベントが .NET アプリにあるかのようにこのイベントを使用します。

この .NET アプローチに加えて、Xamarin.iOS では、より複雑な対話とデータ バインディングに使用できる別のモデルが公開されています。 この手法では、Apple がデリゲートとプロトコルと呼ぶものを使用します。 デリゲートは概念的には C# のデリゲートに似ていますが、 のデリゲート Objective-C は 1 つのメソッドを定義して呼び出すのではなく、プロトコルに準拠するクラス全体です。 プロトコルは C# のインターフェイスに似ていますが、そのメソッドは省略可能です。 たとえば、UITableView にデータを設定するには、UITableView が自身を設定するために呼び出す UITableViewDataSource プロトコルで定義されているメソッドを実装するデリゲート クラスを作成します。

この記事では、次のような Xamarin.iOS でのコールバック シナリオを処理するための強固な基盤を提供し、これらすべてのトピックについて説明します。

  • イベント – UIKit コントロールでの .NET イベントの使用。
  • プロトコル – プロトコルとその使用方法を学習し、マップ注釈のデータを提供する例を作成します。
  • デリゲート – マップの例を拡張して注釈を含むユーザー操作を処理し、強いデリゲートと弱いデリゲートの違いを学習し、それぞれのデリゲートを使用するタイミングを学習することで、デリゲートについて Objective-C 学習します。

プロトコルとデリゲートを示すために、次に示すように、マップに注釈を追加する単純なマップ アプリケーションを構築します。

マップに注釈を追加する単純なマップ アプリケーションの例 マップに追加された注釈の例

このアプリに取り組む前に、UIKit の下にある .NET イベントを確認することから始めましょう。

UIKit を使用した .NET イベント

Xamarin.iOS では、UIKit コントロールで .NET イベントが公開されます。 たとえば、UIButton には TouchUpInside イベントがあります。これは、C# ラムダ式を使用する次のコードに示すように、.NET で通常と同じように処理します。

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

次のような C# 2.0 スタイルの匿名メソッドを使用してこれを実装することもできます。

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

上記のコードは、UIViewController の ViewDidLoad メソッドでワイヤードされます。 変数は aButton ボタンを参照します。ボタンは、Xcode インターフェイス ビルダーまたはコードで追加できます。

Xamarin.iOS では、コントロールで発生する相互作用にコードを接続するターゲット アクション スタイルもサポートされています。

iOS のターゲット アクション パターンの詳細については、「Apple の iOS 開発者ライブラリの iOS のコア アプリケーション コンピテンシー 」の「Target-Action」セクションを参照してください。

詳細については、「Xcode を使用したユーザーインターフェイスの設計」を参照してください。

events

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;

複数の種類のイベントの監視

UIControlEvent フラグの C# イベントには、個々のフラグへの 1 対 1 のマッピングがあります。 同じコード部分で 2 つ以上のイベントを処理する場合は、 メソッドを使用します 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 1 つのクラスは、C# クラスが複数のインターフェイスを実装できるのと同様に、複数のプロトコルを実装できます。

Apple は iOS 全体でプロトコルを使用して、採用するクラスのコントラクトを定義し、実装するクラスを呼び出し元から抽象化するため、C# インターフェイスと同様に動作します。 プロトコルは、デリゲート以外のシナリオ (次に MKAnnotation 示す例など) とデリゲート (このドキュメントの後半の「デリゲート」セクションで説明します) の両方で使用されます。

Xamarin.ios を使用したプロトコル

Xamarin.iOS のプロトコルを使用する例を Objective-C 見てみましょう。 この例では、フレームワークの MKAnnotation 一部である プロトコルを MapKit 使用します。 MKAnnotation は、それを採用する任意のオブジェクトが、マップに追加できる注釈に関する情報を提供できるようにするプロトコルです。 たとえば、 を実装する MKAnnotation オブジェクトは、注釈の場所とそれに関連付けられているタイトルを提供します。

この方法では、プロトコルを MKAnnotation 使用して、注釈に付随する関連データを提供します。 注釈自体の実際のビューは、プロトコルを採用 MKAnnotation する オブジェクト内のデータから構築されます。 たとえば、(下のスクリーンショットに示すように) ユーザーが注釈をタップしたときに表示される吹き出しのテキストは、プロトコルを Title 実装する クラスの プロパティから取得されます。

ユーザーが注釈をタップしたときの吹き出しのテキストの例

次のセクション「 プロトコルの詳細」で説明されているように、Xamarin.iOS はプロトコルを抽象クラスにバインドします。 プロトコルの MKAnnotation 場合、バインドされた C# クラスの名前はプロトコルの名前を模倣するように指定 MKAnnotation され、CocoaTouch のルート 基底クラス である のサブクラス NSObjectです。 プロトコルでは、座標に対して getter と setter を実装する必要があります。ただし、タイトルとサブタイトルは省略可能です。 したがって、 クラス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));

ここでの map 変数は、 の MKMapViewインスタンスです。これは、マップ自体を表すクラスです。 では MKMapViewCoordinate インスタンスから派生したデータを SampleMapAnnotation 使用して、注釈ビューをマップ上に配置します。

プロトコルは MKAnnotation 、実装するオブジェクト全体で既知の機能セットを提供します。コンシューマー (この場合はマップ) は実装の詳細について知る必要はありません。 これにより、マップにさまざまな注釈を追加する作業が効率化されます。

プロトコルの詳細

C# インターフェイスは省略可能なメソッドをサポートしていないため、Xamarin.iOS はプロトコルを抽象クラスにマップします。 したがって、 の Objective-C プロトコルの採用は、Xamarin.iOS で、プロトコルにバインドされている抽象クラスから派生し、必要なメソッドを実装することによって実現されます。 これらのメソッドは、 クラスの抽象メソッドとして公開されます。 プロトコルの省略可能なメソッドは、C# クラスの仮想メソッドにバインドされます。

たとえば、Xamarin.iOS でバインドされているプロトコルの UITableViewDataSource 一部を次に示します。

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 では、プロトコルで省略可能または必須のメソッドをサポートするために、クラスが抽象になります。 ただし、プロトコル (または C# インターフェイス) とは異なり Objective-C 、C# クラスは複数の継承をサポートしていません。 これは、プロトコルを使用する C# コードの設計に影響し、通常は入れ子になったクラスにつながります。 この問題の詳細については、このドキュメントの後半の「デリゲート」セクションで説明します。

GetCell(…)は抽象メソッドであり、セレクターにObjective-Cバインドされています。これはプロトコルtableView:cellForRowAtIndexPath:UITableViewDataSource必須メソッドです。 セレクターはメソッド名の Objective-C 用語です。 必要に応じて メソッドを適用するために、Xamarin.iOS では抽象として宣言します。 もう 1 つのメソッド である NumberOfSections(…)は に numberOfSectionsInTableview:バインドされます。 このメソッドはプロトコルでは省略可能であるため、Xamarin.iOS では仮想として宣言されるため、C# でオーバーライドすることは省略可能です。

Xamarin.iOS では、すべての iOS バインディングが処理されます。 ただし、 から Objective-C プロトコルを手動でバインドする必要がある場合は、 でクラス ExportAttributeを装飾することでこれを行うことができます。 これは、Xamarin.iOS 自体で使用されるのと同じメソッドです。

Xamarin.iOS で型をバインドObjective-Cする方法の詳細については、バインディングの種類に関する記事Objective-Cを参照してください。

ただし、プロトコルはまだ完了していません。 これらは、次のセクションのトピックであるデリゲートの Objective-C 基礎として iOS でも使用されます。

代理人

iOS では、デリゲートを使用 Objective-C して委任パターンを実装します。このパターンでは、あるオブジェクトが別のオブジェクトに作業を渡します。 作業を行うオブジェクトは、最初のオブジェクトのデリゲートです。 オブジェクトは、特定の処理が発生した後にメッセージを送信して作業を行うようにデリゲートに指示します。 でこのようなメッセージを送信することは、C# で Objective-C メソッドを呼び出すことと機能的に同じです。 デリゲートは、これらの呼び出しに応答してメソッドを実装するため、アプリケーションに機能を提供します。

デリゲートを使用すると、サブクラスを作成しなくてもクラスの動作を拡張できます。 iOS のアプリケーションでは、重要なアクションが発生した後、あるクラスが別のクラスにコールバックするときにデリゲートを使用することがよくあります。 たとえば、ユーザーが MKMapView マップ上の注釈をタップすると、クラスはデリゲートにコールバックし、デリゲート クラスの作成者にアプリケーション内で応答する機会を与えます。 この記事の後半の「Xamarin.iOS でデリゲートを使用する例」で、この種のデリゲートの使用例を確認できます。

この時点で、デリゲートで呼び出すメソッドがクラスによってどのように決定されるか疑問に思うかもしれません。 これは、プロトコルを使用する別の場所です。 通常、デリゲートで使用できるメソッドは、採用するプロトコルから取得されます。

デリゲートでのプロトコルの使用方法

以前は、マップへの注釈の追加をサポートするためにプロトコルを使用する方法を確認しました。 プロトコルは、ユーザーがマップ上の注釈をタップした後やテーブル内のセルを選択した後など、特定のイベントが発生した後に呼び出すクラスの既知のメソッド セットを提供するためにも使用されます。 これらのメソッドを実装するクラスは、それらを呼び出すクラスのデリゲートと呼ばれます。

委任をサポートするクラスは、デリゲートを実装するクラスが割り当てられている Delegate プロパティを公開することによって行われます。 デリゲートに実装するメソッドは、特定のデリゲートが採用するプロトコルによって異なります。 メソッドのUITableView場合、プロトコルUIAccelerometerUITableViewDelegate実装します。メソッドの場合は、デリゲートを公開する iOS 全体の他のクラスに 対して などを実装UIAccelerometerDelegateします。

前の MKMapView 例で見たクラスには、 Delegate というプロパティもあります。これは、さまざまなイベントが発生した後に呼び出されます。 の MKMapView デリゲートの型 MKMapViewDelegateは です。 これは、選択後に注釈に応答する例ですぐに使用しますが、最初に強いデリゲートと弱いデリゲートの違いについて説明します。

強力なデリゲートと弱いデリゲート

これまでに見てきたデリゲートは強力なデリゲートです。つまり、厳密に型指定されています。 Xamarin.iOS バインドには、iOS のすべてのデリゲート プロトコルに対して厳密に型指定されたクラスが付属しています。 ただし、iOS には弱いデリゲートの概念もあります。 iOS では、特定のデリゲートのプロトコルに Objective-C バインドされたクラスをサブクラス化する代わりに、NSObject から派生した任意のクラスでプロトコル メソッドを自分でバインドし、ExportAttribute でメソッドを装飾し、適切なセレクターを指定することもできます。 この方法を使用する場合は、 Delegate プロパティではなく WeakDelegate プロパティにクラスのインスタンスを割り当てます。 弱いデリゲートを使用すると、デリゲート クラスを別の継承階層に柔軟に配置できます。 強いデリゲートと弱いデリゲートの両方を使用する Xamarin.iOS の例を見てみましょう。

Xamarin.iOS でのデリゲートの使用例

この例の注釈をタップしたユーザーに応答してコードを実行するには、インスタンスをサブクラス化MKMapViewDelegateして の プロパティにMKMapViewDelegate割り当てます。 プロトコルには MKMapViewDelegate 省略可能なメソッドのみが含まれています。 したがって、すべてのメソッドは、Xamarin.iOS MKMapViewDelegate クラスでこのプロトコルにバインドされている仮想です。 ユーザーが注釈を選択すると、 MKMapView インスタンスはそのデリゲートにメッセージを mapView:didSelectAnnotationView: 送信します。 Xamarin.iOS でこれを処理するには、MKMapViewDelegate サブクラスの メソッドを次のようにオーバーライド DidSelectAnnotationView (MKMapView mapView, MKAnnotationView annotationView) する必要があります。

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 のクラスにバインドされるため、厳密に型指定されたデリゲートを実装するクラスは通常、入れ子になったクラスとして含まれます。

デリゲート クラスの実装を行う場合は、次に示すように、コントローラー内のデリゲートのインスタンスをインスタンス化し、それを の Delegate プロパティにMKMapView割り当てるだけで済みます。

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派生した任意のクラスでメソッドを自分でバインドし、 の MKMapViewプロパティにWeakDelegate割り当てる必要があります。 クラスはUIViewController最終的に (CocoaTouch のすべてのObjective-Cクラスと同様に) からNSObject派生するため、コントローラーで 直接 にバインドされたメソッドをmapView:didSelectAnnotationView:実装し、 にコントローラーをMKMapViewWeakDelegate割り当てるだけで、余分な入れ子になったクラスが不要になります。 次のコードは、この方法を示しています。

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セレクターを間違えた場合は、ランタイムまで見つかりません。

イベントとデリゲート

デリゲートは、.NET でイベントを使用する方法と同様に、iOS のコールバックに使用されます。 iOS API とデリゲートの使用方法 Objective-C を .NET のように見せるために、Xamarin.iOS では、iOS でデリゲートが使用される多くの場所で .NET イベントが公開されます。

たとえば、 が MKMapViewDelegate 選択した注釈に応答した以前の実装は、.NET イベントを使用して Xamarin.iOS に実装することもできます。 その場合、イベントは で MKMapView 定義され、 が呼び出されます DidSelectAnnotationView。 型MKMapViewAnnotationEventsArgsのサブクラスが含まれますEventArgs。 の MKMapViewAnnotationEventsArgs プロパティはView注釈ビューへの参照を提供します。そこから、次に示すように、前と同じ実装を続行できます。

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 で使用される方法が含まれます。 最後に、Xamarin.iOS の観点からデリゲートを調べました Objective-C 。 Xamarin.iOS が厳密に型指定されたデリゲートと弱く型指定されたデリゲートの両方をサポートする方法と、.NET イベントをデリゲート メソッドにバインドする方法について説明しました。