眠いしお腹すいたし(´・ω・`)

C#関連を主に書きます。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Azureでリクエストを1件ずつ処理してみる

ごあいさつ

風邪ひきました( д)、;‘.・

はじめに

最近、お仕事でバックエンドの設計とか実装とかしていてAzureを使っているわけですが

こういう時どうしよう、ああいう事したい時にはどうしたら?

みたいな事が沢山でてきてます。

ほとんどは同僚に聞けば解決なのですが

ちょっとは自分で考えましょう、ということで勉強のために実験してみた事を記事にしてみました。

どんなことしてみたの?

パラレルに発生するリクエストに対して順序通りに1件ずつ処理して同期的に返答するにはどうすれば良いか?

という事を考えてみました。

そもそも「そんな設計自体が間違っている」とか「アンチパターンだ」とか思われる方もいらっしゃると思います

もちろん・・・正解です。 

成果物

github.com

設計

こんなイメージ

f:id:tamafuyou:20170819005114j:plain

リクエスト・レスポンスメッセージングパターンに対して不特定な送信者に対応するためにレスポンスメッセージをトッピクによるPub/Subにした感じ。

解説

使用した材料

  • Function App (従量課金プラン) ・・・ 1個 (必須ではない)
  • Function App (AppServiceプランB1)・・・1個   
  • Service Bus (Standard) ・・・ 1個
  • Azureストレージ ・・・1個 (Function Appを作るのに必要)

下準備

ServiceBusにてキューを1個とトピックを1個作っておきます。

HttpTrigger Function

Httpリクエストを受け付けるためにHttp TriggerのFunctionsを使用します。

        [FunctionName("GetRequestTrigger")]
        public static async Task<HttpResponseMessage> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequestMessage req
            , ILogger log)
        {
            var request = new MessageModel();
            var namevalue = req.RequestUri.ParseQueryString();
            foreach (var key in namevalue.AllKeys) {
                if (key.Equals("a", StringComparison.CurrentCultureIgnoreCase))
                    if (int.TryParse(namevalue[key], out int a))
                        request.A = a;
                if (key.Equals("b", StringComparison.CurrentCultureIgnoreCase))
                    if (int.TryParse(namevalue[key], out int b))
                        request.B = b;
            }

            var namespaceManager =
                NamespaceManager.CreateFromConnectionString(
                    ConfigurationManager.AppSettings["servicebusInQueueConnections"]);
            var subscriptionName = Guid.NewGuid().ToString("N");

            await namespaceManager.CreateSubscriptionAsync(new SubscriptionDescription("out", subscriptionName));

            var sbclient =
                SubscriptionClient.CreateFromConnectionString(
                    ConfigurationManager.AppSettings["servicebusInQueueConnections"], "out", subscriptionName);

            var queueclient =
                QueueClient.CreateFromConnectionString(ConfigurationManager.AppSettings["servicebusInQueueConnections"],
                    "in");

            var resultJson = JsonConvert.SerializeObject(request);
            var binary = Encoding.UTF8.GetBytes(resultJson);


            var outMessage = new BrokeredMessage(new MemoryStream(binary));
            var id = Guid.NewGuid().ToString();
            outMessage.To = id;

            await queueclient.SendAsync(outMessage);

            while (true) {
                var message = await sbclient.ReceiveAsync();
                await message.CompleteAsync();
                if (message.To == id) {
                    var stream = message.GetBody<Stream>();
                    var json = new StreamReader(stream).ReadToEnd();

                    await namespaceManager.DeleteSubscriptionAsync("out", subscriptionName);
                    return req.CreateResponse(HttpStatusCode.OK, json);
                }
            }
        }

リクエストを受け付けたら、まずNamespaceManagerを使用してトピックにサブスクリプションを作成します。

そして、作成したサブスクリプションへ接続するクライアントを準備します。

次にキューへの接続クライントを作成します。

BrokeredMessageを作成して送信メッセージを作成してToプロパティにGUIDを設定し、キューへ送信します。

その後、トピックからの受信を待ちます。

トピックからメッセージを受信しToプロパティが送信時にせっていたGUID出会った場合には処理が戻ってきたという事でHttpRequestMessageを戻して終了です。

このファンクションは従量課金プランのFunction Appへデプロイします。

Service Bus Queue Trigger Function

[FunctionName("SumSBTrigger")]
        public static void SumSBTrigger(
            [ServiceBusTrigger("in", AccessRights.Listen, Connection = "servicebusInQueueConnections")] BrokeredMessage
                myQueueItem
            ,
            [ServiceBus("out", Connection = "servicebusInQueueConnections", EntityType = EntityType.Topic)] out
                BrokeredMessage outMessage, ILogger log)
        {
            myQueueItem.RenewLock();
            var stream = myQueueItem.GetBody<Stream>();
            var json = new StreamReader(stream).ReadToEnd();
            var request = JsonConvert.DeserializeObject<MessageModel>(json);


            Thread.Sleep(5000);
            request.Result = request.A + request.B;
            request.ExecuteDateTime = DateTime.Now;

            var resultJson = JsonConvert.SerializeObject(request);
            var binary = Encoding.UTF8.GetBytes(resultJson);

            outMessage = new BrokeredMessage(new MemoryStream(binary));
            outMessage.To = myQueueItem.To;
            myQueueItem.Complete();
        }

特に特別な事はなく

受信したら五秒待機してトピックにアウトプットしています。

トピックにアウトプットするBrokerdMessage.Toに受信したBrokerdMessage.Toを設定しておきます。

このファンクションがもっとも大事な点はAppServiceプランのFunction Appへデプロイするという点とhost.jsonにて以下の設定を行います。

{
  "serviceBus": {
    "maxConcurrentCalls": 1
  }
}

この設定を行う事で1つのインスタンス場でのFunctionの同時実行数が1つとなりシングルスレッド的に処理を行う事ができます。

なぜAppServiceプランのFunction App?

Function Appはスケールコントローラによって自動的にスケールアウト・スケールダウンが行われます。

従量課金プランの場合やAppServiceプランの自動スケールアウトを有効にしていた場合にはインスタンス数の固定化が行えないため負荷によってインスタンス数が増えてしまいます。

AppServiceプランで手動スケールアウトにして1インスタンスに設定し、maxConcurrentCalls = 1に設定する事で完全に1つのプロセスでキューの処理を行う事ができます。

おわりに

こんな事しなくて良い設計を行う事が大事だと思います。

(ノ゚Д゚)八(゚Д゚ )ノイエーイ

NugetPackage License Downloader

プロジェクトで使用しているnugetパッケージのライセンスをテキストファイルに変換してくれるサービスを作ってみました。

Downloader - NugetPackage License Downloader

1つのnugetパッケージに対して1つのテキストファイルが作成されて、zipファイルでまとまめてダウンロードすることができます。

使ってみて不具合等、要望等ありましたらIssue投げてください。

github.com

I tried to create a service that converts license of nuget package used in project into text file.

Downloader - NugetPackage License Downloader

One text file is created for one nuget package and can be downloaded in zip file at once.

If you have problems such as using, if there are demands etc. Please Issue.

TwoWayViewをnugetへ公開しました

github.com

の Xamarin Androidクローンをnugetへ公開しました。

www.nuget.org

プロジェクトサイトはこちらになります。

github.com

サンプルもありますので機会がありましたら使ってみてください。

DroidKaigi2017アプリをベースにXamarin Android開発を考えてたら2か月以上の月日が経過してました(´・ω・`)

