I want to show a UIImageView in the top bar with an UIImage from a url and then cache it in my iOS native Xamarin iOS app. The problem is that when added to ViewWillAppear the FFImageLoading log shows that the image is being cancelled about 9 times out of 10, but occasionally it will succeed and display. I use:
ImageService.Instance.LoadUrl(url)
.Retry(3, 200)
.Into(avatarImage);
It either succeeds on first attempt or all retries fails (I also tried with a longer retry delay with no success). If I put the code in ViewDidAppear it works 9 times out of 10, but not always and never on first load. If I put it into a button that I push after the view has loaded the image is always loaded successfully. So it seems pretty obvious that FFImageLoading is failing because the UIImageView has not yet been drawn/loaded, but as I have created the UIImageView and added it to the View I don't know what more I can do.
How can I use FFImageLoading to load the image reliably? I can preload it, but that still doesn't solve how I get it into the UIImageView before it is displayed for the first time. If impossible with FFImageLoading I'm open to alternatives.
Log: fails to load into UIImageView:
[0:] FFImageLoadingDebug_SimpleDiskCache path: /var/mobile/Containers/Data/Application/93D1FA0C-B19A-4ECA-A0DA-B0AC408A5B8E/Library/Caches/FFSimpleDiskCache
Thread started: <Thread Pool> #3
Thread started: <Thread Pool> #4
Thread started: <Thread Pool> #5
Thread started: <Thread Pool> #6
Thread started: #7
Thread started: <Thread Pool> #8
Thread started: <Thread Pool> #9
Thread started: #10
Thread started: #11
Thread started: <Thread Pool> #12
Thread started: <Thread Pool> #13
[0:] FFImageLoadingDebug_Image memory cache size: 401,5 MB
[0:] FFImageLoadingDebug_Image loading cancelled: https://assets-cdn.github.com/images/modules/logos_page/Octocat.png;CircleTransformation,borderSize=0,borderHexColor=
[0:] FFImageLoadingDebug_Generating/retrieving image: https://assets-cdn.github.com/images/modules/logos_page/Octocat.png;CircleTransformation,borderSize=0,borderHexColor=
[0:] FFImageLoadingDebug_Wait for similar request for key: https://autodesk-forge.github.io/dist/sample.png?43fa010fd5f9a49ec978f5dec499349d
[0:] FFImageLoadingDebug_Wait for similar request for key: https://autodesk-forge.github.io/dist/sample.png?43fa010fd5f9a49ec978f5dec499349d
[0:] FFImageLoadingDebug_Wait for similar request for key: https://autodesk-forge.github.io/dist/sample.png?43fa010fd5f9a49ec978f5dec499349d
[0:] FFImageLoadingDebug_Generating/retrieving image: https://autodesk-forge.github.io/dist/sample.png?43fa010fd5f9a49ec978f5dec499349d
Thread started: #14
[0:] FFImageLoadingDebug_Wait for similar request for key: https://autodesk-forge.github.io/dist/sample.png?43fa010fd5f9a49ec978f5dec499349d
[0:] FFImageLoadingDebug_Image loading cancelled: https://assets-cdn.github.com/images/modules/logos_page/Octocat.png;CircleTransformation,borderSize=0,borderHexColor=
[0:] FFImageLoadingDebug_File /var/mobile/Containers/Data/Application/93D1FA0C-B19A-4ECA-A0DA-B0AC408A5B8E/Library/Caches/FFSimpleDiskCache/CD275DFA133499968568338D6D522382.864000 saved to disk cache for key CD275DFA133499968568338D6D522382
[0:] FFImageLoadingDebug_Image loaded from cache: https://autodesk-forge.github.io/dist/sample.png?43fa010fd5f9a49ec978f5dec499349d
etc.
Log: Succeeds (same page after reloading a few times):
[0:] FFImageLoadingDebug_Image loaded from cache: https://assets-cdn.github.com/images/modules/logos_page/Octocat.png;CircleTransformation,borderSize=0,borderHexColor=
[0:] FFImageLoadingDebug_Image loaded from cache: https://autodesk-forge.github.io/dist/sample.png?43fa010fd5f9a49ec978f5dec499349d
etc.
I have been unable to figure out why FFImageLoading gets cancelled, so instead I used Akavache and HttpClient to roll my own (I also use CrossConnectivity to check if mobile is connected).
This works every time when updated from ViewWillAppear. Because I use an IObservable it is important to update the image on the main UI thread, i.e. InvokeOnMainThread
.
UrlImageView class used to add a UIView that can load the image from an Url:
public class UrlImageView : UIImageView
{
private GetImageFromUrlObserver getImageFromUrlObserver = null;
public UrlImageView() : base()
{
getImageFromUrlObserver = new GetImageFromUrlObserver(this);
}
public void GetImageFromUrl(string url)
{
if (getImageFromUrlObserver != null)
{
getImageFromUrlObserver.GetImage(url);
}
}
public void Update(byte[] image)
{
this.InvokeOnMainThread(() =>
{
var data = NSData.FromArray(image);
this.Image = UIImage.LoadFromData(data);
});
}
}
The IObserver class (when the image data is retrieved it will trigger OnNext
)
public class GetImageFromUrlObserver : IObserver<byte[]>
{
private UrlImageView urlImageView;
private IDisposable unsubscriber;
public GetImageFromUrlObserver(UrlImageView view)
{
urlImageView = view;
}
public void GetImage(string url)
{
Subscribe(ImageLoader.GetImageFromUrl(url));
}
private void Subscribe(IObservable<byte[]> provider)
{
if (provider != null)
unsubscriber = provider.Subscribe(this);
}
public void OnCompleted()
{
Unsubscribe();
}
public void OnError(Exception error)
{
throw new NotImplementedException();
}
public void OnNext(byte[] image)
{
if (urlImageView != null)
{
urlImageView.Update(image);
}
}
private void Unsubscribe()
{
if (unsubscriber != null)
{
urlImageView = null;
unsubscriber.Dispose();
unsubscriber = null;
}
}
}
HttpClient class that gets the image data:
public static class ImageLoader
{
public static IObservable<byte[]> GetImageFromUrl(string url)
{
IObservable<byte[]> result = null;
var httpClient = new HttpClient(new NativeMessageHandler());
});
try
{
var cache = BlobCache.LocalMachine;
result = cache.GetAndFetchLatest(
url, /* simply use url as cache key */
async () =>
{
try
{
if (!CrossConnectivity.Current.IsConnected) return null;
HttpResponseMessage httpResponse = await httpClient.GetAsync(url);
return await httpResponse.Content.ReadAsByteArrayAsync().ConfigureAwait(false); ;
}
catch (Exception e)
{
EventTrackers.TrackException("GetImageFromUrl inner - " + e.Message);
return null;
}
},
offset =>
{
TimeSpan elapsed = DateTimeOffset.Now - offset;
return elapsed > new TimeSpan(hours: cacheUpdateFrequencyHours, minutes: cacheUpdateFrequencyMinutes, seconds: 0);
}
);
}
catch (Exception e)
{
EventTrackers.TrackException("GetImageFromUrl - " + e.Message);
return null;
}
return result;
}
To display the image you call:
UrlImageView image = new UrlImageView();
View.AddSubview(image);
image.GetImageFromUrl(url);