次の方法で共有


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

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

この .NET アプローチに加えて、Xamarin.iOS では、さらに複雑な操作とデータ バインディングに使用できる別のモデルが公開されています。 この手法では、Apple でデリゲートおよびプロトコルと呼ばれるものを使います。 デリゲートの概念は C# のデリゲートと似ていますが、1 つのメソッドを定義して呼び出す代わりに、Objective-C のデリゲートはプロトコルに準拠するクラス全体です。 プロトコルは 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 Interface Builder またはコードで追加できます。

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

iOS のターゲット アクション パターンについて詳しくは、Apple の iOS 開発者ライブラリで iOS のコア アプリケーション コンピテンシーに関するドキュメントの「ターゲット アクション」セクションをご覧ください。

詳細については、「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;

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

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# のインターフェイスと同様の役割を果たしますが、大きな違いは、プロトコルではオプション メソッドを使用できることです。 オプション メソッドは、プロトコルを採用しているクラスで実装されていない場合は呼び出されません。 また、C# のクラスが複数のインターフェイスを実装できるのと同様に、Objective-C の 1 つのクラスで複数のプロトコルを実装できます。

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

Xamarin.iOS でのプロトコル

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

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

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

次の「プロトコルの詳細」セクションで説明するように、Xamarin.iOS ではプロトコルは抽象クラスにバインドされています。 MKAnnotation プロトコルの場合、バインドされた C# クラスはプロトコルの名前に似た MKAnnotation という名前であり、CocoaTouch のルート基底クラスである NSObject のサブクラスです。 このプロトコルでは、座標のゲッターとセッターを実装する必要がありますが、タイトルとサブタイトルは省略可能です。 したがって、次に示すように、MKAnnotation クラスでは、Coordinate プロパティは abstract で実装する必要があり、TitleSubtitle プロパティは virtual とマークされ省略可能になります。

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

少なくとも Coordinate プロパティが実装されている限り、どのクラスでも MKAnnotation から派生するだけで注釈データを提供できます。 たとえば、コンストラクターで座標を受け取り、タイトルの文字列を返すサンプル クラスを次に示します。

/// <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 をサブクラス化しているすべてのクラスが、バインドされたプロトコルを通じて、注釈のビューの作成時に地図によって使われる関連データを提供できます。 地図に注釈を追加するには、次のコードで示すように、MKMapView インスタンスの AddAnnotation メソッドを呼び出すだけです。

//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 のインスタンスです。 MKMapView は、SampleMapAnnotation インスタンスから派生した Coordinate データを使って、地図上に注釈ビューを配置します。

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

クラスが abstract であることに注意してください。 Xamarin.iOS は、プロトコルのオプションと必須のメソッドをサポートするためクラスを抽象にします。 ただし、Objective-C プロトコル (または C# のインターフェイス) とは異なり、C# のクラスは複数の継承をサポートしていません。 これは、プロトコルを使う C# コードの設計に影響し、通常は入れ子になったクラスになります。 この問題の詳細については、このドキュメントの後半の「デリゲート」セクションで説明します。

GetCell(…) は抽象メソッドであり、Objective-C の "セレクター" である tableView:cellForRowAtIndexPath: にバインドされます。これは、UITableViewDataSource プロトコルの必須メソッドです。 セレクターは、Objective-C でのメソッド名に対する用語です。 そのメソッドを強制的に必須にするため、Xamarin.iOS では abstract と宣言されています。 もう 1 つのメソッド NumberOfSections(…) は、numberOfSectionsInTableview: にバインドされます。 このメソッドはプロトコルではオプションであるため、Xamarin.iOS では virtual として宣言され、C# でのオーバーライドをオプションにしています。

iOS のすべてのバインドは、Xamarin.iOS によって自動的に処理されます。 ただし、Objective-C からプロトコルを手動でバインドする必要がある場合は、ExportAttribute でクラスを修飾してそれを行うことができます。 これは、Xamarin.iOS 自体で使われるのと同じ方法です。

Xamarin.iOS で Objective-C の型をバインドする方法について詳しくは、Objective-C の型のバインドに関する記事をご覧ください。

まだ、プロトコルについてすべて説明したわけではありません。 それらは iOS で Objective-C のデリゲートに対する基礎としても使われており、それが次のセクションのトピックです。

デリゲート

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

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

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

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

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

デリゲートをサポートするクラスは、デリゲートを実装するクラスが割り当てられる Delegate プロパティを公開して、それを行います。 デリゲート用に実装するメソッドは、特定のデリゲートで採用されているプロトコルによって異なります。 UITableView メソッドの場合は UITableViewDelegate プロトコルを実装し、UIAccelerometer メソッドの場合は UIAccelerometerDelegate を実装します。iOS 全体で、デリゲートを公開する他のすべてのクラスについて同じようにします。

前の例で見た MKMapView クラスにも Delegate というプロパティがあり、さまざまなイベントが発生した後で呼び出されます。 MKMapView の Delegate は 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 のクラスにバインドされているため、厳密に型指定されたデリゲートを実装するクラスは、通常、入れ子になったクラスとして含まれます。

デリゲート クラスを実装した後は、次に示すように、コントローラーでデリゲートのインスタンスをインスタンス化し、それを 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 から派生した任意のクラスでメソッドを自分でバインドし、MKMapViewWeakDelegate プロパティに割り当てる必要があります。 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 サブクラスが含まれます。 MKMapViewAnnotationEventsArgsView プロパティでは、注釈ビューへの参照が提供されます。そこから、次に示すように、前と同じ実装を続けることができます。

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 イベントをデリゲート メソッドにバインドする方法を見ました。