Monotouch
Asynchronous
Image Downloader
Image Cache
Mobile Development

Asynchronous image downloader/cache for Monotouch

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In MonoTouch style iOS apps, image loading needs three things to feel correct: it must not block the UI thread, it should avoid downloading the same image repeatedly, and it must cope with view reuse so the wrong image does not flash into a recycled cell. An asynchronous image downloader with caching solves all three problems when it is built around memory cache, in-flight request reuse, and main-thread UI updates.

The Basic Design

A practical image loader usually has these pieces:

  • an HttpClient for download
  • an in-memory cache for already decoded images
  • optional disk cache for persistence
  • a way to deduplicate concurrent requests for the same URL

Here is a compact C# example that works as the core of a MonoTouch or Xamarin.iOS image service:

csharp
1using System;
2using System.Collections.Concurrent;
3using System.Net.Http;
4using System.Threading.Tasks;
5using Foundation;
6using UIKit;
7
8public sealed class ImageCache
9{
10    private readonly HttpClient _httpClient = new HttpClient();
11    private readonly NSCache _memoryCache = new NSCache();
12    private readonly ConcurrentDictionary<string, Task<UIImage>> _inFlight =
13        new ConcurrentDictionary<string, Task<UIImage>>();
14
15    public Task<UIImage> GetImageAsync(string url)
16    {
17        var cached = _memoryCache.ObjectForKey(new NSString(url)) as UIImage;
18        if (cached != null)
19        {
20            return Task.FromResult(cached);
21        }
22
23        return _inFlight.GetOrAdd(url, DownloadAndCacheAsync);
24    }
25
26    private async Task<UIImage> DownloadAndCacheAsync(string url)
27    {
28        try
29        {
30            var bytes = await _httpClient.GetByteArrayAsync(url);
31            var data = NSData.FromArray(bytes);
32            var image = UIImage.LoadFromData(data);
33
34            if (image != null)
35            {
36                _memoryCache.SetObjectforKey(image, new NSString(url));
37            }
38
39            return image;
40        }
41        finally
42        {
43            _inFlight.TryRemove(url, out _);
44        }
45    }
46}

The important detail is _inFlight. If several cells ask for the same URL at once, they share one download task instead of starting several identical network calls.

Update the UI on the Main Thread

iOS UI objects must be updated on the main thread. A table view cell might use the cache like this:

csharp
1public async Task BindAsync(string imageUrl, UIImageView imageView, ImageCache cache)
2{
3    imageView.Image = UIImage.FromBundle("placeholder.png");
4
5    var image = await cache.GetImageAsync(imageUrl);
6    if (image == null)
7    {
8        return;
9    }
10
11    UIApplication.SharedApplication.BeginInvokeOnMainThread(() =>
12    {
13        imageView.Image = image;
14    });
15}

This keeps network and decoding work off the UI thread while ensuring the final assignment is safe.

Handle Cell Reuse Correctly

Table and collection views reuse cells. That means a cell might start loading image A, get reused, and then incorrectly display image A inside row B once the download finishes.

A simple defense is to store the expected URL on the cell:

csharp
1public class PhotoCell : UITableViewCell
2{
3    public string CurrentImageUrl { get; set; }
4}

Then verify it before assigning:

csharp
1public async Task BindAsync(PhotoCell cell, string imageUrl, ImageCache cache)
2{
3    cell.CurrentImageUrl = imageUrl;
4    cell.ImageView.Image = UIImage.FromBundle("placeholder.png");
5
6    var image = await cache.GetImageAsync(imageUrl);
7    if (image == null || cell.CurrentImageUrl != imageUrl)
8    {
9        return;
10    }
11
12    UIApplication.SharedApplication.BeginInvokeOnMainThread(() =>
13    {
14        cell.ImageView.Image = image;
15        cell.SetNeedsLayout();
16    });
17}

That small check prevents a lot of visual bugs.

Add Disk Cache When Needed

Memory cache is fast, but it disappears when the app is terminated or memory pressure clears it. For heavier image usage, save downloaded bytes to disk and check disk before going to the network.

The pattern is:

  1. look in memory
  2. look on disk
  3. download if missing
  4. store in both caches

Disk caching helps especially for large lists, avatars, and images revisited across screens.

Why Not Just Download Directly in the Cell

It is tempting to put HttpClient calls directly into view code, but that usually causes:

  • duplicate downloads
  • no shared cache
  • harder cancellation and reuse handling
  • UI code mixed with networking concerns

A dedicated image service gives you one place to handle caching, logging, reuse, and fallback behavior.

Common Pitfalls

The biggest pitfall is setting the image directly after await without checking whether the cell was reused for a different URL.

Another common issue is updating UIKit controls from a background thread. Network code can run off the main thread, but UI assignment should come back to the main thread.

People also often cache raw bytes but forget that decoding the image repeatedly still costs time. Caching decoded UIImage instances in memory helps a lot for scrolling performance.

Finally, do not ignore duplicate requests. Without in-flight request deduplication, a fast scroll through a table can trigger many identical downloads for the same image.

Summary

  • Load images asynchronously so the UI thread stays responsive.
  • Use memory caching to avoid repeated downloads and decoding.
  • Deduplicate concurrent requests for the same URL.
  • Guard against cell reuse before assigning the final image.
  • Add disk cache when images are reused across screens or app launches.

Course illustration
Course illustration

All Rights Reserved.