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

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.

Related

Cloud Storage for Firebase bandwidth quota exceeded after downloading multiple images – what are the best practices?

I'm building an iOS app with data stored in Firestore and associated images stored in Cloud Storage for Firebase. Essentially, my app's starting screen is a scrolling list of cards representing documents from one of my Firestore collections and an associated image (pulled from Cloud Storage, matched using document title).
To generate the starting scrollview data, each time I open my app, I run a call to both Firestore and Cloud Storage to populate my local structs and retrieve the images (around 10 of them, each 600px by 400 px and between 300-500 KB).
I'm currently on the free Spark plan. My quotas are fine for Firestore, but yesterday, I was testing the app in the simulator (maybe ran it ~60 times?) and exceeded the 1 GB bandwidth limit for Cloud Storage calls. I think this is because I'm downloading a lot of images and not storing them locally (because I download them again each time the app is run?).
Here's the code that runs each time the app is opened to get all of the data and associated images:
// StoryRepository.swift
import Foundation
import FirebaseFirestore
import FirebaseStorage
import FirebaseFirestoreSwift
class StoryRepository: ObservableObject { // listen to any updates
let db = Firestore.firestore()
let storage = Storage.storage()
#Published var stories = [Story]() // Story is a local struct with vars for title, previewImage (the associated card image I get from Cloud Storage), and other data in Firestore.
init() {
loadStoryCardData()
}
func loadStoryCardData() {
db.collection("stories").addSnapshotListener{ (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("Error fetching documents: \(error!)")
return
}
// create Cloud Storage reference
let storageRef = self.storage.reference()
// each document represents a story. get the title of that story, and then fetch the image from Cloud Storage that has the same filename as that title.
documents.forEach { document in
guard let title = document["title"] as? String else {
print("Could not read story title.")
return
}
let primaryImageRef = storageRef.child("storyCardPrimaryImages/" + title + ".jpg")
// Download in memory with a maximum allowed size of 1MB (1 * 1024 * 1024 bytes)
primaryImageRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print("Error fetching primary image: \(error)")
return
} else {
if let image = UIImage(data: data!) {
self.stories.append(Story(title: title, previewImage: image, data: document.data())!)
}
}
}
}
}
}
}
I'm just wondering how to optimize this, because when I release this app and gain users, I can just imagine where my bandwidth will go. Should I store the local structs and images in the iOS FileManager so I only download them once, when the app first runs? How will I update the local data should anything in either Firestore or Cloud Storage change?
I thought about storing the images in a smaller size, but I really need them to be 600px by 400px in my app. I'd prefer to stay on the free Spark plan if possible. Honestly, I'm just a little taken aback that I've already run into quota limits while testing – when it's just me running the app. Overall, I'm at a bit of a loss – any advice would be really, really appreciated.
Should I store the local structs and images in the iOS FileManager so I only download them once, when the app first runs?
Using a local cache like this is definitely one good way to reduce the total bandwidth use.
How will I update the local data should anything in either Firestore or Cloud Storage change?
That will be for you to engineer. In general, one way to do this would be for your process that updates the objects in storage to also update some metadata about the image, and store that in Firestore. Your app can query Firestore, for changes, then check to see if your locally cached files are up to date before downloading them again.
I thought about storing the images in a smaller size, but I really need them to be 600px by 400px in my app.
You can always increase the amount of JPG compression, which will also reduce the quality of the image. That's a tradeoff you'll have to experiment with to your preference.

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/

Storage downloading costs too much for Firestore app in Swift 4

So the way my app is working is kind of like instagram. A user can upload a photo, and whenever someone loads the app it downloads each picture that was uploaded from the firebase.
I understand that I need to buy space or change my plan, but I didn't do that much and I'm wasting 1.7gb from a user in like an hour. Each photo costs like 17mb to upload and download.
I am not sure what I can do to lessen my downloading here.
The way I download from firestore is like this from the f:
// Create a reference to the file you want to download
let islandRef = storageRef.child("images/island.jpg")
// Download in memory with a maximum allowed size of 1MB (1 * 1024 * 1024 bytes)
islandRef.getData(maxSize: 1 * 10240 * 10240) { data, error in
if let error = error {
// Uh-oh, an error occurred!
} else {
// Data for "images/island.jpg" is returned
let image = UIImage(data: data!)
}
}
And each time it loads a photo into a collectionviewcontroller. Which means it is like 17mb for each photo which is a lot. Any suggestions? Thanks
So this is where you want to make a decision about the level of quality for the photos that you upload to firebase. I can assure you that instagram and any other social media platform only store versions of your pictures that are compressed and optimized for size.
You can easily compress your image by doing something like this
let data = imageToUpload.jpegData(compressionQuality: 0.3)
you would then upload that new compressed version of the image to firebase and dramatically improve your storage efficiency.

SDWebImage caching fails when loading big number of images

I'm using SDWebImageDownloader.shared().downloadImage to download images, then
if let i = image {
let key = SDWebImageManager.shared().cacheKey(for: product.imageURL)
SDImageCache.shared().store(i, forKey: key, completion: nil)
}
My code iterates on a product array, and downloads all the images for them (so it will appear while the user is only even if the product's page wasn't opened before)
My problem is, that it doesn't save the images at all, I see a bunch of these in the log:
CFNetwork internal error (0xc01a:/BuildRoot/Library/Caches/com.apple.xbs/Sources/CFNetwork/CFNetwork-811.5.4/Loading/URLConnectionLoader.cpp:304)
Could you please give me a hint where to start?
Thanks!

Cache Image from Firebase

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);

Resources