Firebase Storage retrieval images - swift

I'm trying to get my head around Firebase Storage.
I've seen online and tried two methods of getting my images.
What is the difference between these two? (both work).
So after I get the photoUrl from my Firebase Database:
1.
if let data = NSData(contentsOfURL: NSURL(string:photoUrl)!)
{
let myImage = UIImage(data: data)!
MyImageCache.sharedCache.setObject(myImage, forKey: self.key)
//etc
}
2.
self.storage.referenceForURL(photoUrl).dataWithMaxSize(1 * 1024 * 1024) { (data, error) -> Void in
if (error != nil)
{
print(error)
}
else
{
let myImage = UIImage(data: data!)
MyImageCache.sharedCache.setObject(myImage!, forKey: self.key)
//etc
}
}

Regarding the first method, you shouldn't be using that for network calls. From the docs:
Do not use this synchronous method to request network-based URLs. For network-based URLs, this method can block the current thread for tens of seconds on a slow network, resulting in a poor user experience, and in iOS, may cause your app to be terminated. Instead, for non-file URLs, consider using the dataTaskWithURL:completionHandler: method of the NSURLSession class. See URL Session Programming Guide for details.
The second method is baked into the firebase framework and provides you with convenience methods for downloading an image, i.e. it gives you the option to specify images size. This is probably optimised for getting images and is in most scenarios the preferred method.

Related

How Can I get Image data from specific URI in Swift?

I have a function called downloadImage. I'm trying to get image data and set an imageView. But it prints "no data".
image link: http://commons.wikimedia.org/wiki/Special:FilePath/Flag%20of%20Montenegro.svg
Here is my code:
func downloadImage() {
let uri = "Special:FilePath/Flag%20of%20Montenegro.svg"
let baseURL = URL(string: "http://commons.wikimedia.org/wiki/")!
let imageURL = URL(string: uri, relativeTo: baseURL)!
if let data = try? Data(contentsOf: imageURL){
if let image = UIImage(data: data){
DispatchQueue.main.async {
self.flagImageView.image = image
}
}
else{
print("image cannot be taken")
}
}else{
print("no data")
}
}
Here is console output:
2022-09-03 21:21:56.426579+0300 CountryBook[5242:197393] nil host used in call to
allowsSpecificHTTPSCertificateForHost
2022-09-03 21:21:56.426913+0300 CountryBook[5242:197393] nil host used in call to allowsAnyHTTPSCertificateForHost:
2022-09-03 21:21:56.427580+0300 CountryBook[5242:197905] NSURLConnection finished with error - code -1002
no data
Note:
I Allowed Arbitrary Loads from info.plist by marking it as YES.
Assuming you did allow the usage of http corectly. There is more going on here:
The link you provided redirects to https://upload.wikimedia.org/wikipedia/commons/6/64/Flag_of_Montenegro.svg. Data(contentsOf: is not for this purpose. It is best suited for loading data from a bundle file url and not complex redirecting or cookies or header..... . Use a proper URLSesssion.
Even if you get your data it will not work this way. UIImage doesn´t support SVG format. See this SO question for more info
Remarks:
Just use https. It´s de facto standard. And your link to wikipedia would support it. Falling back to http should be the last resort while developing and never in production.
this is actually due to the fact that apple no longer allows http urls by default, it wants them to be https
When using a https url, you also shouldn't need to allowArbitraryLoads
From what I suspect though, wikipedia is https, so I think you can just change your url to have https like so:
https://commons.wikimedia.org/wiki/Special:FilePath/Flag%20of%20Montenegro.svg

Return Image from Function in Swift