はじめに

ジメジメした時期に色々と(´Д`)ハァ…な事が多い日本のXamarin界隈ですが皆さんいかがお過ごしでしょうか?

今回はXamarin Android開発に関して勉強してみたり考えてみた事をジメジメ書き綴ってみようかと思います。

主に頭の中の整理用の記事ですので大多数の方には( ゚Д゚)ハァ?みたいな内容です。

DroidKaigi2017

DroidKaigi2017 凄く良いイベントでした。

本当にキッチリ技術にフォーカスしていて1プログラマとして色々なセッションを聞けてとても面白かったです。

ただ私自身がAndroidアプリを作った経験が少なく分かる事が限られてしまっていたのがとても残念でした。

そこでDroidKaigi2017アプリをXamarinで作成してAndroid開発とXamarin Android開発を両方勉強してv( ̄Д ̄)v イエイしてみようと思い立って約3か月・・・・

ようやく一区切りできたので今回の記事を書いている状況です。

前提条件

今回の取り組みに関する知識・経験の前提を記載します。

  • Xamarin Androidアプリは作った事があるけどFragmentとかViewとかサポートライブラリすら使ったことがなかった。
  • Androidアプリは修正とかした事があるけど,FragmentとかViewとかサポートライブラリすら使っていないアプリだった。
  • Xamarin.Formsを使用したAndroid/iOSクロスプラットフォーム開発はチョットワカル
  • Xamarin.Formsで利用している範囲のAndroidはフワッとわかる。
  • Rxはちょっとわかるけど他ののプラットフォームのRxとかUniRxとかは使ったことない。
  • Javaの言語仕様よく知らない。C#に似てるらしい。
  • Kotlinの言語仕様はよく知らない。なんか新しいらしい。
  • MVVMはくぁwせdrftgyふじこlp

今回目標にした事を記載します。

  • DroidKaigi2017アプリのコードを読んでFragmentとかViewとか使い方と最近のAndroidアプリの作りを理解しよう。
  • DroidKaigi2017アプリをXamarin Androidに移植してXamarin Android開発をしてみよう。
  • モデル層を共通化してDroidKaigi2017 for iOSを作成してみよう(今回未達)

今回の成果

DroidKaigi2017アプリのコードを読んでみた

github.com

メモ取りながら作業をしていなかったので細かい点まで覚えていませんでした。

ですので印象的だったことをピックアップしていきたいと思います。

DI

google.github.io

Dagger2というライブラリを用いてDIを行っていた。

このDagger2というライブラリのパターンが私が普段よく使っている.NETのDIライブラリと使い方がかなり違っていて理解するのに一番時間がかかった。

Dagger2はアノテーションプロセッシングを活用してDIを実現してた。

クラスやメソッドにアノテーション(C#でいうAttribute)をつけることでインジェクションや依存関係を定義する。

私が普段書くプログラムではクラスにてDI設定を定義するはなくて、外部から(コード or Config)定義して使用する書き方をしていたし、それが普通だと思っていたのでビックリした。

どこでコンテナへのインジェクション設定を書いてるんだろうってずっと探してたけど、まさかアノテーション書くだけで実現していたとは・・・

Yukiの枝折: Android: Dagger2

Yukiの枝折: Android: Dagger2 - Subcomponent vs. dependencies

この辺を読んで勉強した。

DaggerはAndroidのライフサイクルに対応するための、特化したDIライブラリという感じだった。

でも結局Daggerの書き方には違和感があってなじまなかった。

私はクラスにDIに必要な要素を書きたくない(´・ω・`)

