Cache Image from Firebase - ios

I have multiple cells that currently hold different photos from Firebase. Every time a user loads these images then scrolls, they are re-downloaded which eats up data fast. I find this concerning to any user who has a metered data plan. What could I do to solve this? Does Firebase offer any options to cache downloaded images?
This is how I am currently calling an image into a cell:
if let imageName = post["image"] as? String {
let imageRef = FIRStorage.storage().reference().child("images/\(imageName)")
imageRef.data(withMaxSize: 25 * 1024 * 1024, completion: { (data, error) -> Void in if error == nil {
let image = UIImage(data: data!)
cell.postImageView.image = image

Use Kingfisher to cache images. It's light and very easy to use. Just pass your url from firebase and it will automatically cache it.
let url = URL(string: "url_of_your_image")
imageView.kf.setImage(with: url)

You might use Alamofire too. It does not handle caching automatically though, but against Kingfisher, it has the ability to handle almost all kinds of networking needs.
PS: Yes I know that -generally- I do not need any networking capabilities if I'm using Firebase.
But, for example; since Firebase Database and Firestore cannot handle full-text search, you need to use third-party solutions, so, you might be in need of full-featured networking utility sometime.

Firebase already does cache the database locally, before it fetch real-time data from the server, so the problem is not as severe. But if you want to do better caching, use Glide, Glide caches images and you can specify time signatures so it re-fetches images only if they are updated.
This is super easy, but you do need to host your images in google cloud services, or aws, or anywhere even
ImageView imageView = (ImageView) findViewById(R.id.my_image_view);
Glide.with(this).load("url").into(imageView);

Related

How do I release cached images when using UIImage(data:)?

I'm noticing heavy memory usage from my image cache in my collection view and need to understand how to release it. I understand the difference between UIImage(named:) and UIImage(contentsOfFile:). However, I'm using UIImage(data:) and I can't seem to find any documentation on releasing image caches in this instance. Any help appreciated. Here's my code snippet:
if let setImage = cell?.viewWithTag(101) as? UIImageView {
if let url = URL(string: imageURLs[indexPath.item]) {
let task = URLSession.shared.dataTask(with: url, completionHandler: { data, _, error in
guard let data = data, error == nil else {
print("No data detected: \(Error.self)")
return
}
DispatchQueue.main.async {
let newImageData = UIImage(data: data)
self.imageData[indexPath.item] = newImageData!
setImage.image = self.imageData[indexPath.item] as? UIImage
}
})
task.resume()
URLSession.shared.finishTasksAndInvalidate()
}
}
UIImage(data:) doesn’t store in the system image cache. So, if you remove all of the references to the associated images from your imageData, make sure the image views are getting released, etc., you should be good.
If imageData a simple collection, consider making it a NSCache, where you can constrain the total count or cost. Also, we often employ two tier caching mechanisms (with smaller in-memory limits, larger persistent storage caches).
You might consider using one of the many third party libraries (AlamofireImage, KingFisher, SDWebImage, etc.) as they tend to employ decent caching strategies, getting you out of the weeds of this. They all offer nice “asynchronous image” extensions to UIImageView, too. (For example, the implementation you’ve shared with us is going to suffer from backlogging issues if you scroll quickly through a big collection view, something that is easily tackled with these UIImageView extensions.) Your UICollectionViewDataSource really should not be burdened with this sort of code.
I was under the impression that Firestore auto-caching applied to cloud Storage, but it only applies to cloud Database. Once I implemented local caching with NSCache, my problem was solved.

What is the fastest way to convert an imageURL from Firebase into a UIImage?

In my iOS app I need to take an imageURL string and convert it into a UIImage.
I wrote the below function to handle this:
func getImage(urlString: String) -> UIImage {
let url = URL(string: urlString)!
do {
let data = try Data(contentsOf: url)
let image = UIImage(data: data)!
return image
} catch {
print(error, " This was the error in p2")
}
return UIImage(named: "media")!
}
The issue is that this takes too long. I believe it's a solid second or longer for this to complete.
I need that time to be significantly shorter.
Question: Is there a faster way to get the UIImage based on an imageURL from Firebase? (maybe a cocoa-pod? or better way to write the code?)
Additional questions:
Would this be any faster if the image in Firebase were of lower quality?
Would it be a viable solution to lower the quality of the image right before being passed into this function?
A lot the prominent iOS apps (and web and mobile and general) that do a lot of downloading of images take advantage of progressive jpeg. This way your user will see at least something while the image loads and over time the image will get progressively better. As a lot of commenters have mentioned, you’re not in control of Firebase like you would be if you had your own backend server delivering the pictures that you could do performance optimizations. Therefore one of the best things you can do is implement progressive jpeg in your app.
The first link is a library that will allow you to use progressive jpeg in your iOS app. The second link is a detailed approach used at FaceBook on faster loading of images.
https://www.airpair.com/ios/posts/loading-images-ios-faster-with-progressive-jpegs
https://code.fb.com/ios/faster-photos-in-facebook-for-ios/

Does Firebase Storage's getMetaData() call the backend (and thus introduce a small delay)?

I currently use the following (Swift 4) code to download an image stored on the Firebase Storage :
func getImage(completion: #escaping (UIImage?)->()) {
let ref = Storage.storage().reference().child("myImage.jpg")
ref.getMetadata() {
(metadata, error) in
guard let url = metadata?.downloadURLs?.first, error == nil else {
print(String(describing: error))
completion(nil)
return
}
//got url download image here and return it
//this function is not important but it does it asynchroniously
func downloadImageOrReturnACachedVersionOfItBy(url, completion)
}
I notice a considerable delay in downloading the image, even if downloadImageOrReturnACachedVersionOfItBy(...) actually takes care of image cashing
Questions:
Does the function getMetadata() contact the backend each time its called?
And thus can this introduce a round-trip web service calling delay?
If so...to avoid this, would it be a good idea store the returned url, when the image was uploaded, returned in the metadata from Firebase putData() locally in the app? Are these download url's fixed for life? Are the automatically being invalidated somehow?
1) Does the function getMetadata() contact the backend each time its
called? And thus can this introduce a round-trip web service calling
delay?
Yes, this is because the download URL may have changed (e.g. developer revoked it), so we need to fetch it again.
Note that we have a downloadURL() method that does the same thing but without you having to parse metadata manually.
2) If so...to avoid this, would it be a good idea store the returned
url, when the image was uploaded, returned in the metadata from
Firebase putData() locally in the app? Are these download url's fixed
for life? Are the automatically being invalidated somehow?
I'd recommend using the FirebaseUI integration with SDWebImage, which automatically caches and displays these images.
// Reference to an image file in Firebase Storage
let reference = storageRef.child("images/stars.jpg")
// UIImageView in your ViewController
let imageView: UIImageView = self.imageView
// Placeholder image
let placeholderImage = UIImage(named: "placeholder.jpg")
// Load the image using SDWebImage
imageView.sd_setImage(with: reference, placeholderImage: placeholderImage)

Refresh cached image in AlamofireImage?

Is it possible to automatically remove cached image and download newer if image was updated on server? I have tried AlamofireImage and STXImageCache but they both download an image only once and do not update it.
I try like that:
private let downloader = ImageDownloader()
func downloadImage(path: String?, completion: #escaping (UIImage?) -> ()) {
guard let path = path else { return }
let urlRequest = URLRequest(url: URL(string: path)!)
downloader.download(urlRequest) { response in
completion(response.result.value)
}
}
Manual update is not so good also, because if I don't know if an image was updated on server I have to forget about image caching at all.
Looks like none of available pods are able to refresh cached images, or any other cached content, when the content is changed on server. It is technically possible due to Content-Control technology, but apple does not seem to provide this possibility https://developer.apple.com/documentation/foundation/nsurlrequest.cachepolicy/1414422-reloadrevalidatingcachedata
reloadRevalidatingCacheData
Specifies that the existing cache data may
be used provided the origin source confirms its validity, otherwise
the URL is loaded from the origin source.
This constant is unimplemented and shouldn’t be used.
Your best move would be to ask server developer to generate unique URL addresses to any image that will change(avatar, backgrounds, icons etc), or if server developer is not available you should just get rid of the caching in places where there is such a problem and use casual download.
In any case you can use my gist for downloading images cached and not cached here https://gist.github.com/sam-moshenko/562ec61431c4a0ebeb68899b4d1b4d26 (Just don't forget to install pod 'AlamofireImage')

iOS/Swift: How to manage image downloading from Firebase in a feed

I'm trying to implement a feed for images using Firebase Storage, think of it like the Instagram photo feed.
My Problem:
I don't know how many images there are in my Firebase reference folder
I want only the images to be shown that have been taken, let's say in the last week
I tried downloading the images from the reference like this:
let storageRef = storage.reference()
for i in 0...10 {
let imageRef = storageRef.child("images/" + String(i) + ".jpg")
imageRef.dataWithMaxSize((10 * 1024 * 1024), completion: { (data, error) -> Void in
if (error != nil) {
print(error)
} else {
let image: UIImage! = UIImage(data: data!)
self.downloadedPhotos.append(image) //downloadedPhotos is an array of UIImages
self.configureFeed() //takes care of some UI
}
})
}
Here I run into the obvious problem: I've only downloaded 10 images, called "1","2",..., "10"
What kind of way to name the images when users upload them would you suggest?
How could I keep track of how many images there are?
How could I delete those images from the reference that are older than a week?
Should I use Image Cashing Libraries like Kingfisher or would you go with the above style?
Thank you guys really much for any help!
Let's handle these one at a time:
What kind of way to name the images when users upload them would you
suggest?
I'd take a look at the many other questions like this: Retrieving image from Firebase Storage using Swift
How could I keep track of how many images there are?
See above.
How could I delete those images from the reference that are older than a
week?
You can use Object Lifecycle Management to delete images after a certain time. Deleting them from the database would be harder--maybe an integration with Google Cloud Functions GCS notifications could sync this.
Should I use Image Cashing Libraries like Kingfisher or would
you go with the above style?
I totally recommend using an image loader like SDWebImage or PINRemoteImage after pulling the download image from the realtime database.

Resources