Realm Mobile Platform を使用したリアルタイム データベース

 

Realm は Realm Mobile Database 1.0 のリリースおよび Realm Mobile Platform で Xamarin のサポートを含む、Xamarin ユーザーにとって非常に便利な機能を発表しました。本記事では、Realm と Xamarin を使用して、ユーザーが楽しみながら、リアルタイムで同期するコラボレーション アプリを素早く作成する方法を紹介し、Xamarin ベースの RealmDraw アプリを作り直す方法を紹介します。

Xamarin は 2016年 2月に Skia Sharp を紹介しましたが、RealmDraw サンプルにベクター描画ライブラリーが必要だったので、それは自然な選択でした。Realm Cocoa Team は、その時には既に Realm で共有描画アプリを作成していました。そこで Xamarin アプリで Android および iOS 間のコードを簡単に共有できることを紹介したいと思っていました。

以下のチュートリアルでは、SkiaSharp を使用して簡単なフリーハンドの描画アプリケーションのビルドし、その後、Realm Mobile Platform(RMP)を使用して共有描画アプリケーションを作成する方法を紹介します。

 

 

Azure で Realm Object Server の設定

アプリの開発に入る前に、以下、主な Microsoft の標準ガイドに従った Azure で開発サーバーを設定するための簡単なガイドになります。

  • Create a Linux VM on Azure using the Portal (ポータルを使用して Azure で Linux VM を作成)」ドキュメントに従い、Ubuntu サーバー 16.04 LTS のインスタンスを作成します
  • Opening ports to a VM in Azure using the Azure portal (Azure ポータルを使用して Azure で VM のポートを開放)」のガイドに従い、ポート 9080 を開きます。サーバーに既に Network Security Group がある場合には、新しく作成する必要はありません。SSH を使用して接続するポート 22 の定義が含まれていることが確認できます。
  • SSH を使用してサーバーに接続し、ターミナル セッションを確立します。
  • 最後に、以下の Realm Object Server のインストールの手順に従って、Realm Object Server を起動します。

# Download the Realm Object Server repository from PackageCloud
curl -s https://packagecloud.io/install/repositories/realm/realm/script.deb.sh | sudo bash

# Update repositories (may not be necessary on a new server) sudo apt-get update
# Install the Realm Object Server sudo apt-get install realm-object-server-developer
# Enable and start the service sudo systemctl enable realm-object-server sudo systemctl start realm-object-server

 

SkiaSharp による設定

このチュートリアルは、ハード コードされた描画から始まり、SkiaSharp のインストール方法、タッチに反応する描画方法を紹介し、最後に Realm との統合を紹介します。

Xamarin Forms では、複雑なタッチ イベントを簡単に取り込む方法はまだないため、このチュートリアルのコードではネイティブ UI プロジェクトを使います。

  • iOS と Android の新規の Single View プロジェクトを作成します。共通コード用に共有プロジェクトを使用します。
  • NuGet SkiaSharp.Views を追加します。それと同時に SkiaSharp も追加されます。
  • 最後に、テンプレートの UI を削除し、SkiaCanvas を追加して各プロジェクトを整理します。

iOS の設定

  • ビジュアル エディターで Main.Storyboard を開きます。
  • UIButton を削除します。
  • メイン ビューを選択します。[Properties Widget] パネルで、名前を Canvas に、クラスを SKCanvasView に変更します。

iOS の設定

  • ViewController.cs を開き、ViewDidLoad の Button ロジックを削除します。
  • SkiaSharpSkiaSharp.Views.iOSusing 文を追加します。
  • OnPainting メソッドを追加し、ViewDidLoad で設定すると、ファイル全体が以下のようになります。

using System;
using SkiaSharp;
using SkiaSharp.Views.iOS;
using UIKit;

namespace RealmDrawLite.iOS {
    public partial class ViewController : UIViewController     {