Rx

RxJava2とRxAndroidというライブラリが使われているみたい。

主に通信を非同期で扱うために使われていた。SingleというObservableの実装があってSingleをSubscribeして結果を受け取った後の処理を書く、みたいな感じだった。

C#でいうところのObservable.FromAsyncでTaskをObservableにしたものって感じ。Nextの次にCompleteが来るやつ。

この使い方ならasync/awaitで書けばよいのに?って思ったらJavaには無いっぽい。だからRxが発達してるんだなぁって理解できた。

通信系はRx使っておくとリトライとか書きやすいのでRxでも良いなぁって思った。

あとはDB操作系でもSingleな戻り値のパターンがあったかな。

retrofit2

これすごかった。

インターフェース書いてアノテーションでURL指定すると、それだけでHttpRequestの実装が終わってる( ゚Д゚)

.NETにも移植したものがあるらしい。

アーキテクチャ

いわゆるクリーンアーキテクチャリポジトリーまでがあってUseCase以降はViewModelとなるパターン。

なんでUseCaseつくらないのかなぁって思ったけど、そもそも単一プラットフォームに対する実装であればUseCase部はViewModelにしちゃえばよいのかなぁって思った。

結構ViewModelでContextを使ってた。ViewModelのライフサイクルがFragmentのライフサイクルと一致していれば特に問題ないんだなぁって感じた。

でもちょっと違和感。それはFragmentとかViewでするべきでは?的な感じがした。

Android Data Binding

xmlのAttributeにViewModelとのバインディングを指定できるすごいやつ。Xamlバインディングとは仕組み的には結構違う。C#はその時にBindingするための処理をするけどAndroidではコンパイル前にバインディングクラスが作られてる。 感覚的にはxamlコンパイルクラスにバインディングの為の関数類も生成されてますって感じ。

なので、単純にバインディングだけじゃなくてコードビハインド的なこともできる(FindViewByIdとか書かなくてよい。)

Converterとかない感じ? 見当たらなかった。そのせいなのかViewModelでViewのプロパティが生えている事が多かった。Visibilityとかリソースとか。

思ったこと

Activity Fragment Viewの使い方は大体わかった。

全体的にアノテーションプロセッシングとかJavaのツールを利用したライブラリが多くて、これをXamarinにそのまま移植するのはハードルが非常に高いと感じた。

Xamarin Androidは、確かにネイティブのAPIは殆どカバーしているけれどJavaの言語仕様や周辺ツール群をカバーしているわけじゃない。

VisualStudioでC#でXamarin Android開発を行う事は単純にJavaの代わりにC#を使うという事ではなくて全く別の開発プラットフォームであると認識したうえで.NETでよく使われるライブラリ群を利用してどのようにAndroid開発にフィットさせるかが大切なのではないか? というところまで考えて、移植作業ではその辺の部分を意識した開発を行ってみた。

DroidKaigi2017アプリをXamarinで再現してみた。

全ての機能を移植していません。セッション周りのみとなります。

他の画面はiOS移植を行う際にForms Embeddingの練習に使おうかと思います。

https://sleepyandhungry1984.tumblr.com/post/162514943270/xamarin-andorid-application
sleepyandhungry1984.tumblr.com

