Xamarin.iOS での UI スレッドの操作

アプリケーションのユーザー インターフェイスは、マルチスレッド デバイスであっても常にシングル スレッドです。画面の表現は 1 つだけであり、表示される内容に対する変更は、1 つの "アクセス ポイント" を介して調整される必要があります。 これにより、(たとえば) 複数のスレッドが同じピクセルを同時に更新することがなくなります。

コードでは、メイン (または UI) スレッドからのみ、ユーザー インターフェイス コントロールを変更する必要があります。 別のスレッド (コールバックやバックグラウンド スレッドなど) で発生する UI の更新は、画面にレンダリングされない場合があり、クラッシュの原因となる可能性もあります。

UI スレッドの実行

ビューでコントロールを作成したり、タッチなどのユーザー開始イベントを処理したりしているとき、コードは既に UI スレッドのコンテキストで実行しています。

コードがタスクまたはコールバックのバックグラウンド スレッドで実行している場合は、メイン UI スレッドで実行していない可能性があります。 この場合は、次のように InvokeOnMainThread または BeginInvokeOnMainThread の呼び出しにコードをラップする必要があります。

InvokeOnMainThread ( () => {
    // manipulate UI controls
});

InvokeOnMainThread メソッドは NSObject で定義されているため、任意の UIKit オブジェクト (ビューやビュー コントローラーなど) で定義されているメソッド内から呼び出すことができます。

Xamarin.iOS アプリケーションのデバッグ中に、コードが間違ったスレッドから UI コントロールにアクセスしようとすると、エラーがスローされます。 これは、InvokeOnMainThread メソッドでこれらの問題を追跡して修正するのに役立ちます。 これはデバッグ中にのみ発生し、リリース ビルドではエラーはスローされません。 次のようなエラー メッセージが表示されます。

UI Thread Execution

バックグラウンド スレッドの例

単純なスレッドを使ってバックグラウンド スレッドからユーザー インターフェイス コントロール (UILabel) へのアクセスを試みている例を次に示します。

new System.Threading.Thread(new System.Threading.ThreadStart(() => {
    label1.Text = "updated in thread"; // should NOT reference UILabel on background thread!
})).Start();

そのコードでは、デバッグ中に UIKitThreadAccessException がスローされます。 この問題を修正するには (そして、ユーザー インターフェイス コントロールがメイン UI スレッドからのみアクセスされるようにするには)、次のように、UI コントロールを参照するコードを InvokeOnMainThread 式の内部にラップします。

new System.Threading.Thread(new System.Threading.ThreadStart(() => {
    InvokeOnMainThread (() => {
        label1.Text = "updated in thread"; // this works!
    });
})).Start();

このドキュメントの例の残りの部分ではこれを使う必要はありませんが、覚えておく必要がある重要な概念であり、アプリでネットワーク要求を行うときは、通知センターを使うか、別のスレッドで実行される完了ハンドラーを必要とする他の方法を使います。

async/await の例

C# 5 の async/await キーワードを使うときは、待機中のタスクが完了したとき、メソッドは呼び出し元のスレッドで続行されるため、InvokeOnMainThread は必要ありません。

このコード例 (Delay メソッドの呼び出しで待機しており、これはあくまでもデモのためです) では、UI スレッド (TouchUpInside ハンドラーです) で呼び出される async メソッドを示します。 含んでいる側のメソッドが UI スレッドで呼び出されるため、UILabel でのテキストの設定や UIAlertView の表示などの UI 操作は、バックグラウンド スレッドで非同期操作が完了した後、安全に呼び出すことができます。

async partial void button2_TouchUpInside (UIButton sender)
{
    textfield1.ResignFirstResponder ();
    textfield2.ResignFirstResponder ();
    textview1.ResignFirstResponder ();
    label1.Text = "async method started";
    await Task.Delay(1000); // example purpose only
    label1.Text = "1 second passed";
    await Task.Delay(2000);
    label1.Text = "2 more seconds passed";
    await Task.Delay(1000);
    new UIAlertView("Async method complete", "This method", 
               null, "Cancel", null)
        .Show();
    label1.Text = "async method completed";
}

非同期メソッドが (メイン UI スレッドではなく) バックグラウンド スレッドから呼び出される場合は、やはり InvokeOnMainThread が必要です。