ぴーさんログ

だいたいXamarin.Formsのブログ

JXUGC #16 Xamarin.Forms Custom Renderer ハンズオン を開催してきました

さる2016/8/10、渋谷のdots.さん会場にて「JXUGC #16 Xamarin.Forms Custom Renderer ハンズオン」というイベントをやってきました。先生役で。

Twitterの反応を見る限り、おおむね好評だったようで何よりです。

イベントページ

JXUGC #16 Xamarin.Forms Custom Renderer ハンズオン - connpass

JXUGC #16 Xamarin.Forms Custom Renderer ハンズオン - dots. [ドッツ]

資料

ハンズオン手順

iOSAndroid、UWP(!) の3プラットフォームに対応しています。

趣旨

Xamarin.FormsのRendererを理解して自由に新規コントロールを定義したり、既存コントロールをカスタムできるようになろう。そのための要点を押さえます。

進め方

必要なコードを全て資料に載せておいて、それをコピペして動かしていくスタイル。先生(私)も実際に作りながら所々に解説を入れていきました。

私は壇上で好きに喋りながらコードをコピペしてるだけなので楽でしたが、詰まってる人を助けるサポートスタッフの方々は大変だったと思います。お疲れさまでした。<(_ _)>

今回の資料は使いまわしできるので需要があればリバイバルとかありかも知れませんねー。

【Xamarin.Forms】ViewRendererと仲良くなるための簡易チュートリアル

この記事はXamarin.Formsの標準コントロールだけでは対応しきれなくなった時、ViewRendererを駆使した独自コントロールで乗り越えるためのチュートリアルです。

目次

Xamarin.Formsコントロールの仕組み

最初にXamarin.Formsのコントロールがどのような仕組みで成り立っているか確認しておきましょう。

Xamarin.Formsでは各プラットフォームのコントロールをラップし、抽象化したコントロールを定義する事でView層コードの共通化を可能としています。 (XAMLで記述できますがWPFのように自由なUIレンダリングができるわけではありません)

この記事内では抽象化されたコントロールの事を Formsコントロール 、各プラットフォームのコントロールの事を Nativeコントロールと呼ぶ事にします。そして、FormsコントロールとNativeコントロールを結びつけるのがRendererです。

Rendererの主な役割はNativeコンロールの生成、Formsコントロールのプロパティ値変更をNativeコントロールへ伝達すること、逆にNativeコントロールのイベトをFormsコントロールに伝播させることなどです。

独自のコントロールを作るには、PCLプロジェクトでFomrsコントロールを定義、各プラットフォームプロジェクトでRendererを定義し、必要に応じて各プラットフォームプロジェクトでカスタムコントロールを定義します。

Xamarin.Forms公開当初は、既存コントロールのちょっとしたカスタマイズでもCustom Rendererが推奨されていました(それしか方法がなかったとも言う)、しかし現在ではそういった場合にはEffectsを利用するのが良いでしょう。

独自のコントロールを作る

さて、概要を把握したところで実際に独自のコントロールを作って表示してみましょう。 今回はBoxViewっぽいもの作ります。

※以降の解説コードではPCLプロジェクト構成を前提として、iOSで実装したサンプルを示します。

Formsコントロールの定義

PCLプロジェクトに Xamarin.Forms.View を継承した MyBoxView を定義。中身はありません。

using System;
using Xamarin.Forms;

namespace ViewRendererTutorial
{
    public class MyBoxView : View
    {
    }
}

Rendererの定義

iOSプロジェクトに MyBoxViewRenderer を定義します。

using System;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
using UIKit;
using ViewRendererTutorial;
using ViewRendererTutorial.iOS;

// FormsコントロールとRendererの対応を宣言
[assembly: ExportRenderer (typeof (MyBoxView), typeof (MyBoxViewRenderer))]

namespace ViewRendererTutorial.iOS
{
    // ViewRendererの型引数にFormsコントロールとNativeコントロールを与える
    public class MyBoxViewRenderer : ViewRenderer<MyBoxView, UIView>
    {
        protected override void OnElementChanged (ElementChangedEventArgs<MyBoxView> e)
        {
            // Nativeコントロールのインスタンス生成はココ!
            if (Control == null) {
                var nativeControl = new UIView ();
                nativeControl.BackgroundColor = Color.Lime.ToUIColor ();
                SetNativeControl (nativeControl);
            }

            base.OnElementChanged (e);
        }
    }
}

Nativeコンロールのインスタンス生成は OnElementChanged 内で行います。 ButtonRendererやImageRendererなどから派生する場合はこの処理は実装済みですが、ViewRendererから派生する場合は自分で書かなくてはなりません。