動作はこんな感じです。横画面をキャプチャし忘れましたが問題ないです。

方針

どういう事を実現してみたかったかを書いてみます。

  • Xamarin.Formsアプリでよく使っているパターンで実装してみたかった。
  • できる限りAndroid産のインフラライブラリを使用しないで馴染みの深いライブラリを採用してXamarin Androidにフィットさせてみる。
  • DroidKaigiアプリはconfigChangesを指定していたがArchitecture Componentsの考え方を使えばConfigChangesを指定せずに回転を乗り越えられそうだったので乗り越えてみた。

Architecture Components

コード書き始めた頃に発表になりました。

当然DroidKaigiアプリでは採用されていないんですが、気になったので調べてみて一部をC#に移植してみて適応してみました。

ちゃんとしたArchitecture ComponentsはXamarinチームの凄い人がXamarin環境でも利用できるようにしてくれると思います。

Nyanto

今回作成したインフラ周りはNyantoというプロジェクトにまとめました。

機会があったら独立させてパッケージ化しようかなぁと思いますが使っても私しか幸せになれないのでArchitecture Componentsを使ってください。

このフレームワーク

  • AutofacとArchitectureComponentsのViewModel周りの実装を活用して良い感じにDIを行う
  • ReactivePropertyの利用を行いやすくする。

ということを目的にしています。

ACのViewModelはHolderFragmentというRetainInstance = trueにしたFragmentにViewModelのインスタンスを持たせることによって、画面が回転してActivity/Fragmentのインスタンスが破棄されてもViewModelは維持される、というようなことをしています。

このコードを見た時に、真っ先に思ったことが、なんでViewModelの部分はGenericになっていないのだろう?っていううことでした。

なのでこのViewModel部分をGeneric化してC#に移植し、NyantoではAutofacのILifetimeScopeをあてはめて使用しています。

これにより SingletonはApplicationクラスのライフサイクルと同じで InstancePerLifetimeScopeActivityの存在期間と同じというスコープを実現しました。

またACのLiveDataにあたる部分はReadOnlySwitchReactiveProperty

DroidKaigi2017forXamarin/ReadOnlySwitchReactiveProperty.cs at master · yuka1984/DroidKaigi2017forXamarin · GitHub

というIReacOnlyReactiveProprty<T>の実装を作成することで対応しました。

ReadOnlySwitchReactivePropertyは作成時に通常のIObservable<T>に加えてIObservable<bool>を引数に取ります。

そしてbool IsActiveというプロパティを持ちます。