        public ViewController(IntPtr handle) : base(handle)         {
        }
        public override void ViewDidLoad()         {
            base.ViewDidLoad();             Canvas.PaintSurface += OnPainting;
        }
        protected void OnPainting(object sender, SKPaintSurfaceEventArgs e)         {
            var canvas = e.Surface.Canvas;             canvas.Clear(SKColors.White);             var circleFill = new SKPaint {Color = SKColors.Blue};             canvas.DrawCircle(100, 100, 40, circleFill);
        }
    }
}

iOS プログラムをビルドして実行すると、白い画面に青い円が表示されます。


Android の設定

これらの簡単な変更に Android で AXML ファイルを編集するには、テキスト形式でレイアウト ファイルを編集する方が簡単です。Xamarin Studio で右クリックし、[Open With - Source Code Editor] を選択します。

  • Resource/layout ディレクトリ以下にある Main.axml ファイルを開きます。
  • Button android:id="@+id/myButton” … の行を以下に変更します:
<SkiaSharp.Views.Android.SKCanvasView android:id="@+id/canvas" android:layout_width="match_parent" android:layout_height="match_parent" />
  • MainActivity.cs には、using SkiaSharp;using SkiaSharp.Views.Android; を追加します。
  • OnCreate の標準 Button 設定を削除します。
  • `OnStart` メソッドを追加して、SKCanvasView を作成すると MainActivity.cs が以下のようになります:

using Android.App;
using Android.OS;
using SkiaSharp;
using SkiaSharp.Views.Android;

namespace RealmDrawLite.Droid {
    [Activity(Label = "RealmDrawLite", MainLauncher = true, Icon = "@mipmap/icon")]      public class MainActivity : Activity      {
        protected override void OnCreate(Bundle savedInstanceState)         {
          base.OnCreate(savedInstanceState);           SetContentView(Resource.Layout.Main);
        }
        protected override void OnStart()         {
          base.OnStart();           var canvas = FindViewById(Resource.Id.canvas);           canvas.PaintSurface += OnPainting;
        }
        protected void OnPainting(object sender, SKPaintSurfaceEventArgs e)         {
        var canvas = e.Surface.Canvas;         canvas.Clear(SKColors.White);         var circleFill = new SKPaint {Color = SKColors.Blue};         canvas.DrawCircle(100, 100, 40, circleFill);
        }
    }
}

これでビルドして実行することができ、同様に SkiaSharp が白い背景に青い円を描画するのを確認できます。

 

描画にタッチを追加

これからビルドするアプリは、シェイプ ベクター描画アプリケーションではなく、ホワイトボードのようなものであり、タッチをトラックし、タッチ間の直線を描画します。描画速度が速いほど、図面の精度は荒くなります。iPad Pro の Apple Pencil は適切なテスト環境となります。


iOS のタッチによる描画

以下のように、ViewController.cs ですべて変更し、フィールドを追加して、ビルドするパスをトラックし、デバイスのスケーリングを補正します:


private SKPath _path;
private float _devScale;  // usually 2.0 except iPhone 6+, non-retina iPad Mini
public ViewController(IntPtr handle) : base(handle)
{

    _devScale = (float)UIScreen.MainScreen.Scale;
}

これでペインティング メソッドは、ビルドしてきたパスを描画します:


protected void OnPainting(object sender, SKPaintSurfaceEventArgs e)
{

    if (_path != null)     {
        var canvas = e.Surface.Canvas;         var paint = new SKPaint {             Color = SKColors.Blue,             Style = SKPaintStyle.Stroke,             StrokeWidth = 10         };         canvas.DrawPath(_path, paint);     } }

パスをビルドするには、ポイントを拡大して線を追加することによってタッチ イベントに応答する必要があります:


