load image asynchronously in uiimageview in Xamarin Monotouch - c#

I have table cell, in that I am showing multiple images in scrollview horizontally, I need load images into imageview asynchronously, that means I need to place default placefolder.png file, if the images are not loaded.
If the images are loaded I need to replace placeholder.png with the loaded images.
Can anyone please help me..
_image = new UIImageView(FromUrl(contentItem.FilePath, cachedImages)) { Tag = 1001 };
_imagePath = contentItem.FilePath;
_image.Frame = new RectangleF(x, 0, 250, _image.Image.CGImage.Height);
x = x + (Convert.ToInt32( Bounds.Width));
scrollwidth = scrollwidth + Convert.ToInt32( Bounds.Width);
_scrollView.ContentSize = new SizeF (scrollwidth, _image.Image.CGImage.Height);
_scrollView.Add (_image);

Since it is a table cell and you can't be certain if the cell is still the same when the task finishes I would create a download task similar to this first:
public async static Task<string> DownloadImage(string fileWebAddress, string localPath)
{
await new WebClient ().DownloadFileTaskAsync (url, fileName);
return localPath;
}
Then when creating the cell you would first check if you have already downloaded the image to local storage:
if (File.Exist(contentItem.FilePath))
{
SetImage(_image, contentItem.FilePath);
}
If not, then start async download and when it returns you check if the task was for the current cell (contentItem cannot be local function variable, it needs to be cell variable that can change):
else
{
DownloadImage(contentItem.WebPath, contentItem.FilePath).ContinueWith (t =>
{
if (contentItem.FilePath == t.Result)
{
SetImage(_image, t.Result);
}
});
}

Related

Xamarin iOS post image to Instagram

Goal: Using an iOS native method, push a user made picture onto their Instagram feed in C#.
public bool ShareImage(byte[] imageByte)
{
bool result = false;
//string fileName = "xxx.png";
//string path = Path.Combine(FileSystem.CacheDirectory, fileName);
//File.WriteAllBytes(path, imageByte);
NSData data = NSData.FromArray(imageByte);
UIImage image = UIImage.LoadFromData(data);
//NSUrl url = NSUrl.FromString($"instagram://library?AssetPath={path}"); // Only opens
NSUrl url = NSUrl.FromString($"instagram://library?LocalIdentifier={1}");
if (UIApplication.SharedApplication.CanOpenUrl(url))
{
UIApplication.SharedApplication.OpenUrl(url);
}
return result;
}
As far as I'm aware the way I have to do this is to save my image to the device and get a Local Identifier for this.
I have constructed this from the snippets of objective-c code I have seen. Sharing photos only works with the native app installed, as I have learnt from my trials to get the facebook module working.
Edit: Using PHAssetChangeRequest from the iOS Photos namespace does not work.
A collegue has pointed out to me about the possibility of saving then using a photo picker to get the PHAsset for the Local Identifier. But this is an extra step I do not want the users of the App to go through. Better to just remove Instagram support as I just can go through the generic share method as shown below. The disadvantage of this method is that the user has then to pick the medium to share over.
public async void ShareImage(byte[] imageByte)
{
string fileName = "xxx.png";
string path = Path.Combine(FileSystem.CacheDirectory, fileName);
File.WriteAllBytes(path, imageByte);
await Share.RequestAsync(
new ShareFileRequest()
{
File = new ShareFile(path),
Title = "xxx"
}
);
}
Edit 2
Tried a different way using UIDocumentInteractionController but it is showing nothing and not posting, it is not throwing any exceptions to give me any clues as to what I'm doing wrong.
public bool ShareImage(byte[] imageByte)
{
bool result = false;
string fileName = "xxx.igo";
string path = Path.Combine(FileSystem.CacheDirectory, fileName);
File.WriteAllBytes(path, imageByte);
//string caption = "xxx";
string uti = "com.instagram.exclusivegram";
UIImageView view = new UIImageView();
//UIDocumentInteractionController controller = UIDocumentInteractionController.FromUrl(NSUrl.FromString(path));
UIDocumentInteractionController controller = new UIDocumentInteractionController
{
//controller.Url = NSUrl.FromString(path);
Url = NSUrl.FromFilename(path),
Uti = uti
};
//CoreGraphics.CGRect viewDimensions = new CoreGraphics.CGRect(0, 0, 200, 100);
_ = controller.PresentOpenInMenu(CoreGraphics.CGRect.Empty, view, true);
//_ = controller.PresentOpenInMenu(viewDimensions, view, true);
return result;
}
Edit 3
Using
UIView view = new UIImageView();
if (UIApplication.SharedApplication.KeyWindow.RootViewController is UIViewController uiController && uiController.View != null)
{
view = uiController.View;
}
I was able to get the possible share options to show. I was logged in to Instagram using the native App, but the post did not show.
Change
Url = NSUrl.FromFilename(path)
to
Url = new NSUrl(path,false)
The second way is pointing to the correct file path not the file name .
Change
controller.PresentOpenInMenu(CoreGraphics.CGRect.Empty, view, true);
to
controller.PresentOptionsMenu(UIScreen.MainScreen.Bounds, (UIApplication.SharedApplication.Delegate as AppDelegate).Window, true);
Show UIDocumentInteractionController inside current Window and set the proper frame.

