Search code examples
c#xamarin.iosasync-awaittask-parallel-librarytaskcompletionsource

Unwrapping an async operation and it's async callback in a TaskCompletionSource


I have a method in a service that gets called by my view model to fetch an image. The image is fetched from an external library (iOS API in Xamarin) which uses a callback mechanic instead of being awaitable.

In order to make my method awaitable, I wrap the method in a TaskCompletionSource. The problem though is in the API callback, I need to invoke another method that must return a Task. The completion source sets it's result as a Task<IBitmap>, and I then return the CompletionSource Task, which now becomes Task<Task<IBitmap>> So what I end up with as my final return value is Task<Task<IBitmap>> instead of just Task<Bitmap>.

public Task<IBitmap> GetAlbumCoverImage(IAlbum album)
{
    var assets = PHAsset.FetchAssetsUsingLocalIdentifiers(new string[] { album.Identifier }, null);

    var asset = assets.FirstOrDefault(item => item is PHAsset);
    if(asset == null)
    {
        return null;
    }

    var taskCompletionSource = new TaskCompletionSource<Task<IBitmap>>();
    PHImageManager.DefaultManager.RequestImageForAsset(
        asset, 
        new CoreGraphics.CGSize(512, 512), 
        PHImageContentMode.AspectFit, 
        null, 
        (image, info) => taskCompletionSource.SetResult(this.ConvertUIImageToBitmap(image)));

    return taskCompletionSource.Task;
}

private Task<IBitmap> ConvertUIImageToBitmap(UIImage image)
{
    var imageData = image.AsJPEG().GetBase64EncodedData(Foundation.NSDataBase64EncodingOptions.SixtyFourCharacterLineLength);
    byte[] imageBytes = new byte[imageData.Count()];

    System.Runtime.InteropServices.Marshal.Copy(imageData.Bytes, imageBytes, 0, Convert.ToInt32(imageData.Count()));

    return BitmapLoader.Current.Load(new MemoryStream(imageBytes), 512, 512);
}

How should I go about unwrapping the nested Task, so that I'm only returning a Task<IBitmap>?


Solution

  • You don't need to use TaskCompletionSource<Task<IBitmap>>, use TaskCompletionSource<UIImage> which returns a task that when completed returns an image. Await that task to asynchronously get the result which you can then convert using ConvertUIImageToBitmap:

    public async Task<IBitmap> GetAlbumCoverImage(IAlbum album)
    {
        // ...
        var taskCompletionSource = new TaskCompletionSource<UIImage>(); // create the completion source
        PHImageManager.DefaultManager.RequestImageForAsset(
            asset, 
            new CoreGraphics.CGSize(512, 512), 
            PHImageContentMode.AspectFit, 
            null, 
            (image, info) => taskCompletionSource.SetResult(image)); // set its result
    
        UIImage image = await taskCompletionSource.Task; // asynchronously wait for the result
        return await ConvertUIImageToBitmap(image); // convert it
    }