要するにIsActivetrueの間はいつものReadOnlyReactiveProperty<T>と同じように動作するけどfalseになった時には内部的にValue値は更新されるけどP```ropertyChangedは発生せず値が流れていかない、という動作をします。

またIsActiveがfalseからtrueに変化したときに最新の値を流します。

そしてViewModelBaseには

DroidKaigi2017forXamarin/ViewModelBase.cs at master · yuka1984/DroidKaigi2017forXamarin · GitHub

IObservable<bool>IsActiveObservableというプロパティを持ちます。

IsActiveObservableはNyantoの実装によってViewModelを使用するFragmentが乗っかるActivityのライフサイクルがstart or resumeの時にtrueを流しそれ以外に変化したときにはfalseを流します。

この二つを組み合わせることで画面を更新してはいけない時には更新しない、という仕組みを実現しています。

その他、NyantoにはIObservableとViewのプロパティのバインドを助けるためのExtentionsだったりが含まれています。

Nyanto.ViewSupportTool

FindViewByIdを書くのがつらかったので作りました。

AndroidのLayoutのxmlファイルからViewへのアクセスクラスを生成してみる - Qiita

DroidKaigi2017forXamarin/SessionDetailFragment.cs at master · yuka1984/DroidKaigi2017forXamarin · GitHub

少しは手間を減らせたと思います。

nugetパッケージ化してビルド前に走らせるとか出来るんですけど今回はそこまでしてません。めんどくさいです。

TwoWayView

セッション画面を実現するために使われています。RecyclerViewの実装なのですがGridなLayoutの拡張って感じです。

ここはすっごい苦労しました。 は 一応Bindingしてくれてるgituhubリポジトリがあったんですが、このライブラリはサポートツールのかなり古いバージョンを使用していてサポートライブラリバージョンの辻褄をどうしても合わせられなかったのでフルでC#移植を行いました。

その際にバージョン依存している部分は本家TwoWayViewのIssueに対応方法っぽいものが載っていた採用して移植をしています。最新のサポートライブラリで動作します。

こちらも機会があったらパッケージ化しようかなぁって思ったり思わなかったりしています。めんどくさい。

アーキテクチャ

私の好みのアーキテクチャで実装してます。

私の好きなやつです。以上です。

クリーンアーキテクチャに近い感じでリポジトリをSingleInstanceにしてObserverパターンで全体的に伝搬させるやつですね。

DIを活用して実装しているので結構気軽にインスタンスのライフサイクルの変更にも対応できます。

できる限りTwoWayにならないように設計することを心掛けるようにしています。

バックエンド

本家はGithubjsonを読んだりgoogleフォームにフィードバックを送信したりしてるんですけどフィードバック送っちゃうのはだめだと思うのでバックエンドをAzure MobileAppsのEasyTableに置き換えました。オフライン同期とか使えるんですけど、ちょっと試してみてはまりそうだったのでオフライン同期は使用せずにそれっぽいことをしています。

開発としては最初から置き換えて実装したわけではなくて、最初はGithubからデータを取得するモッククラスやFeedbackの部分はオンメモリで登録したかのように動作するモッククラスを作成してView/ViewModelを作成していって、最後にAzureEasyTableを利用した実装で置き換える。

みたいな感じでいつもやっている開発手法がXamarin Androidでもちゃんとできて、当たり前ではあるけど良い経験ができました。

その他

本家ではViewModelでもContextを扱うような実装だったのですが、個人的にはあまり納得できなかったのでContextを扱う処理はFragmentの方で行うようにしました。

終わりに

細かい点を挙げれば、こうしたい、ああしたい、は沢山あるのですけど、ひとまずの目標を達成したので一区切りで次はiOSネイティブとXamarin iOSの学習に入ろうかと思います。

Xamarin.FormsでLink表現してみた( ̄ー+ ̄)

今回はXamarin.FormsでLink表現をおこなってみました。

LinkerLabelって名前にしています。

完成動画はこち

https://sleepyandhungry1984.tumblr.com/post/162190761683/links-in-label-for-xamarinforms
sleepyandhungry1984.tumblr.com

コードはこち

github.com

スマホアプリでリンク表現が正しいのか・・・

..(/^^)/ ソレハコッチニオイトイテ

表現手段が増えることは良い事です。

利用方法

まずは、このLinkerLabelをどのように使うかのコードを示します。

          <shared:LinkerLabel Text="{Binding BaseText}" 
                              FontSize="15"
                              VerticalOptions="Center" HorizontalTextAlignment="Start"
                              HorizontalOptions="Center"  ItemsSource="{Binding LinkWords}" Command="{Binding LinkCommand}"/>

LinkerLabelはLabelを継承していますので基本的にはLabelです。

Labelとの違いはItemsSourceとCommandのBindablePropertyを持っている事です。

ItemsSourceにIEnumerableを渡してあげると、Text文字列中で、その配列に含まれる文字と一致する部分をLink表現にします。 そしてCommandを設定しておくとLink部分をタップした際にコマンドが実行されコマンドプロパティとしてタップした文字が渡されます。

ItemsSourceは当然INotifyCollectionChangedに対応していますのでItemsSourceを変更すればLink表現も追従します。

今回のサンプルのViewModelはLinkerLabel/MainPageViewModel.cs at master · yuka1984/LinkerLabel · GitHubですが画面から入力した文字列をLinkWordsコレクションに追加する事でLink表現が変化しています。

では実装方法を見ていきましょう。

共通実装部分

LinkerLabel/LinkerLabel.cs at master · yuka1984/LinkerLabel · GitHub

Labelを継承してItemsSourceやCommand、LinkColorのプロパティを増やしてそれに対する実装をおこなっています。

ItemsSourceが変更された際には

        private void UpdateMatchWords()
        {
            var txt = Text;
            var buffer = new List<MatchWord>();
            var sources = ItemsSource.Cast<string>().ToList();
            foreach (var source in sources)
            {
                var matches = Regex.Matches(txt, source);
                if (matches.Count > 0)
                    foreach (Match match in matches)
                        if (!buffer.Any(x => x.StartPosition <= match.Index && x.EndPositon >= match.Index))
                            buffer.Add(new MatchWord
                            {
                                Word = source,
                                StartPosition = match.Index
                            });
            }
            _matchWords = buffer;
            OnPropertyChanged(nameof(MatchWords));
        }

こんな感じでTextからLink箇所を抽出して配列化してinternalなプロパティMatchWordsを設定しています。

Rendererの方でMatchWordsの変更を受け取ってプラットフォーム毎にゴニョゴニョする感じになります。

Androidの実装

コードはこち

LinkerLabel/LinkerLabelRenderer.cs at master · yuka1984/LinkerLabel · GitHub

qiita.com

ほぼ記事そのままの実装なので詳細はこちらの記事を見てください。

要約するとSpannableStringというクラスを使うことでリンク表現できますよ~ってことです。

iOSの実装

コードはこち

LinkerLabel/LinkerLabelRenderer.cs at master · yuka1984/LinkerLabel · GitHub

qiita.com

ほぼ記事そのままの実装なので詳細はこちらの記事を見てください。

要約すると

NSMutableAttributedStringというクラスを使うことでリンク表現できますよ~

Tap検出はUITextView.GetClosestPositionToPointできますよ~

でもLabelのiOSでのプラットフォームコントロールはUILabelなのでできませんよ~

なので仮想のUITextViewを作ってtap検出しましたよ~

っていう感じです。

Xamarin iOSでNSLocationInRange関数が見つからなくて代替手段を考えるのに少し時間がかかりました。

おわりに

今回はしませんでしたがURL検出などを行うようにすればAndroidのLinkifyみたいなこともXamarin.Formsでも可能かもしれませんね。

最近はXamarin Androidでの開発の勉強をしていてDroidKaigi2017アプリをXamarin Androidで書くとどうなるか?みたいな研究をしているのですが、これが本当に難しくて今まで私がXamarin Nativeに思っていた事と現実にはかなりの差があって、考えを改めながら色々と作っている最中です。

ずっとローカルで作業していたのですが最近GitHub上に公開しました。

GitHub - yuka1984/DroidKaigi2017forXamarin

まだまだなのですが、少しずつXamarin Androidでの開発を習得していきたいと思います。

それではまた~(^^)/

Xamarin.FormsでSnackBarを表示してみた

はじめに

今回はAndroidのSnackBarをXamarin.FormsでAndroid/iOSで実装してみました。

完成動画はこちら

コードはこちら

github.com

SnackBarはAndroidのマテリアルデザインライブラリに含まれる表現で、画面下からちょこっと出てきて通知を行ったりToastと違いタップによって動作を行ったりすることができます。

今回はXamairn.FormsでSnackBarをAndroid/iOSで使用してみたいと思います。

共通部分解説

まずは共通ライブラリで使用するインターフェース

public interface ISnackBar
{
    void Show(string text, int duration, string actionText, Action action);
}

本来ならもっとオプション化が必要になりますが、今回はそれは主旨ではないので省きます。

Android実装開設

XamarinForms_SnackBar/AndroidSnackBar.cs at master · yuka1984/XamarinForms_SnackBar · GitHub

AndroidはSnackBarを直接呼び出しているだけなので特に特殊なことはないです。

iOS実装開設

XamarinForms_SnackBar/TouchSnackBar.cs at master · yuka1984/XamarinForms_SnackBar · GitHub

考え方としてはUIViewでSnackBarの見た目を構築してUIView.Animateにて表示アニメーション、時間経過後およびAction実行時に閉じるアニメーションを行っています。

KeyWindowにAddSubViewしているのでXamarin.Formsで画面遷移してもSnackBarは表示されたままになります。

これを例えば表示中のViewに表示したいようなケースの場合にはwindowsのPresentatedViewControllerにAddSubViewすればよいです。

How to acces the current view UIViewController from an external service — Xamarin Forums

SnackBarのレイアウトはLaytouAnchorを使用しています。

iOS 9で追加されたNSLayoutAnchor使うと簡単にわかりやすく間違えずにNSLayoutConstraint(制約)が作れます【Auto Layout】 - Qiita

UIView.Animateでは定義したTopのLayoutAnchorに対してConstantプロパティを変更してAnimationを行っています。

UIView.Animate(OpenDuration, () =>
{
    initialTop.Constant -= BoxHeighy;
    window.LayoutIfNeeded();
});

AndroidのSnackBarは重ねて表示するということがなくSnackBar表示中に別のSnackBarを表示した場合には、まず現在表示中のSnackBarが閉じて新しいSnackBarが開くという動作になるため、iOSでもそのように実装しました。

_addedsnacks にUIViewと表示待機ノ為のTask.Delayに設定したCancellationTokenSourceを保持させておきShowメソッドが呼び出された際にはClearSnackにてDelayをキャンセルさせて表示中SnackBarを閉じ、その後新しいSnackBarを表示します。

private async Task ClearSnack()
{
    var count = _addedsnacks.Count;
    foreach (var added in _addedsnacks)
        added.Item1.Cancel();

    await Task.Delay((int) (CloseDuration * 1000 * count));
}

Actionの実行に関しては引数で受け取ったActionクラスを使って表示待機をキャンセル後Actionを実行しています。

button.TouchUpInside += (sender, e) =>
{
    cancel.Cancel();
    action();
};

SnackBarの利用

めんどくさがってすいません。

もう少しちゃんと実装するならDependencyService使うなりContainer使うなりしてください。

    public partial class SnackBarSamplePage : ContentPage
    {

        public SnackBarSamplePage()
        {
            InitializeComponent();
            SnackButton.Clicked += ButtonOnClicked;
#if __IOS__
             snackbar = new SnackBarSample.iOS.TouchSnackBar();
#else
            snackbar = new SnackBarSample.Droid.AndroidSnackBar();
#endif
        }
        private ISnackBar snackbar = null;

        private void ButtonOnClicked(object sender, EventArgs eventArgs)
        {
            snackbar.Show("Description" + DateTime.Now.ToString(), 2500, "Click", () => { DisplayAlert("alert", "click", "close"); });
        }
    }

おわりに

いかがでしょうか?

こんな感じで割と簡単にアニメーション付きの特殊動作などを実行することができます。

ではでは( `ー´)ノ