I have tried to return an Image or UIImage from the function below but there is an issue that says
"Unexpected non-void return value in void function."
I am retrieving the Image from Firebase Storage and just want to return it...
func retrivePhotos(path: String) -> UIImage {
let storageRef = Storage.storage().reference()
let fileRef = storageRef.child(path)
// Retrieve the data with the path
fileRef.getData(maxSize: 5 * 1024 * 1024) { data, error in
if error == nil && data != nil {
let image = UIImage(data: data!)
return image
}
}
}
The thing to understand is that your call to getData(maxSize:) is a separate asynchronous function. That function does not, and cannot, return the image in its return. It needs to start a network request. It starts that network request and then returns immediately. When the function returns, the network request hasn't even begun sending over the network yet.
The return in the braces following that function call are a return from a closure, which is a block of code that you are passing to the function.
Your function also cannot return an image if it is getting the image using an async function. (unless you rewrite it using async/await, a new language feature that is a little beyond the scope of this question, and likely beyond your current understanding.)
You need to rewrite your function to take a completion handler, just like the getData(maxSize:) function takes a completion handler:
typealias ImageHandler = (UIImage?) -> Void
func retrivePhotos(path: String, completion: ImageHandler) {
let storageRef = Storage.storage().reference()
let fileRef = storageRef.child(path)
// Retrieve the data with the path
fileRef.getData(maxSize: 5 * 1024 * 1024) { data, error in
if error == nil && data != nil {
let image = UIImage(data: data!)
completion(image)
} else {
completion(nil)
}
}
}
(That is over-simplified. You'd probably write it to take a Result instead of an Optional UIImage.)
Think of async functions like this. The main program is you, cooking dinner for your family. You realize you are out of milk, so you send your kid to the store to buy some. You don't hand your kid some money and expect them to hand you back the milk right then and there. You hand them the money, tell them to get you milk, and then go back to the rest of the cooking. At some time in the future, your kid comes back and either gives you the milk (The success case) or tells you there was a problem that prevented them from getting the milk (the error case.)
At the point your kid come back from the store, you run the "kid is back from the store" completion handler. You either go into sauce-making mode with the milk, or an error handler where you decide what to do since you don't have milk.

Swift, Kingfisher Cache not working properly?

I am trying to cache images using the Cocoapod KingFisher, the code i am using does display the image from the database storage but it does no caching. I am curious as to know why?
The print always says "cache Result none". And i also notice that the images are not cached.
Code for calling the imageDownloader:
DownloadImage(imageId : nextUser.id, cardImage: secondProfilePic)
Code for downloading and caching, also for checking if cached.
func DownloadImage(imageId : String, cardImage : UIImageView){
let imagesStorageRef = Storage.storage().reference().child("profilepic/").child(imageId)
//Get URL For Cache
imagesStorageRef.downloadURL { url, error in
if let error = error {
// Handle any errors
cardImage.image = UIImage(named: "RentOutProfilePic")
print("Error")
} else {
// Get the download URL for '.jpg'
let pathURL = url
print("Sets Image")
cardImage.kf.indicatorType = .activity
cardImage.kf.setImage(with: pathURL,
options: [
.transition(.fade(0.3)),
.cacheOriginalImage
])
}
if let url = url{
let tempUrl:String = url.path
let cache = ImageCache.default
let cached = cache.imageCachedType(forKey: tempUrl)
print("cache Result \(cached)")
}
}
}
Kingfisher is using url.absoluteString as the cache key for an image by default. So in your code, url.path will always return you the result of "not cached".
You are trying to print the cache result as the same time when you set the image. At the first time, your image would be in download progress so you always get .none even you set the key correctly according to 1. In the following invocation of this method with the same id, you should get a cache result either as disk or memory.
I am not sure how did you get the conclusion of "the images are not cached". Kingfisher is doing cache based on url by default. If you have different urls every time (which is returned from your imagesStorageRef) you call the image view setting method, there would be no matching cache and downloading would happen. If this is your case, you can customize to use the imageId as the cache key instead. To do that, you need to specify another cache key. See this wiki section for more.

A background URLSession with identifier already exists

I have an S3Service which is a singleton that manages all the S3 related uploads and downloads.
When I upload the first image it works fine but if I try to upload an Image consecutively It gives me this warning and the completion block never gets called.
A background URLSession with identifier com.amazonaws.AWSS3TransferUtility.Identifier.TransferManager already exists.
This is how I upload method looks:
if let data = image.jpegData(compressionQuality: 0.5) {
let transferUtility = AWSS3TransferUtility.s3TransferUtility(forKey: S3Service.TRANSFER_MANAGER_KEY)
transferUtility.uploadUsingMultiPart(data: data, bucket: EnvironmentUtils.getBucketName(), key: filename, contentType: "image/jpg", expression: nil, completionHandler: { task,error in
if let error = error {
print(error.localizedDescription)
} else {
print("Image upload success")
}
})
}
The call to register transfer utility AWSS3TransferUtility.register(with: serviceconfig, forKey: KEY) was causing the above issue. There are two things that should be kept in mind.
The AWSS3TransferUtility should be registered only once per Application session. Then we can use AWSS3TransferUtility.S3TransferUtilityForKey to get the instance wherever needed.
If these are for different users within the app, ( e.g. sign-up) and if we want to keep AWSS3TransferUtility separate for each user, register AWSS3TransferUtility with a different key (preferably the same key for the same user) and look up using that key.

Image Caching – How to control when images are disposed?

I need to cache images on disk but to limit them say to 20 images. I am trying Nuke library. When I run
Nuke.loadImage(with: url, options: options, into: imageView)
image is cached as long as I am inside my View Controller. When I leave the views, next time images are being fetched again. So how do I make Nuke (or other lib) save those images for specific image count or time.
Before trying Nuke I was just saving images to Documents folder of the app every time I fetch image. I am sure there is a better way.
Update: I was able to do it with Kingfisher.
func getFromCache(id: String) {
ImageCache.default.retrieveImage(forKey: id, options: nil) {
image, cacheType in
if let image = image {
self.galleryImageView.image = image
} else {
print("Not exist in cache.")
self.loadImage()
}
}
}
private func loadImage() {
ImageDownloader.default.downloadImage(with: url, options: [], progressBlock: nil) {
(image, error, url, data) in
if let image = image {
self.galleryImageView.image = image
ImageCache.default.store(image, forKey: id, toDisk: true)
}
}
}
If I understand correctly, first retrieveImage fetches images from disk cache. Later from memory cache. And it frees them only when memory warning received. I hope it does. God help us.
There are several advanced options for image caching. You can get a direct access to Memory Cache (default Nuke's ImagePipeline has two cache layers):
// Configure cache
ImageCache.shared.costLimit = 1024 * 1024 * 100 // Size in MB
ImageCache.shared.countLimit = 20 // You may use this option for your needs
ImageCache.shared.ttl = 90 // Invalidate image after 90 sec
// Read and write images
let request = ImageRequest(url: url)
ImageCache.shared[request] = image
let image = ImageCache.shared[request]
// Clear cache
ImageCache.shared.removeAll()
Also, use ImagePreheater class. Preheating (a.k.a. prefetching) means loading images ahead of time in anticipation of their use. Nuke lib provides an ImagePreheater class that does just that:
let preheater = ImagePreheater(pipeline: ImagePipeline.shared)
let requests = urls.map {
var request = ImageRequest(url: $0)
request.priority = .low
return request
}
// User enters the screen:
preheater.startPreheating(for: requests)
// User leaves the screen:
preheater.stopPreheating(for: requests)
You can use Nuke lib in combination with Preheat library which automates preheating of content in UICollectionView and UITableView. On iOS 10.0 and above you might want to use new prefetching APIs provided by iOS instead.