Xamarin.Forms トースト(DependencyService)

2015.11.14

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

1 はじめに

前回紹介した、Xamarin.Formsのアラートダイアログでは、「Androidのトーストのようなポップアップは無い」と説明しました。 しかし、「絶対にトーストが利用できない」という訳でも有りません。 各プラットフォームにおけるUIルール的に、問題(違和感)無いのかというのは、ここでは一旦棚上げし、Androidのトースト(Android以外ではトーストのようなもの)をXamarin.Formsから利用する要領について説明します。

2 DependencyService

まずは最初に、今回のサンプルで、Xamarin.Formsでトーストを表示している部分のコードを見てください。

App.cs

// ボタンを生成する
var button = new Button() {
     Text = "Toast"
};

// ボタンにタッチした際に、インターフェースIMyToastのShow()メソッドを呼ぶ
button.Clicked += (s, e) => {
    DependencyService.Get<IMyToast>().Show("Developers.IO");
};

そして、その実行画面が次のとおりです。

iOS Android
001 002

Xamarin.Formsの共通コードで、トースト表示のコーディングをすることで、各プラットフォームで動作するようになっています。

これは、Xamarin.Formsにいくつか用意されている拡張方法のうち、「DependencyService」という方法を使用したものです。

Xamarin.Formsには、トーストのUIが用意されていませんので、とりあえず、インターフェース(プロトコル)だけを定義して、それを使用してコーディングします。 そして、その実際の定義は、各プラットフォームのプロジェクトで実装するという訳です。

DependencyServiceを使用する手順は、概ね次のとおりです。

  • インターフェース定義(共通プロジェクト)
  • インターフェースの実装とDependency指定(各ターゲットごと)
  • インスタンスを取得して実行(共通プロジェクト)

(1) インターフェース定義(共通プロジェクト)

共通プロジェクトで、インターフェースを定義します。

App.cs

//「依存処理」のインターフェース定義
public interface IMyToast {
    void Show(string message);
}

上記では、Show()というメソッドを持つ、IMyToastというインターフェースを定義しました。

(2) インターフェースの実装とDependency指定(各ターゲットごと)

各ターゲットプロジェクトごとにインターフェースの実装を行い、assembly属性でXamarin.Forms.Dependencyを指定します。 なお、属性の記述は、名前空間より外で行う事に注意が必要です。

次のコードは、Androidプロジェクトへ追加したMyToast.csのコードです。 Androidでは、元々「Toast」が有りますので、それをコールしただけです。

MyToast.cs

[assembly: Xamarin.Forms.Dependency(typeof(MyToast))]
namespace App1.Droid {
    class MyToast:IMyToast {
        public void Show(string message) {
            Toast.MakeText(Android.App.Application.Context, message, ToastLength.Short).Show();
        }
    }
}

次のコードは、iOSプロジェクトに追加したコードです。 iOSには、Toastが有りませんので、ちょっと厄介です。 無理やり、トーストに似せたUIを作るしかないのです。 具体的には、UIViewを拡張してメッセージを表示し、タイマーでAlpha値を変更することでフェードアウトさせています。

完全に、Androidと同じというわけでは有りませんが、頑張れは、見分けがつかないぐらいまで書くことも可能でしょう。 と言うより、完全に「自由に設計できる」とうことです。

Toast.cs

[assembly: Xamarin.Forms.Dependency(typeof(MyToast))]
namespace App1.iOS {
    class MyToast:IMyToast {

        public void Show(string message) {
            var toast = new Toast();
            toast.Show(UIApplication.SharedApplication.KeyWindow.RootViewController.View, message);
        }
    }

    // 独自のトーストビューを設計する
    class Toast {
        // トーストビュー本体
        UIView _view;
        // 文字列を表示するためのラベル
        UILabel _label;
        // トーストのサイズ(固定)
        int _margin = 30;
        int _height = 40;
        int _width = 150;

        NSTimer _timer = null;

        public Toast() {
            // トーストビューの生成
            _view = new UIView(new CGRect(0, 0, 0, 0)) {
                BackgroundColor = UIColor.Black,
            };
            _view.Layer.CornerRadius = (nfloat)20.0;
            //  メッセージ表示用のラベル
            _label = new UILabel(new CGRect(0, 0, 0, 0)) {
                TextAlignment = UITextAlignment.Center,
                TextColor = UIColor.White
            };
            _view.AddSubview(_label);

        }

        // 表示開始
        public void Show(UIView parent, string message) {
            // 既に表示中の場合は、処理を停止する
            if (_timer != null) {
                _timer.Invalidate();
                _view.RemoveFromSuperview(); // 親ビューから削除する
            }

            // 当初、アルファ値0.7で表示を開始する
            _view.Alpha = (nfloat)0.7;

            // 親Viewからトーストのサイズを調整する
            _view.Frame = new CGRect(
                (parent.Bounds.Width - _width) / 2,
                parent.Bounds.Height - _height - _margin,
                _width,
                _height);

            _label.Frame = new CGRect(0, 0, _width, _height);
            _label.Text = message; // ラベルの表示文字列を設定する

            parent.AddSubview(_view);

            //タイマー開始
            var wait = 10; // 消え始めるまでのウエイト
            _timer = NSTimer.CreateRepeatingScheduledTimer(TimeSpan.FromMilliseconds(100), delegate {
                // alpha値が0になったらViewのサイズを0にしてタイマーを停止する
                if (_view.Alpha <= 0) {
                    _timer.Invalidate();
                    _view.RemoveFromSuperview(); // 親ビューから削除する
                } else {
                    if (wait > 0) {
                        wait--;
                    } else {
                        _view.Alpha -= (nfloat)0.05;
                    }
                }
            });
        }
    }
}

(3) インスタンスを取得して実行(共通プロジェクト)

共通プロジェクトで、各ターゲットに定義されたクラスのインスタンスを取得するには、DependencyService.Get<T>を使用します。

App.cs

public class App : Application {
    public App() {
        MainPage = new MyPage();
    }
}

class MyPage : ContentPage {
    public MyPage() {
            
        // iOSだけ、上部に余白をとる
        Padding = new Thickness(0, Device.OnPlatform(20, 0, 0), 0, 0);

        // ボタンを生成する
        var button = new Button() {
            Text = "Toast"
        };

        // ボタンにタッチした際に、インターフェースIMyToastのShow()メソッドを呼ぶ
        button.Clicked += (s, e) => {
            DependencyService.Get<IMyToast>().Show("Developers.IO");
        };
        // 画面にボタンを配置する
        Content = new StackLayout {
            Children = { button }
        };
    }
}

3 まとめ

今回は、Xamarin.Formsの拡張方法の1つのDependencyServiceについて紹介しました。

これを見て分かる通り、各プラットフォームでネイティブのフレームワークを利用して拡張できるので、ネイティブで書けることは、ほぼ全部拡張できるということになります。

今回、Windows Phoneのサンプルは有りませんでしたが、要領は同じです。

なお、注意として、Dependencyの定義を忘れたりすると、DependencyService.Get<T>は、インスタンスの取得に失敗してnullを返すので未実装の可能性がある場合は、処置が必要です。

githubサンプルコード(https://github.com/furuya02/Xamarin.Forms.ToastSample)

本記事は、2015年11月14日現在で最新の Xamarin.Forms 1.5.1.6471 を使用して作成されています。

4 参考リンク

Xamarin Developer Accessing Native Features via the DependencyService

この辺でXamarin導入による 効果と限界をしっかり把握してみよう MVP Community Camp 2015

Xamarin記事一覧(SAPPOROWORKSの覚書)