I can't display an image in app from firebase storage using the image url

I want to show an Image which I have successful stored into firebase storage. I got the user to pick an image from gallery and saved it on the database. I then saved the URL link in the real-time database, which I thought I could just bind to the image.source however this does not work. I have also tried copying the link from the firebase console straight into XAML that didn't work. I also tried downloading the image onto the phone(code below) how ever this doesn't work. I can't find any tutorials anywhere for displaying images from firebase for Xamarin forms. I'm a bit stuck here.
public void LoadImages(string imgurl)
{
var webClient = new WebClient();
byte[] imgBytes = webClient.DownloadData(imgurl);
//string img = Convert.ToBase64String(imgBytes);
petimage.Source = ImageSource.FromStream(() => new MemoryStream(imgBytes));
}
protected override void OnAppearing()
{
base.OnAppearing();
RetrivePetInfo();
}
private async void RetrivePetInfo()
{
var pet = await firebaseHelper.GetPet(PetName);
if (pet != null)
{
if(pet.imageUrl!=null)
{
LoadImages(pet.imageUrl);
}
}
}
Update your security rules with match /{allPaths=**} to indicate that public read and write access is allowed on all paths:
service firebase.storage {
match /b/zain*******.appspot.com/o {
match /{allPaths=**} {
// Allow access by all users
allow read, write;
}
}
}

Unity iOS app image cache issues