Xamarin.FormsでCustomRendererでContentPageをカスタマイズしてみる

はじめに

今回はContentPageのRendererを継承したクラスを作成してContentPageをカスタマイズしてみたいと思います。

完成品はこちら

f:id:tamafuyou:20170513004320g:plain

コードはこちら

github.com

IsBusyプロパティに連動したローディング表現を実装してみます。

以前はに書いた記事ではNavigationPageでIsBusyがTrueになった時に単純にインジケータを動かすだけでしたが

IsBusyがTrueになった時にインジケータの表示に加えてページをスライドアウトさせて非表示にしIsBusyがFalseになった時にはページがスライドインしてくるような動作をAndroid/iOSで書いてみました。

更にIsBusyがTrueになってから1秒のDelay後にローディングが行われるようにしました。

ページの実装

XamarinFormsLoadingPage/MainPage.xaml.cs at master · yuka1984/XamarinFormsLoadingPage · GitHub

Pageでの実装は今回もボタンをクリックするとIsBusyが変化するだけのものです。

ボタンをクリックするとIsBusyがTrueになります。

2500msec後にIsBusyはfalseになります。

その途中にボタンをクリックした場合にはIsBusyはfalseになります。

Androidの実装解説

XamarinFormsLoadingPage/LoadingPageRenderer.cs at master · yuka1984/XamarinFormsLoadingPage · GitHub

