Tuesday, 13 May 2014

Base64 Images with Xamarin.IOS

Today I was adding the ability to render Base64 encoded image strings to my iOS application which is built using Xamarin.iOS. These images are just small thumbnails and are stored in a small database within my app.

Since iOS 7 it's really easy to render these as UIImage's inside an UIImageView, in objective-c you simply do something along the lines of:

NSData* data = [[NSData alloc] initWithBase64EncodedString:base64String options:0];
UIImage* image = [UIImage imageWithData:data];

With Xamarin.IOS this would translate to:

var image = new NSData(base64image, NSDataBase64DecodingOptions.None);
var uiImage = UIImage.LoadFromData(image);


However running this gives you the following exception:

System.Exception: Could not initialize an instance of the type 'MonoTouch.Foundation.NSData': the native 'initWithBase64EncodedData:options:' method returned nil.


Ouch

As this exception is within the NSData class which is a class that Xamarin generates to map C# to native Objective-C it's fairly safe to say this is a bug. Turns out after a bit of searching it's a known bug and has existed since February 2014. Sounds like it should be fixed by now so I ensured I was up to date, Xamarin.iOS 7.2.3 and alas the issue still occurred.

I was now looking at alternative ways to do this, I could use the NSData.FromUrl method as iOS does recognise the data:image format, the method you use for Base64 Images within HTML Img tags, but it does have a max url length issue which you can quite easily hit with 20+KB images.

Instead I thought lets convert the Base64 string to its byte array representation and then populate NSData from the array.

Simply:

var base64Bytes = Convert.FromBase64String(base64image);
var data = NSData.FromArray (base64Bytes);                            
var uiImage = UIImage.LoadFromData(data);


This works great but I was left thinking, wouldn't it be great if when the bug is resolved the code automatically used the native implementation but for now use the workaround.

To that end I then decided to then use a helper method that would first try the native route and fallback if needed.

Like so:

public static UIImage FromBase64(string base64Data){
  NSData imageData;
  try
   {
      imageData = new NSData(base64Data, NSDataBase64DecodingOptions.None);
   }
   catch (Exception ex)
   {
     if (!ex.Message.StartsWith("Could not initialize an instance of the type 'MonoTouch.Foundation.NSData'"))
     {
       throw;
     }
     var encodedDataAsBytes = Convert.FromBase64String(base64Data);
     imageData = NSData.FromArray(encodedDataAsBytes);  
   }
  return UIImage.LoadFromData(imageData);
  }

Quite simple and does the job.

I've got this in my app currently but I'm unsure if I will leave this in when I finish / publish the app. It hides the bug but at the expense of throwing and catching an exception for each image load who's cost could add up. For now though, until I've proven it to be a performance overhead it's handy workaround that could benefit from a micro-optimisation  :)

Hope this helps.

No comments:

Post a Comment