I've taken over development of an app that is complete minus a bug requiring an internet connection to load images as it doesn't access them from the cache despite an attempt to do so.
Can anyone help me figure out what is going wrong in the following?
public class SpriteCache : Singleton<SpriteCache>
{
Dictionary<string, Sprite> _cache = new Dictionary<string, Sprite>();
public void LoadSprite(string url, Action<Sprite> callback)
{
StartCoroutine(LoadSpriteCoroutine(url, callback));
}
public IEnumerator LoadSpriteCoroutine(string url, Action<Sprite> callback)
{
if (_cache.ContainsKey(url))
{
callback(_cache[url]);
yield break;
}
var www = new WWW(url);
while (!www.isDone)
{
yield return www;
}
if (!string.IsNullOrEmpty(www.error))
{
Debug.LogErrorFormat("Tried to obtain texture at '{0}' but received error '{1}'", url, www.error);
yield break;
}
var texture = www.texture;
if (texture == null)
{
Debug.LogErrorFormat("No texture found at '{0}'!", url);
yield break;
}
var sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(texture.width / 2, texture.height / 2));
_cache[url] = sprite;
callback(sprite);
}
edit:
A further explanation to clarify
var www = new WWW(url)
This grabs the images stored on a server which works, as far as I'm aware after one instance of grabbing the image this should place the item in cache for use later.
I've tried using the following updated method to see if that would fix it.
var www = WWW.LoadFromCacheOrDownload(url, 1)
This resulted in it not working in any capacity and never changing the images from the placeholders.
The first if statement in the "LoadSpriteCoroutine" is supposed to catch if the sprite already exists in the "_cache" Dictionary by checking if there is a key for the url, which there should be after the first running instance with and internet connection
Load image from _cache if its in there:
if (_cache.ContainsKey(url))
{
callback(_cache[url]);
yield break;
}
Add image to _cache if it wasn't previously in there:
var sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(texture.width / 2, texture.height / 2));
_cache[url] = sprite;
So I figured out a solution,
all the other methods of saving images to the cache seemed to fail for iOS, I removed all of the previous "_cache" stuff.
this function for saving worked for me:
//if file doesn't exist then save it for fututre reference
if (!File.Exists(_FileLocation + texName))
{
byte[] bytes;
if (Path.GetExtension(texName) == ".png")
{
bytes = texture.EncodeToPNG();
}
else
{
bytes = texture.EncodeToJPG();
}
File.WriteAllBytes(Application.persistentDataPath + texName, bytes);
}
that section is placed after the "callback(sprite)".
The check for saved items is placed where the old "if(_cache.ContainsKey(url))" section was
texName = Path.GetFileName(texName);
if (File.Exists( _FileLocation + texName))
{
url = _FileLocation + "/"+ texName;
}
please note that the "LoadSpriteCoroutine" now take a extra string argument 'texName' in its calls which is just the a shortened instance of the url which it then cuts to just the file name and extension. If it finds a match then it replaces url with a the persistent file path + the fileName to grab it locally opposed to over the network as normal.
_FileLocation is defined as follows
string _FileLocation;
public void Start()
{
_FileLocation = Application.persistentDataPath;
}
The reason behind referencing it in that method is to save performance from calling the address though application AND if you were working on a cross platform solution then the data path for saving may need to be changed as such you could put a detection if or case statement to change _FileLocation to suit your needs.

Xamarin forms - Loading ImageSource from Blob storage

I'm facing a somewhat weird issue.
I have a simple code (I guess) in my Xamarin Forms project to take pictures thanks to the great plugin.Media which allows cross platform picture capture:
public async Task<CameraResult> TakeCrossPictureAsync()
{
var file = await CrossMedia.Current.TakePhotoAsync(mediaOptions);
CameraResult res = new CameraResult()
{
Picture = ImageSource.FromFile(file.Path),
FilePath = file.Path,
Name = await this.UploadFileAsync(file.GetStream())
};
}
When I do this, I'm able to display the Image with this line in my ViewModel which is bound to an FFImageLoading:CachedImage
CameraResult camRes = await picManager.TakeCrossPictureAsync();
if (camRes != null)
this.Picture = camRes.Picture;
The "Picture" attribute is a Xamarin.Forms.ImageSource.
I then navigate to another page, passing the ImageSource as an argument and I'm able to display it in another FFImageLoading:CachedImage :
<ffimageloading:CachedImage Source="{Binding Picture}" LoadingPlaceholder="loading.png" CacheDuration="10" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"/>
No problem there, everything is working like a charm.
But, when I try to do the same thing by downloading the image from a Blob storage :
public async Task<ImageSource> GetFileAsync(string name)
{
var blob = this.container.GetBlobReference(name);
if (await blob.ExistsAsync())
{
await blob.FetchAttributesAsync();
byte[] blobBytes = new byte[blob.Properties.Length];
await blob.DownloadToByteArrayAsync(blobBytes, 0);
Stream stream = new MemoryStream(blobBytes);
return(ImageSource.FromStream(() => stream));
}
}
The Imagesource returned is bound to the same FFImageLoading:CachedImage in the first and second view.
The problem is that it displays perfectly in the first view but when I pass the ImageSource as an argument to the second ViewModel, the image does not show in the UWP project and displays the LoadingPlaceHolder or I get an unhandled exception on Android.
Is anyone able to tell me how the two ImageSources differ and why it is displayed from the file in the second view but not from the blob ?
Thank you in advance !

Xamarin.forms how to hold image on same page after navigating another page

