UIAlertController を async/await 対応させて便利に使う

 これは Xamarin Advent Calendar 2014 23日目 の記事です。

 なんか空いてたのでエントリーしましたが、急だったので軽い話です。

の焼き直しみたいなものです。

 iOS8 では、UIAlertDialog が非推奨になり、代わりに UIAlertController を使えとのこと。

 普通に使うとこうなります。

button1.TouchUpInside += (sender, e) => 
{
    var alert = UIAlertController.Create("", "こんぼう をすてますか?", UIAlertControllerStyle.Alert);
    alert.AddAction(UIAlertAction.Create("はい", 
        UIAlertActionStyle.Default, x=> label1.Text = "こんぼう をすてました")); 
    alert.AddAction(UIAlertAction.Create("いいえ",  
        UIAlertActionStyle.Default, x=> {})); 
     
    this.PresentViewController(alert, true, null);
};

 このくらいなら問題ありません。

 次に、こんぼう をすてる前にもう一度問いかけるようにします。 2つ目の UIAlertController が入れ子になってしまって見づらい、 残念な感じ です。

button1.TouchUpInside += (sender, e) => 
{
    var alert = UIAlertController.Create("", "こんぼう をすてますか?", UIAlertControllerStyle.Alert);
    alert.AddAction(UIAlertAction.Create("はい", 
        UIAlertActionStyle.Default, x=> 
        {
            // 念押しの確認ダイアログ(入れ子でつらい
            var alert2 = UIAlertController.Create("", "ほんとうにすてますか?", UIAlertControllerStyle.Alert);
            alert2.AddAction(UIAlertAction.Create("もちろん", UIAlertActionStyle.Default, _=> 
            {
                label1.Text = "こんぼう をすてました"
            }));
        alert2.AddAction(UIAlertAction.Create("やめる",  UIAlertActionStyle.Default, _=> {})); 

        // アラート2の表示
        this.PresentViewController(alert2, true, null);
    })); 

    // アラート1の表示
    alert.AddAction(UIAlertAction.Create("いいえ",  UIAlertActionStyle.Default, x=> {})); 

    this.PresentViewController(alert, true, null);
};

 Objective-C や Swift なら、ここで打つ手は今のところ無いでしょう。 しかし Xamarin には、C# には async/await がありまぁす! アラートの表示を async/await(というか Task)対応してみましょう。

private Task<int> ShowDialog(string message, string button1Title, string button2Title)
{
    var comp = new TaskCompletionSource<int>();

    var alert = UIAlertController.Create("", message, UIAlertControllerStyle.Alert);
    alert.AddAction(UIAlertAction.Create(button1Title, UIAlertActionStyle.Default, x=> 
    {
        comp.SetResult(1); // OKボタン
    })); 
    alert.AddAction(UIAlertAction.Create(button2Title,  UIAlertActionStyle.Default, x=> 
    {
        comp.SetResult(0); // Cancel
    })); 

    this.PresentViewController(alert, true, null);

    return comp.Task;
}

Task<int> を返すメソッド ShowDialog です。UIAlertController のボタンが押されたら SetResult して Task の値を決定します。

 このメソッドを使う方は、こうなります。

button1.TouchUpInside += async (sender, e) => 
{
    if (await ShowDialog("こんぼう をすてますか?", "はい", "いいえ") == 0) 
        return;

    if (await ShowDialog("ほんとうにすてますか?", "もちろん", "やめる") == 0) 
        return;

    label1.Text = "こんぼう をすてました";
};

なんて見やすいコードになったことでしょう。すばらしい!

入れ子でなく、フラットに書けるので、こんな事もできます。

button1.TouchUpInside += async (sender, e) => 
{
    while (await ShowDialog("こんぼう をすてますか?", "はい", "いいえ") == 1) 
    {
        label1.Text = "それをすてるなんてとんでもない!";
    }

	label1.Text = "すてるのをやめました";
};

こんぼうを捨てるのをあきらめるまで、なんどでも聞いてきます。 コールバックスタイルのメソッドでループとか、ベタに書くと頭痛いです。

動かすとこんな感じです。

ShowDialog は拡張メソッドとして作成しておくと、呼び出しに便利かもしれません。 コールバックスタイルの機能を、Task化するパターンはよく使いそうな気がします。TaskCompletionSource、覚えておきましょう。