これまでの実装で独自のコントロールを表示する事ができます。

f:id:ticktack623:20160611114508j:plain

<StackLayout VerticalOptions="Center">
    <local:MyBoxView x:Name="myBoxView" HeightRequest="100" />
    <Label Text="ViewRenderer tutorial" VerticalOptions="Center" HorizontalOptions="Center" />
</StackLayout>

Binding可能なプロパティでNaitiveコントロールと連携する

続いて、FormsコントロールにBindablePropertyを追加してみましょう。(BindingはXAMLの華ですからね!)

BoxViewらしくColorPropertyを追加する事にします。 (実は前段までの実装で、BackGroundColorを変えるだけで同じ事ができますが説明のために目を瞑ってください)

Formsコントロールの定義

中身が空だったMyBoxViewにBindablePropertyを追加します。

using System;
using Xamarin.Forms;

namespace ViewRendererTutorial
{
    public class MyBoxView : View
    {
        // BindablePropertyを追加
        public static readonly BindableProperty ColorProperty =
            BindableProperty.Create (nameof (Color), typeof (Color), typeof (MyBoxView), default (Color),
             propertyChanged: (bindable, oldValue, newValue) =>
                    ((MyBoxView)bindable).Color = (Color)newValue);

        public Color Color {
            get { return (Color)GetValue (ColorProperty); }
            set { SetValue (ColorProperty, value); }
        }
    }
}

Rendererの定義

using System;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
using UIKit;
using ViewRendererTutorial;
using ViewRendererTutorial.iOS;

[assembly: ExportRenderer (typeof (MyBoxView), typeof (MyBoxViewRenderer))]

namespace ViewRendererTutorial.iOS
{
    public class MyBoxViewRenderer : ViewRenderer<MyBoxView, UIView>
    {
        protected override void OnElementChanged (ElementChangedEventArgs<MyBoxView> e)
        {
            if (Control == null) {
                var nativeControl = new UIView ();
                SetNativeControl (nativeControl);
            }

            if (e.NewElement != null) {
                // Formsコントロールのプロパティ値を反映
                UpdateColor ();
            }

            base.OnElementChanged (e);
        }

        protected override void OnElementPropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged (sender, e);
            
            // プロパティ値の変更を反映
            if (e.PropertyName == MyBoxView.ColorProperty.PropertyName) {
                UpdateColor ();
            }
        }

        private void UpdateColor ()
        {
            if (Element == null)
                return;

            Control.BackgroundColor = Element.Color.ToUIColor ();
        }
    }
}

OnElementChanged で最初のプロパティ値を反映、 OnElementPropertyChanged で変更されたプロパティ値をFormsコントロールに反映させればOKです。

f:id:ticktack623:20160611115325g:plain

<StackLayout VerticalOptions="Center">
    <local:MyBoxView x:Name="myBoxView" HeightRequest="100"
            Color="Lime" />
    <Label Text="ViewRenderer tutorial" VerticalOptions="Center" HorizontalOptions="Center" />
    <Button Text="Change Color" Clicked="ButtonClicked" />
</StackLayout>
Random random = new Random();
void ButtonClicked (object sender, System.EventArgs e)
{
    myBoxView.Color = Color.FromRgb (
        random.Next (255),
        random.Next (255),
        random.Next (255));
}

BindablePropertyの変更が反映されていますね!

NativeコントロールからFormsコントロールにメッセージを送る

MyBoxViewにクリック機能を追加してみましょう。 FormsコントロールにClickedイベントを追加し、Naitiveコントロールがハンドルしたユーザー操作を伝播させます。

クリック対応自体はTapGestureRecognizerを使うとFormsコントロール側だけで完結できますが、これから解説する方法は他にも応用が効きますよ。

Formsコントロールの定義

using System;
using Xamarin.Forms;
using System.Runtime.CompilerServices;

// 指定したassemblyにinternal要素へのアクセスを許可する
[assembly: InternalsVisibleTo ("ViewRendererTutorial.iOS")]

namespace ViewRendererTutorial
{
    public class MyBoxView : View
    {
        public static readonly BindableProperty ColorProperty =
            BindableProperty.Create (nameof (Color), typeof (Color), typeof (MyBoxView), default (Color),
             propertyChanged: (bindable, oldValue, newValue) =>
                    ((MyBoxView)bindable).Color = (Color)newValue);

        public Color Color {
            get { return (Color)GetValue (ColorProperty); }
            set { SetValue (ColorProperty, value); }
        }
        
        // イベントを追加
        public event EventHandler Clicked;