I have uploaded image on my profile page and I want to hold that image until I logout in xamarin forms.
My image will be lost if I select another page so I want to hold it until I log out.
var profile = new Image { };
profile.Source = "profile.png";
profile.HorizontalOptions = LayoutOptions.StartAndExpand;
profile.VerticalOptions = LayoutOptions.StartAndExpand;
var profiletap = new TapGestureRecognizer();
profiletap.Tapped += async (s, e) =>
{
var file = await CrossMedia.Current.PickPhotoAsync();
if (file == null)
return;
await DisplayAlert("File Location", file.Path, "OK");
im = ImageSource.FromStream(() =>
{
var stream = file.GetStream();
//file.Dispose();
return stream;
});
profile.Source = im;
// await Navigation.PushModalAsync(new PhotoPage(im));
};
profile.GestureRecognizers.Add(profiletap);
Pages do not get destroyed when navigating to another page and coming back, which is why a page's constructor only gets executed the first time it is shown. So I am not sure what you mean when you say you want to hold that image.
Having said that, you could always assign the entire profile variable to a static global variable in your App class like below so that it stays the same no matter what. Then you would have to assign/initialize the global variable at the correct time.
But again, I am not sure if that is necessary, so you might try to explain more what the issue actually is:
In the App class:
public class App : Application {
public static Image ProfileImage = new Image {
Source = "profile.png",
HorizontalOptions = LayoutOptions.StartAndExpand,
VerticalOptions = LayoutOptions.StartAndExpand
};
....
}
Then in your page:
public class ProfilePage : ContentPage {
public ProfilePage() {
....
App.ProfileImage.GestureRecognizers.Add(profiletap);
}
}
Edit: See my answer here for an example of using a plugin to allow the user to choose a photo from their device's camera roll. Once you have the photo path, you can simply use HttpClient to send the image and a base64 string. There are plenty of other example online about how to do that.
Edit #2: After this line of your code:
var file = await CrossMedia.Current.PickPhotoAsync();
You now have the file and the path in file variable. So currently all you are doing is showing the image using ImageSource.FromStream but in order to keep showing the image when you return to the page, you need to also save the image to the device. In order to do that, you will need to write platform specific code in each project and reference that in your shared code. Something like this:
In your iOS and Android project, create a new file (FileHelper_Android.cs and FileHelper_iOS.cs for example) and add the following (the same code can be added to both iOS and Android files, just change the name of the class and file:
using ....;
[assembly: Dependency(typeof(FileHelper_Android))]
namespace YourNamespace.Droid{
/// <summary>
/// Responsible for working with files on an Android device.
/// </summary>
internal class FileHelper_Android : IFileHelper {
#region Constructor
public FileHelper_Android() { }
#endregion
public string CopyFile(string sourceFile, string destinationFilename, bool overwrite = true) {
if(!File.Exists(sourceFile)) { return string.Empty; }
string fullFileLocation = Path.Combine(Environment.GetFolderPath (Environment.SpecialFolder.Personal), destinationFilename);
File.Copy(sourceFile, fullFileLocation, overwrite);
return fullFileLocation;
}
}
}
Do the same on iOS and just change the file name. Now in your shared project you need to create IFileHelper.cs like so:
public interface IFileHelper {
string CopyFile(string sourceFile, string destinationFilename, bool overwrite = true);
}
Finally, in your page you would write the following:
_fileHelper = _fileHelper ?? DependencyService.Get<IFileHelper>();
profiletap.Tapped += async (s, e) =>
{
var file = await CrossMedia.Current.PickPhotoAsync();
if (file == null)
return;
await DisplayAlert("File Location", file.Path, "OK");
profile.Source = im;
imageName = "SomeUniqueFileName" + DateTime.Now.ToString("yyyy-MM-dd_hh-mm-ss-tt");
filePath = _fileHelper.CopyFile(file.Path, imageName);
im = ImageSource.FromFile(filePath)
// await Navigation.PushModalAsync(new PhotoPage(im));
};
Above, once the user chooses the file, we copy that file locally and the we also set your im variable to the new local file path, which gets returned from the IFileHelper.CopyFile method.
You still need to handle the case when the user comes back to the page or turns the app off and on again. In that situation, you need to load the saved image path. I would suggest either saving the image path into the DB, unless the user will only ever have a single profile image, then you could always just load that same path and filename. Let me know if you still have issues.

Categories