protected SKPoint CG2SKPoint(CoreGraphics.CGPoint p)
{

    return new SKPoint { X = _devScale * (float)p.X, Y = _devScale * (float)p.Y }; {
public override void TouchesBegan(NSSet touches, UIEvent evt) {
    base.TouchesBegan(touches, evt);     _path = null;     var touch = touches.AnyObject as UITouch;     if (touch != null)     {         _path = new SKPath();         _path.MoveTo(CG2SKPoint(touch.LocationInView(View)));     } }
public override void TouchesMoved(NSSet touches, UIEvent evt) {     base.TouchesMoved(touches, evt);     var touch = touches.AnyObject as UITouch;     if (touch != null)     {         _path.LineTo(CG2SKPoint(touch.LocationInView(View)));         View.SetNeedsDisplay();     } }
public override void TouchesEnded(NSSet touches, UIEvent evt) {     base.TouchesEnded(touches, evt);     var touch = touches.AnyObject as UITouch;     if (touch != null)     {         _path.LineTo(CG2SKPoint(touch.LocationInView(View)));         View.SetNeedsDisplay();     } }


Android のタッチによる描画

MainActivity.cs ですべて変更し、フィールドを追加して、メイン ビューを覚え、ビルドするパスをトラックします:


private SKPath _path;
private SKCanvasView _canvasView;

protected override void OnStart() {     base.OnStart();     _canvasView = FindViewById(Resource.Id.canvas);     _canvasView.PaintSurface += OnPainting;     _canvasView.Touch += OnTouch; }

これで初期時のみペインティング メソッドをクリアし、それ以外の場合はビルドしてきたパスを描画します:


protected void OnPainting(object sender, SKPaintSurfaceEventArgs e)
{
    var canvas = e.Surface.Canvas;
    if (_path == null)
        canvas.Clear(SKColors.White);
    else
    {
        var paint = new SKPaint { Color = SKColors.Blue, Style = SKPaintStyle.Stroke, StrokeWidth = 10 };
        canvas.DrawPath(_path, paint);
    }
} 

パスをビルドするために、タッチ イベントをトラックします:

 

Realm でポイントを記憶

スクリーンを触ってすぐに線を描くのではなく、ローカルの Realm に保存します。つまり、アプリケーションを終了し、後でディスクからロードした描画に戻ってくることができます。

順番にポイントを記録するのではなく、それらを連続ストロークである _Paths_ にグループ化します。これは色を変更するのに便利で、描画を共有し始めるのに必要です。


Realm の追加

初めに、Realm NuGet パッケージを iOS と Android のプロジェクトに追加する必要があります。これにより、Realm.DatabaseFody、および多数のサポート System パッケージも追加されます。一見たくさんあるように見えますが、それらの多くはビルド時にのみ使用され、アプリにすべて含まれてサイズが大きくなるわけではありません。

2 つのシンプルなクラスを使用して描画をトラックします。


public class DrawPoint : RealmObject
{
    public double X { get; set; }
    public double Y { get; set; }
}

public class DrawPath : RealmObject {     public string Color { get; set; }     public IList Points { get; } }

これで、より多くのロジックを共有し、個々の UI プロジェクトからロジックを取り除くことで、より多くのコードを共有できます。

以下、LiteDrawer.cs ファイル全体です。上記のタッチ メソッドの描画ロジックが、Realm に保存したデータからのみ動作する DrawPathsDrawAPath へ移動する方法を確認できます。これらのメソッドは、Drawing アクションとポイントを記録する Input メソッドを完全に分離します。

有効な _drawPath フィールドがまだあり、_drawPath フィールドを使用して、ポイントを追加するパスを記憶します。毎回 Realm から取得ができますが、メモリ内でそれをトラックし続けることによって、レスポンシブな描画の提供に役立ちます。


using System;
using SkiaSharp;
using Realms;

namespace RealmDrawLite {
    public class LiteDrawer     {
        private Realm _realm;         private DrawPath _drawPath;
        public LiteDrawer()         {
            // Realm.DeleteRealm(new RealmConfiguration("FabScribbles")); // Uncomment this line to start fresh             // Realm.DeleteRealm(new RealmConfiguration("FabScribbles")); // Uncomment this line to start fresh         }
        public void DrawPaths(SKCanvas canvas)         {             using (var paint = new SKPaint { Style = SKPaintStyle.Stroke, StrokeWidth = 10 })             {                 canvas.Clear(SKColors.White);                 foreach (var drawPath in _realm.All())                     DrawAPath(canvas, paint, drawPath);             }         }
        private void DrawAPath(SKCanvas canvas, SKPaint paint, DrawPath drawPath)         {
            using (var path = new SKPath())             {                 SKColor pathColor;                 SKColor.TryParse(drawPath.Color, out pathColor);                 paint.Color = pathColor; // change the current drawing color to this path                 var isFirst = true;                 foreach (var point in drawPath.Points)                 {                     if (isFirst)                     {                         isFirst = false;                         path.MoveTo(point.X, point.Y);                     }                     else                         path.LineTo(point.X, point.Y);                 }                 canvas.DrawPath(path, paint);             }          }
        public void StartDrawing(SKPoint pt)         {             _realm?.Write(() =>             {                 _drawPath = new DrawPath                 {                     Color = SKColors.Teal.ToString(),                     Points = { new DrawPoint { X = pt.X, Y = pt.Y } }                 };                 _realm.Add(_drawPath);             });         }
        public void StopDrawing(SKPoint pt)         {             _realm?.Write(() =>             {                 _drawPath.Points.Add(new DrawPoint { X = pt.X, Y = pt.Y });             });             _drawPath = null;         }     } }

ここまでに、Realm のイディオムが登場してきました。詳細は、 Realm の ドキュメント を参照してください。

Realm SDK では、標準の C# コレクション インターフェイスおよび LINQ を使用していることが多いので、Real SDK を使用していることに気付かないかと思われます。以下、主な注意項目になります:

  • すべての更新、データの追加または変更を _realm.Write() でラップしています。
  • `All` を使用して、指定したクラスのすべてのオブジェクトを繰り返し使用します。foreach (var drawPath in _realm.All()) など。
  • パスの関連するポイントを繰り返す使用するのは、通常の C# foreach (var point in drawPath.Points) です。

サンプルで、ここまでをビルドして実行すると、描画が連続的に蓄積されるのを確認できます。アプリを終了して、次回の起動時に、前回保存したすべての下書きを即座に描画できます。Realm は、非常に速いので、騙された感覚で、UI スレッドでデータベース操作を行います。


iOS で Realm を使用して描画

これで ViewController.cs は、イベントを LiteDrawer に転送するだけの コントローラーになってきました。

初めに、ローカル _path フィールドを LiteDrawer に置き換えます。


private LiteDrawer _drawer;
public override void ViewDidLoad()
{

    ...     _drawer = new LiteDrawer();

OnPainting メソッドは単なる転送元です:


protected void OnPainting(object sender, SKPaintSurfaceEventArgs e)
{
    _drawer.DrawPaths(e.Surface.Canvas);
}

Touches メソッドは Drawer を呼び出してデータを追加します:


protected SKPoint CG2SKPoint(CoreGraphics.CGPoint p)
{
    return new SKPoint { X = _devScale * (float)p.X, Y = _devScale * (float)p.Y };
}

public override void TouchesBegan(NSSet touches, UIEvent evt) {     base.TouchesBegan(touches, evt);     var touch = touches.AnyObject as UITouch;     if (touch != null)         _drawer.StartDrawing(CG2SKPoint(touch.LocationInView(View))); }
public override void TouchesMoved(NSSet touches, UIEvent evt) {     base.TouchesMoved(touches, evt);     var touch = touches.AnyObject as UITouch;     if (touch != null)     {         _drawer.AddPoint(CG2SKPoint(touch.LocationInView(View)));         View.SetNeedsDisplay();     } }
public override void TouchesEnded(NSSet touches, UIEvent evt) {     base.TouchesEnded(touches, evt);     var touch = touches.AnyObject as UITouch;     if (touch != null)     {         _drawer.StopDrawing(CG2SKPoint(touch.LocationInView(View)));         View.SetNeedsDisplay();     } }


Android で Realm を使用して描画

同様に Android の MainActivity.cs のトリミングは、イベントを Drawer へ転送します:


protected override void OnStart()
{

    ...     _drawer = new LiteDrawer(); }
protected void OnPainting(object sender, SKPaintSurfaceEventArgs e) {     _drawer.DrawPaths(e.Surface.Canvas); }
private void OnTouch(object sender, View.TouchEventArgs touchEventArgs) {     var touchPoint = new SKPoint {         X = touchEventArgs.Event.GetX(),         Y = touchEventArgs.Event.GetY()};     switch (touchEventArgs.Event.Action & MotionEventActions.Mask)     {         case MotionEventActions.Down:             _drawer.StartDrawing(touchPoint);             break;
        case MotionEventActions.Move:             _drawer.AddPoint(touchPoint);             _canvasView.Invalidate();         break;
        case MotionEventActions.Up:             _drawer.StopDrawing(touchPoint);             _canvasView.Invalidate();         break;     } }

 

Realm Mobile Platform による図面の共有

ローカルの Realm の使用から同期までのステップは非常にわずかです。

  • サーバーがどこにあるのか把握する必要があります。今回は固定アドレスを使用します。
  • 通常は、クライアント アプリをサーバーに認証する必要がありまが、今回は省略して、ユーザー名/パスワードでハードコードしたアドレスを指定します。
  • ローカルのタッチではなく、Realm を更新することで、描画を起動する必要があります。

サーバーへの接続

フィールド _realm は、LiteDrawer コンストラクーでは設定されなくなりましたが、一連の認証ステップの後に作成する必要があります。以下、Credentials.UsernamePassword の使用方法を確認できます。他のメソッドについては、Authentication Documents を参照してください。

ローカルの Realm の名前を付けた _SharedScribbles_ ではなく、new SyncConfiguration(user、new Uri($ "realm:// {serverIP} /〜/ SharedScribbles"));) に渡したアドレスとしてサーバーの SharedScribbles を参照します。


private IDisposable _notificationToken;
private IQueryable _allPaths;

public async void LoginToServerAsync(string username, string password, string serverIP) {     User user = null;     try     {         user = User.Current; // if still logged in from last session     }     catch (Exception) { }
    try     {         if (user == null)         {             var credentials = Credentials.UsernamePassword(username, password, createUser: false);             user = await User.LoginAsync(credentials, new Uri($"http://{serverIP}"));         }         var config = new SyncConfiguration(user, new Uri($"realm://{serverIP}/~/SharedScribbles"));         _realm = Realm.GetInstance(config);     }     catch (Exception)     {         return;     }
    if (user != null)     {         _allPaths = _realm.All() ;         _notificationToken = _allPaths.SubscribeForNotifications((sender, changes, error) =>         {             RefreshOnRealmUpdate();         });     } }


変更に対するアクション

上記のように LoginToServerAsync の終わりに、live クエリ結果の _allPaths を保存し、変更時の通知を受けるかを確認することができます。_notificationTokenLiteDrawer のフィールドとして保持しています。そうしないと、ガベージ コレクションによって通知を停止する場合があります。

DrawPath オブジェクトのみを登録しています。このアプリと同じサーバーを共有するその他のアプリの両方で、描画する際に、パスを追加または更新します。この単純なバージョンでは、誰かが単一のポイントをどこかに追加するたびに、すべてを再描画します。つまり、リフレッシュ メソッドでは、何を変更したかの詳細を意識する必要はありません。

純粋なローカル バージョンでは、_outside inwards_ からスクリーンのリフレッシュを呼び出していたことを注意してください (GUI コードのタッチ イベントがリフレッシュする)。`LiteDrawer` の共有コードからこれらのリフレッシュをトリガーするには、以下のように、呼び出し側のアプリケーションによってプロパティを設定します:


internal Action RefreshOnRealmUpdate { get; set; } = () => { };`

面白い実験の 1つとして、(MacOS 上で実行する) Realm Browser があるサーバー上の SharedScribbles Realm を直接開くことができます。DrawPath を削除すると、すべてのデバイスから消えます。色を編集すると、そのパスを別の色で再描画します。


ローカルと共有 Realm で同じコード

上記の LiteDrawer の変更に関して重要なポイントは、変更していないことです。同じように動作するだけなので、Realm へのパスやポイントを記述するコード変更する必要はありません。一度同期した Realm を開くと、別の Realm のように動作します。アプリケーション コードの観点からは、更新を行うバックグラウンド スレッドと Realm オブジェクト サーバーからの共有データ間に違いはありません。


iOS で RMP を使用した描画

期待通り、ViewController.cs の変更はほとんどありません。最も重要なのは、 `_drawer.RefreshOnRealmUpdate` を設定し、ログインするクレデンシャルを渡すことです。


public override void ViewDidLoad()
{

    ...
    _drawer = new LiteDrawer();
     _drawer.RefreshOnRealmUpdate = () => { View?.SetNeedsDisplay(); };
     _drawer.LoginToServerAsync("foo@foo.com", "bar", "192.168.0.51:9080"); }

その他の変更としては、TouchesMovedTouchesEnded からの 2つの View?.SetNeedsDisplay(); を削除しています。これら 2つの呼び出しは、効率的に _drawer.RefreshOnRealmUpdate に移動しました。


Android で RMP を使用して描画

Android の MainActivity.cs でも、まったく同じ変更をします。OnTouch とログインから refresh の呼び出しを移動します。


protected override void OnStart()
{

    ...
    _drawer = new LiteDrawer();
     _drawer.RefreshOnRealmUpdate = () => { _canvasView.Invalidate(); };
     _drawer.LoginToServerAsync("foo@foo.com", "bar", "192.168.0.51:9080"); }

 

まとめ

この記事 (サンプル コードも含む) は、逆三角形のようなものになります。ほとんどの作業は、GUI から入り、タッチと描画のレスポンスをトラックすることができました。Realm を追加してデータを管理するのは、クラスを追加してデータ構造体を管理するよりも、即座に描画から保存したデータを使用して描画へ移行するよりも僅かな作業量となります。

最後に、描画をライブで共有する 大きな ステップは、サーバーに接続し、スクリーンのリフレッシュをトリガーする方法を変更するケースとなります。Realm と Xamarin を使用して、作成しましょう。


ソース コード

始める前に、進捗状況の確認に役立つ、このチュートリアルで使用したソースは、以下の一連のアーカイブに保存されています:

  1. エンプティ アプリに SkiaSharp を追加して、ベーシックな青い円を描画。アーカイブ
  2. タッチを追加して即座に描画。アーカイブ
  3. Realm に描画を保存して、永久的な描画。アーカイブ
  4. Realm Mobile Platform 完全に共有し描画。アーカイブ

フル バージョンのソース

主な RealmDraw サンプルは、他のフレームワークと一緒に Xamarin で利用可能であり、以下の多くの追加機能が含まれています:

  • デバイスをシェイクしてクリア
  • ログイン情報の入力およびエラー処理
  • Active Directory の認証オプション
  • 複数の鉛筆でタップして色を変更
  • さまざまな描画の最適化でスムーズな描画、サーバーの更新とは別に即座に描画を処理
  • 描画をデバイスのスケールに標準化し、すべてのタブレットやスマホで完全に描画
  • 通知トークンと他のイベント ハンドラーを適切にクリーンアップ
  • 使用し最後の色を含む設定をローカルで保持

 


Infragistics Infragistics
エンタープライズ向け統合 UI 開発コンポーネント。WinForms、モバイル、Web 用の UI コントロール。
CData ドライバー CData ドライバー
50 以上のデータ ソースへのアクセスをプログラミングなしで可能にするデータベース ドライバー。
SmartBear Software SmartBear
GUI テスト / プロファイラー / 負荷テスト / API テスト: ソフトウェア テストの自動化/工数削減/品質向上。
/n software IP*Works! /n software IP*Works!
クロスプラットフォーム対応のインターネット アプリケーション開発向けコンポーネント スイート。
UXDivers Grial UI Kit
Xamarin Forms 対応の XAML ベースの UI、UX テンプレートを提供
XFINIUM.PDF XFINIUM.PDF
Xamarin 対応のクロスプラットフォーム PDF 開発ツール
Aspose Aspose
.NET/Java で Word、Excel、PowerPoint、PDF などの Office ファイルを操作できる API ライブラリ。
Visual Studio Microsoft Visual Studio
最新の統合開発環境!アプリケーションの迅速かつ高品質な構築を支援する開発環境を提供。