        // Rendererからのシグナルを受け取る
        internal void SendClicked ()
        {
            Clicked?.Invoke (this, EventArgs.Empty);
        }
    }
}

Formsコントロールにイベントを追加、Rendererから発火してもらうために SendClicked メソッドを用意します。 また、internalメソッドを呼んでもらうために InternalsVisibleToAttribute でNativeプラットフォームAssemblyからのアクセスを許可します。

このアプローチはXamarin.Forms標準のButton、ButtonRendererのそれとほぼ同じです。

Rendererの定義

using System;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
using UIKit;
using ViewRendererTutorial;
using ViewRendererTutorial.iOS;

[assembly: ExportRenderer (typeof (MyBoxView), typeof (MyBoxViewRenderer))]

namespace ViewRendererTutorial.iOS
{
    public class MyBoxViewRenderer : ViewRenderer<MyBoxView, UIView>
    {
        protected override void OnElementChanged (ElementChangedEventArgs<MyBoxView> e)
        {
            if (Control == null) {
                var nativeControl = new UIView ();
                SetNativeControl (nativeControl);
                
                // NativeコントロールがタップされたらFormsコントロールにシグナルを送る
                nativeControl.AddGestureRecognizer (
                    new UITapGestureRecognizer (() => Element?.SendClicked ()));
            }

            if (e.NewElement != null) {
                UpdateColor ();
            }

            base.OnElementChanged (e);
        }

        protected override void OnElementPropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged (sender, e);

            if (e.PropertyName == MyBoxView.ColorProperty.PropertyName) {
                UpdateColor ();
            }
        }

        private void UpdateColor ()
        {
            if (Element == null)
                return;

            Control.BackgroundColor = Element.Color.ToUIColor ();
        }
    }
}

RendererはFormsコントロールの事を知っているのでメソッドを叩くのも容易です。

サンプルでは省略していますが、本番ではDisposeのタイミングでイベントハンドラの解除などをきちんと行いましょう。

f:id:ticktack623:20160611115857g:plain

<StackLayout VerticalOptions="Center">
    <local:MyBoxView x:Name="myBoxView" HeightRequest="100"
            Color="Lime"
            Clicked="MyBoxViewClicked" />
    <Label Text="ViewRenderer tutorial" VerticalOptions="Center" HorizontalOptions="Center" />
</StackLayout>
void MyBoxViewClicked (object sender, System.EventArgs e)
{
    DisplayAlert ("ViewRendererTutorial", "MyBoxView Clicked.", "OK");
}

クリックイベントをハンドルできました!

FormsコントロールからNativeコントロールにメッセージを送る

先程とは逆にFormsコントロールからNativeコントロールにメッセージを送る場面を考えてみます。

例えばNativeコントロールを操作するAPIを抽象化するケースがそれにあたります。

サンプルとして簡単なMapコントロールを定義し、地図の表示位置を移動させるAPIを実装してみましょう。 (これはXamarin.Forms.Mapsの実装を簡略化したものです)

Formsコントロールの定義

using System;
using Xamarin.Forms;

namespace ViewRendererTutorial
{
    public class MyMap : View
    {
        public void MoveToResion (double latitude, double longitude)
        {
            // Rendererにメッセージを送る
            MessagingCenter.Send (this, "MyMapMoveToRegion", new Tuple<double, double> (latitude, longitude));
        }
    }
}

Formsコントロールに抽象化したAPIを定義、内部でMessagingCenterを利用してRendererにメッセージを送ります。

Rendererの定義

using System;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
using UIKit;
using MapKit;
using CoreLocation;
using ViewRendererTutorial;
using ViewRendererTutorial.iOS;

[assembly: ExportRenderer (typeof (MyMap), typeof (MyMapRenderer))]

namespace ViewRendererTutorial.iOS
{
    public class MyMapRenderer : ViewRenderer<MyMap, MKMapView>
    {
        protected override void OnElementChanged (ElementChangedEventArgs<MyMap> e)
        {
            if (Control == null) {
                var nativeControl = new MKMapView ();
                SetNativeControl (nativeControl);
            }

            // Formsコントロールからのメッセージを受け取る
            MessagingCenter.Subscribe<MyMap, Tuple<double, double>> (this, "MyMapMoveToRegion",
                                                                     (sender, args) =>  MoveToRegion(args.Item1, args.Item2), Element);
            base.OnElementChanged (e);
        }

        private void MoveToRegion (double latitude, double longitude)
        {
            var mapRegion = new MKCoordinateRegion (
                    new CLLocationCoordinate2D (latitude, longitude),
                    new MKCoordinateSpan (1.0d, 1.0d));
            Control.SetRegion (mapRegion, true);
        }
    }
}

