This is a special guest post from the Realm team, written by Andy Dent.

Andy Dent is a C# developer at Realm, working virtually in Copenhagen from the sunny state of Western Australia. When not sweating over code he can be found sweating at kung fu and online at andydent.name.

Today, Realm is announcing a couple of really neat things for Xamarin users, including the 1.0 release of the Realm Mobile Database, and support for Xamarin on the Realm Mobile Platform. With these tools, we wanted to show you how quickly you could make a collaborative app that syncs in realtime, while maybe having a little fun along the way, so we’ll be rewriting the RealmDraw app in Xamarin.

Xamarin introduced Skia Sharp in February 2016 so it was a natural choice when we needed a vector drawing library for our RealmDraw sample. The challenge had already been laid down by the Realm Cocoa Team showing how cool a shared drawing app can be with Realm. We wanted a Xamarin app to match that would also show how easily code can be shared across Android and iOS.

This tutorial shows how to build a simple freehand drawing app using SkiaSharp and then, with the aid of the Realm Mobile Platform (RMP), make it a shared drawing app. It assumes a reasonable familiarity with Xamarin Studio to build a Xamarin app.



Setting Up a Realm Object Server on Azure

Before we get into the app development, here’s a quick guide to setting up a Development server on Azure, mostly following the standard Microsoft guides. We will use the Ubuntu version of the server.

  • Follow the “Create a Linux VM on Azure using the Portal” documentation to create an instance of Ubuntu Server 16.04 LTS.
  • Follow the “Opening ports to a VM in Azure using the Azure portal” guide and allow through port 9080. Note that you do not need to create a new Network Security Group as one has already been created with your server – you will see it contains a definition for port 22 which is why you can use SSH to connect.
  • Use SSH to connect to your server so you have a terminal session.
  • Now we use the steps from Install the Realm Object Server below to install and start the 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

Setup with SkiaSharp

Our tutorial starts with some hardcoded drawing to prove your SkiaSharp installation works, then moves on to how to draw in response to touches, and concludes by integrating Realm. For more background on SkiaSharp, see the Xamarin blog post on SkiaSharp.

There is not yet a simple way to capture complex touch events, within Xamarin Forms, so this tutorial code uses native UI projects. That keeps things a little simpler so you can focus on the drawing code and particularly sharing drawing with RMP.

  • Start with a new Single View project for iOS and Android. We will use a Shared project for the common code.
  • Add the NuGet SkiaSharp.Views. That will also add SkiaSharp.
  • Now clean up each project to remove the template UI and add a SkiaCanvas
  • .

iOS Configuration

  • Open the Main.Storyboard with the visual editor.
  • Remove the UIButton.
  • Select the main view. In the Properties Widget panel change the Name to Canvas and the Class to SKCanvasView.
  • How to Add SkiaSharp to the Storyboard

  • Open ViewController.cs, and remove the Button logic in ViewDidLoad.
  • Add using statements for SkiaSharp and SkiaSharp.Views.iOS.
  • Add an OnPainting method and set it up in ViewDidLoad so your entire file looks like this:
    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);
            }
        }
    }
    

You can now build and run the iOS program and you should see a blue circle drawn on an otherwise white screen.

Android Configuration

To edit the AXML file in Android, for these simple changes, editing the layout files in text format will be easier – you can right-click on them in Xamarin Studio and choose Open With – Source Code Editor.

  • Open the Main.axml file under the Resource/layout directory.
  • Replace the line Button android:id="@+id/myButton” … with
    
    
  • In MainActivity.cs, add using SkiaSharp; and using SkiaSharp.Views.Android;.
  • Remove the standard Button setup in OnCreate.
  • Add an `OnStart` method to create the SKCanvasView so your MainActivity.cs looks like:
    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);
            }
        }
    }
    

You can now build and run that and similarly see SkiaSharp working to draw a blue circle on white background.

Adding Touches to Draw