PageRendererクラスを継承して実装を行います。

AndroidのPageRendererはVisualElementRendererを継承しているわけですがVisualElementRendererはAndroidのViewGroupを継承しています。

ビューとビューグループ - Android入門

部品(View or ViewGroup)を組み合わせて画面を構成できます。

セットアップ

まずはクラスセットアップとしてOnElementChangedにてAddViewを使ってAndroid.Widget.ProgressBarを追加します。

_progress = new AProgressBar(Context, null, Android.Resource.Attribute.ProgressBarStyleSmall)
{
    Indeterminate = true
};
AddView(_progress);
_progress.Visibility = ViewStates.Invisible;

次にValueAnimatorの設定を行っています。

ValueAnimatorは

AndroidでもiPhoneに負けないようなアニメーションを実装してみよう - Yahoo! JAPAN Tech Blog

アニメーションを実行するために、アニメーション中の値を計算して、それらの値をターゲットオブジェクトに設定するための、簡単なタイミングエンジンを提供します。

というものでこれを使用してIsBusyが変化したときのアニメーション動作を作成ています。

if (animator == null)
{
    animator = ValueAnimator.OfFloat(0f, 1f);
    animator.SetDuration(300);
    animator.Update += (s, a) =>
    {
        var view = GetChildAt(1);
        var width = view.Width;
        var height = view.Height;
        var c = (float) a.Animation.AnimatedValue;
        view.Left = (int) (width * c);
        view.Right = view.Left + width;

        _progress.Alpha = c;
        System.Diagnostics.Debug.WriteLine(c);
        ;
    };
}

0fから1fまで300msecで変化して変化のたびにUpdateイベントを発行してくれるような感じです。

値を使用して画面のスライドアウトとプログレスバーのAlphaを同時に変更しています。

レイアウト

OnLayoutのoverrideにてプログレスバーの位置を調整しています。

真ん中よりちょっと上くらいに表示させます。

var width = r - l;
var woffset = (width - 100) / 2;

var hoffset = (b - t) / 10;

_progress.Layout(l + woffset, t + hoffset, r - woffset, t + hoffset * 4);

IsBusy

OnElementPropertyChangedのoverrideにてIsBusyが変化したときの動作を実装しています。

IsBusyがTrueになった場合ValueAnimatorにStartDelayを設定しStartしています。

IsBusyがFalseになった場合にはValueAnimatorでStartDelayを0にしてReverseしています。

Reverseすることで1f -> 0fにアニメーションしていくのでIsBusyがTrueになった時の逆のアニメーションが行われます。

Pause Resuemeを使用することでアニメーション中にIsBusyの変化が起きた場合に対応します。

    var view = GetChildAt(1);
    if (view != _progress)
    {
        if (animator.IsStarted)
            animator.Pause();
        if (page.IsBusy)
        {
            _progress.Alpha = 0;
            if (!animator.IsPaused)
                animator.StartDelay = 1000;
            else
                animator.Resume();
            animator.Start();
        }
        else
        {
            animator.StartDelay = 0;
            if (animator.IsPaused)
                animator.Resume();
            animator.Reverse();
        }
    }