OnElementChanged でFormsコントロールからのメッセージを購読します。 (本当はElement更新時のSubscribe、UnSubscribe、Dispose時のUnSbscribeをしないとダメですよ)

f:id:ticktack623:20160611120428g:plain

FormsコントロールからRenderer経由でNativeコントロールを操作できていますね。

さらなるステップアップ

Xamarin.FormsはOSSなので既存のソースコードを見て勉強することができます。

github.com

FormsコントロールはXamarin.Forms.Core、RendererはXamarin.Forms.Platform.【プラットフォーム】/Renderers にあります。

Xamarin.Formsのソースを読んでクールなコントロールを作りましょう。

【修正版リリース済み】Xamarin.Forms 2.2.0.43を使うとiOSで死ぬっぽい

※本件の修正版(Xamarin.Forms 2.2.0.45)がリリース済みです。そちらを使えば問題ありません。

フォーラムでのアナウンスによると...

Thanks for all of your reports. There was a problem with the build/packaging. The issues are now fixed. Please update to 2.2.0.45

What happened? One of our build machines was set to beta channel and it was 'picked' by our CI to build the Nuget packages. This caused the iOS API mismatch.

Why didn't tests catch this? Our UI tests ran on an OSX machine that was correctly set to the stable channel. We are discussing mistake-proofing guards to prevent mismatches in the future.

https://forums.xamarin.com/discussion/comment/199961/#Comment_199961

どうやらNuGetパッケージのビルドマシンがbetaチャンネルになってた所為で、新しいXamarin iOSをターゲットにしてしまったため、iOS APIを正しく呼べなかったという事みたい。


2016/5/30 現在、Xamarin.Forms 2.2のhotfixである2.2.0.43がリリースされているのですが、 何やら問題があるらしく、iOSで「Method 'CGSize..ctor' not found.」例外を吐いて死ぬ場合があります。

自分が把握している範囲では、どうやらLabelを使うと件の例外が発生する模様。

※追記 ScrollViewも死ぬみたい

f:id:ticktack623:20160530233005j:plain

謎のエラーに苦しめられる人々

対処法

Xamarin.Formsのバージョンを一つ前の2.2.0.31に落としましょう。

Visual Studioの場合、UIでバージョン指定ができるので簡単です。

一方、MacのXamarin Studio(Stable)ではUIでバージョン指定できません。(alfaチャンネルで使える次世代Xamarin StudioではVS同様にUIで選択可能)

検索キーワードに「version:x.y.z」付加すると過去のバージョンをインストールできます。(単に「version:」だけ付加すると全てのバージョンが列挙されるようなのでこっちでもいい)

f:id:ticktack623:20160530213341j:plain

バージョン指定方法を忘れて迷走する人々

早くhotfixのhotfixが来るといいですね。

Realm Xamarinを試してみた

5/10にRealmのXamarin対応版が公開されたので試してみました。

Realm Xamarinを公開! - Realm is a mobile database: a replacement for SQLite & Core Data

RealmはSQLiteやCoreDataから置き換わることを目標とするモバイルデータベースです。

Realm Xamarin自体はまだベータ版といったところですが、データベースエンジン自体は先にリリースされているJava版、Objective‑C版、Swift版と同じらしいので安心ですね。

という訳でRealm Xamarinを試すにあたってSQLiteが使われているTodoアプリのサンプルをRealmバージョンに改造してみました。

ソースコードGitHubに置いてあります。

P3PPP/xamarin-todo-with-realm

使ってみた感想

現時点ではRealmへのLinqクエリサポートが不完全なため満足なフィルタリングが使えません。実践投入するのは少なくともWhererがサポートされてからが良いでしょう。

Realmが管理中のオブジェクトはトランザクション外での変更が禁止されているので、うっかり双方向Bindingに繋げると死にます。編集画面を開いている間中トランザクションを開きっぱなしにするか、編集用のViewModelを用意する事になりそう。

サンプルの改造点

各プロジェクトでNuGetパッケージを追加、更新。 (Xamarin.Forms 2.2.0、Realm 0.74.1)

PCLプロジェクトを修正。

TodoItem.cs

SQLite版ではIDプロパティをオートインクリメントにしていますが、現時点ではRealmがオートインクリメントに対応していないそうなので、intからstringに変更してGUIDを使うことにしました。

TodoItemDatabase.cs

基本的にSQLite DBの操作をRealmに置き換え。