The app we’re building isn’t a shape vector drawing app but is more like a whiteboard – it tracks your touches and draws straight lines between them. The faster you draw, the more jagged your drawing will become. An Apple Pencil on an iPad Pro makes a fine test environment.

iOS Draw with Touches

All the changes are in ViewController.cs, adding fields to track the path we are building and compensate for device scaling:

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;
}

The painting method now draws a path we have built:

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);
    }
}

To build the path, we need to respond to touch events by scaling points and adding lines:

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 Draw with Touches

All the changes are in MainActivity.cs, adding fields to remember the main view and track the path we are building:

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

The Painting method now only clears at the start, and otherwise draws the path we have built:

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);
    }
}

To build the path up, we track Touch events:

private void OnTouch(object sender, View.TouchEventArgs touchEventArgs)
{
    float fx = touchEventArgs.Event.GetX();
    float fy = touchEventArgs.Event.GetY();
    switch (touchEventArgs.Event.Action & MotionEventActions.Mask)
    {
        case MotionEventActions.Down:
            _path = new SKPath();
            _path.MoveTo(fx, fy);
            break;

        case MotionEventActions.Move:
            _path.LineTo(fx, fy);
            _canvasView.Invalidate();
            break;
     }
}

Remembering Points With Realm

Instead of drawing lines immediately as you touch the screen, we are now going to save them in a local Realm. This means you can quit the application and come back to it later with your drawing being loaded from disk.

Rather than just recording the points in order, we will group them into _Paths_ which are a continuous stroke. This is useful for changing colors and will be essential when we start sharing drawings.

Adding Realm

To begin, you need to add the Realm NuGet package to your iOS and Android projects. This will also add Realm.Database, Fody and a number of support System packages. It looks like a lot but many of them are only used at build time and don’t bulk out your app.

We will use two simple classes to track the drawing.

    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; }
    }

More of the logic now becomes shared – we’re getting more code sharing by stripping logic from the individual UI projects.

Here’s the LiteDrawer.cs entire file. You can see how drawing logic from the touch methods above has now been moved into DrawPaths and DrawAPath which work solely from the saved data in the Realm. These methods cleanly separate the Drawing action from the Input methods that record points.

We do still have a current _drawPath field which is used to remember the path to which points are being added. This could be retrieved from the Realm every time but keeping track of it in memory helps provide responsive drawing.

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 = Realm.GetInstance("FabScribbles"); // local Realm 
        }

        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 AddPoint(SKPoint pt)
        {
            _realm?.Write(() =>
            {
                _drawPath.Points.Add(new DrawPoint { X = pt.X, Y = pt.Y });
            });
        }

        public void StopDrawing(SKPoint pt)
        {
            _realm?.Write(() =>
            {
                _drawPath.Points.Add(new DrawPoint { X = pt.X, Y = pt.Y });
            });
            _drawPath = null;
        }
    }
}

Did you notice a couple of little Realm idioms in there? You can read more about them in the main Realm docs. Most of the Realm SDK uses standard C# collection interfaces and LINQ so you don’t notice when you are using it. The main things to remember are:

  • All updates, adding or modifying data are wrapped in a _realm.Write().
  • Use `All` to iterate through all objects of a given class, eg: foreach (var drawPath in _realm.All()).
  • Iterating through the path’s related points is normal C# foreach (var point in drawPath.Points).

When you build and run this stage of the sample, you will see your drawings continuously accumulate. Quit the app and next time you launch, all your saved scribbles from before immediately draw. Realm is so fast we’re also cheating a bit and just doing these database operations on the UI thread.

iOS Draw With Realm

The ViewController.cs now becomes much more of a Controller just forwarding events to our LiteDrawer.

Start by replacing the local _path field with a LiteDrawer instead.

        private LiteDrawer _drawer;
        public override void ViewDidLoad()
        {
            ...
            _drawer = new LiteDrawer();

The OnPainting method is just a forwarder:

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

The Touches methods now call the Drawer to add data:

        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 Draw With Realm

Similar trimming of the Android MainActivity.cs has it forwarding events to the 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;
            }
        }