iOSの実装解説

XamarinFormsLoadingPage/LoadingPageRenderer.cs at master · yuka1984/XamarinFormsLoadingPage · GitHub

PageRendererクラスを継承して実装します。

iOSのPageRendererクラスはUIViewControllerクラスを継承して実装されているためカスタマイズする際にはUIViewControllerでできる事は全てできます。

UIViewControllerまとめ - Qiita

UIViewControllreの位置づけというのはFormsのPageと近いものがあると思います。

ただXamarin.FormsでのUIViewControllerの使われ方という側面でいうと、本来のUIViewControllerの使われ方とは少し異なる感じですので注意が必要です。

セットアップ

UIActivityIndicatorViewをViewにAddSubViewで追加します。

if (_indicator == null)
{
    _indicator = new UIActivityIndicatorView(UIActivityIndicatorViewStyle.Gray);
    View.AddSubview(_indicator);
    Indicatorconstraint(_indicator);
}

レイアウトはLayoutAucherを使って設定します。

iOS 9で追加されたNSLayoutAnchor使うと簡単にわかりやすく間違えずにNSLayoutConstraint(制約)が作れます【Auto Layout】 - Qiita

Xmarin.FormsでいうRelativeLayoutに近い感じで設定できます。

私自身はこちらの方が書きやすくてよいです。

CreateAnchorした後に.Activeプロパティをtrueにしないと制約が確定しませんので注意が必要です。

よく付け忘れて制約が適用されずに悩む事をしてしまうことが多いです。

CenterX CenterYを使うことで中央より少し上くらいに表示します。

TranslatesAutoresizingMaskIntoConstraintsをfalseにしないと制約が適用されません。

UserInteractionEnable = falseとするとタップしても反応せずに背面に透過します。

protected virtual void Indicatorconstraint(UIActivityIndicatorView indicatorView)
{
    indicatorView.CenterXAnchor.ConstraintEqualTo(View.CenterXAnchor).Active = true;
    indicatorView.CenterYAnchor.ConstraintEqualTo(View.CenterYAnchor, -30).Active = true;
    indicatorView.TranslatesAutoresizingMaskIntoConstraints = false;
    indicatorView.UserInteractionEnabled = false;
}

IsBusy

今回はUIView.Animateを使用してみました。

ただこういうケースでは本来はUIView.Transitionを使うのが良いとは思うのですがレイアウト面がまだ使いこなせていないのでAnimateとしました。

iOSアプリ開発でアニメーションするなら押さえておきたい基礎 - Qiita

こちらの記事で学習できます。

UIView.Animateを使用して

  • インジケータのAlphaを0から1fへ変更
  • Pageの中身のView(PageのXAMLiOSのUIViewに変換された様なもの)のAlphaを1fから0fへ変更
  • Pageの中身のviewの位置を0fから1fの値の変化に応じて移動

というアニメーション動作を設定しています。

戻す際には逆動作となります。

if (e.PropertyName == "IsBusy")
{
    var isbusy = (Element as Page).IsBusy;
    var view = View.Subviews.First(x => x != _indicator);
    var distance = view.Frame.Width > view.Frame.Height ? view.Frame.Width : view.Frame.Height;
    view.Layer.RemoveAllAnimations();
    if (isbusy)
    {
        _indicator.Alpha = 0;
        _indicator.StartAnimating();
        UIView.Animate(0.3, 1, UIViewAnimationOptions.CurveEaseIn
            , () =>
            {
                view.Frame = new CGRect(View.Frame.X + distance, View.Frame.Y,
                    view.Frame.Width, view.Frame.Height);
                view.Alpha = 0;
                _indicator.Alpha = 1;
            }
            , null);
    }
    else
    {                    
        UIView.Animate(0.3, 0, UIViewAnimationOptions.CurveEaseOut
            , () =>
            {
                view.Frame = new CGRect(View.Frame.X, View.Frame.Y,
                    view.Frame.Width, view.Frame.Height);
                view.Alpha = 1;
                _indicator.Alpha = 0;
            }
            , () => { _indicator.StopAnimating(); });
    }
}
            

また今回はUIViewControllerのViewからインジケータもPageのViewもRemoveSubViewせずに実装しているのでWillAnimateRotationのoverrideにてIsBusyプロパティに応じてPageのViewの調整を行っています。

このような感じでContentPageのRendererを拡張し様々な事を行うことができます。

ここからかなり詰めていけば、独自のNavigationやUXを実現することも可能です?

ではでは( `ー´)ノ