ただし、現時点ではRealmへのLinqクエリでWherer等のサポートが不完全なため、いったんToList()してから改めてフィルタリングしています。

App.cs

TodoItem.IDをstring型に変更した関係でApp.csも一部修正。

Views/TodoItemListX.xaml.cs

SQLite版を踏襲すると TodoItemListX.xaml.csでTodo編集ページのBindingContextにTodoItemを渡すことになります。 そのまままでは、双方向BindingでプロパティSetterが呼ばれて死ぬので一工夫必要です。 (Realmが管理中のRealmObjectはトランザクション外での編集禁止)

今回は編集用のコピーを作ってBindingContextにセットしています。

JXUGC #13 東京でLTしてきました

2016/5/7(土)に開催された「JXUGC #13 東京 緊急開催 Xamarin のすべて!」でLTしてきました。

今回のJXUGカンファレンスは参加人数がとても多く(約150人の定員に倍以上の応募!)、LT枠を含めた参加者は別室に詰めてました。ストリーミングでセッションを観ながらあーだこーだ言ってるのが思いの外楽かったので、「教師&生徒」スタイルではなく参加者全員でワイワイ進める勉強会をやったら面白そうです。

「ごちうサーチ」のアイコンを描きました

本日公開されたMacストアアプリ「ごちうサーチ」のアイコンを描いてました。

Macを使ってる人達は今すぐゲットだ!

ごちうサーチ を Mac App Store で




いきさつ


  • 動画の再生位置を探すアプリ → シークバー
  • ごちうさ → 兎とコーヒー

「シークバーの現在位置がカップに入った兎」というアイディアが浮かんだので作ってみる


Macアプリらしいアイコンの作り方を調べながら試行錯誤...


一晩明け、意匠に「ラテアート」を取り入れることを思いつく

f:id:ticktack623:20160506221815p:plain:w400

(シークバーは破線状に、全体としては真上から見たコーヒーカップのように)


最終的にアプリアイコンになった物がこちら


とまぁ、こんな感じで作られましたとさ。

【Xamarin.Forms 2.3プレビュー】Xamarin.Forms Themesを触ってみた

Evelve 2016で紹介されたXamarin.Forms Themesが(ようやく)NuGetに配信されました。 中身のdllを見る限り、現時点ではiOSAndroidのみ対応しているようですね。

早速試した方がいらっしゃいます!

Xamarinメモ その18 Xamarin.Forms.ThemesをPrism.Unity.Formsと併用する場合の注意 – A certain engineer "COMPLEX"

僭越ながら補足させていただくと...

補足1

App.xamlの追加方法は @omanuke さんの記事を参考にすると良いでしょう。

補足2

「assemblyがロードされない場合」が発生する理由は、XAMLしか参照していないassemblyはリンカが依存関係を検出できないためアプリパッケージにプラットフォーム実装dllが含まれなくなるからです。

よって、コードビハインドでテーマのResourceDictionaryを追加するか、公式ドキュメントのようにダミーコードを入れることで回避できます。単にテーマを使いたいだけならAppクラスのコンストラクタResources = new Xamarin.Forms.Themes.DarkThemeResources(); とするのが簡単です。

Xamarin.Forms Themesの中身

Themesは2種類のNuGetパッケージで構成されています。

  • Xamarin.Forms.Theme.Base
  • Xamarin.Forms.Theme.Light / Dark

それぞれどいう役割なのか少し覗いてみました。

Xamarin.Forms.Theme.Base

Baseには色などのリソースは定義されておらず、以下の物を提供します。

  • 角丸やシャドウなどのスタイルを適用するためのEffect
  • Effectの設定値(影の大きさ等)を保持するためのAttachedProperty

まさに Xamarin.Formsの公式ブログ で紹介されていたようなカスタムスタイルですね。

Xamarin.Forms.Theme.Light / Dark

以下のものを内包したカスタムResourceDictionaryを提供します。

  • カラーテーマ
  • カラーテーマに基づく、標準コントロールのスタイル
  • 標準コントロールの拡張スタイル(角丸Imageなど、StyleClassで適用)
  • DataPagesのコントロール用のDataTemplate

個人的にはDataPagesコントロール用のDataTemplateは別のdllにした方が良いように思うんですが...

ThemesとDataPegesを合わせてAndroidのマテリアルデザインのような物を作ろうとしているのでしょうか。

感想

統一感のあるクールなUIを簡単に構築できるのはとても魅力的です。 一方、リソースキーやスタイルクラスの名前を知らないと、使用やカスタマイズできないのが辛いように思えます。(ドキュメントとして公開されるか、ソース解析しないとわからない。)