Sharing Your Drawings With Realm Mobile Platform

The steps from using a local Realm to synchronised are very small.

  • You need to know where the server is – for now we use a fixed address.
  • Your client app needs to be authenticated to the server, we will cheat and specify a hardcoded address with username/password.
  • Drawing must be triggered by the Realm being updated, not by local touches.
  • Connecting to the Server

    The field _realm is no longer set in the LiteDrawer constructor but has to be created after a series of authentication steps. You can see below the use of Credentials.UsernamePassword – see our Authentication Documents for other methods.

    Instead of a local Realm named _SharedScribbles_ we refer to it on the server as an address passed into new SyncConfiguration(user, new Uri($"realm://{serverIP}/~/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();  
                    });
                }
            }
    

    Reacting to Changes

    Just at the end of LoginToServerAsync above, you can see how we save a live query result _allPaths and subscribe to be notified when it is changed. We retain the _notificationToken as a field of LiteDrawer because otherwise, garbage collection would cause notifications to cease.

    We are only subscribing to the DrawPath objects. Both this app and others sharing the same server will be adding or updating paths, as they draw. In this unsophisticated version, we just redraw everything each time someone adds a single point, somewhere. That means our refresh method doesn’t need to care about the details of what has been changed.

    Remember in the purely local version, refreshing the screen was driven from the _outside inwards_ – a touch event in the GUI code caused a refresh. To trigger those refreshes from the shared code in `LiteDrawer`, it has a property set by the calling app:

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

    As a fun experiment, you can directly open the SharedScribbles Realm on the server with the Realm Browser (running on MacOS). If you delete a DrawPath, it will vanish from all the devices. If you edit its color, they all redraw with that path in a different color.

    Same Code for Local as Shared Realms

    The important thing about the changes to LiteDrawer above are what we didn’t change. There was no need to change any of the code writing paths and points into the Realm because they just work the same way. Once a synchronized Realm is opened, it acts like just another Realm. From the point of view of your application code, there’s no difference between a background thread doing some updates and shared data arriving from the Realm Object Server.

    iOS Draw With RMP

    As you probably expect, the ViewController.cs changes very little. Most importantly, we set the `_drawer.RefreshOnRealmUpdate` and then pass in credentials to login.

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

    The only other changes are removing the two View?.SetNeedsDisplay(); from TouchesMoved and TouchesEnded. Those two calls effectively moved into the _drawer.RefreshOnRealmUpdate.

    Android Draw With RMP

    Exactly the same changes occur in Android’s MainActivity.cs – move the refresh calls from OnTouch and login.

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

    The Final Point

    This article, and the sample code, is like an inverted triangle. Most of the work initially went into the GUI, being able to track touches and draw in response. Adding Realm to manage the data was little more work than adding a couple of classes to manage those data structure, and shifting from immediate drawing to using saved data to draw.

    The final, big step of making your drawings shared live was just a case of connecting to a server and changing how you trigger a screen refresh. Go create — we can’t wait to see what you’ll make with Realm and Xamarin.

    Source Code

    To help you get started and to check progress along the way, the source for this tutorial has been saved in a series of archives:

    1. Empty app with just SkiaSharp added to draw a basic blue circle. Archive
    2. Touches added to immediately scribble. Archive
    3. Your drawing stored in Realm so it’s now persistent. Archive
    4. Full shared drawing with Realm Mobile Platform. Archive

    Full Version Source

    The main RealmDraw sample is also available for Xamarin, along with other frameworks and includes a number of additional features:

    • Shake the device to clear
    • Enter login credentials, with error-handling
    • Optional Active Directory authentication
    • Change colors by tapping different pencils
    • Smooth drawing with various drawing optimizations, treating the immediate drawing separately from server updates
    • Normalise drawings to device scale so they draw fully on all tablets and phones
    • Properly cleanup notification tokens and other event handlers
    • Local persist settings including the last color used.