I'm struggling to download pictures from firebase storage to an UIImage in swift 5.
I can well upload them. When I tried to retrieve picture, the UIImage display a black screen.
here my function which return the UIImage
import UIKit
import Firebase
func getImageEvent (imagePath : String) -> UIImage? {
var myImage : UIImageView?
//Access to the storage
let storageRef = Storage.storage().reference(withPath: imagePath)
storageRef.getData(maxSize: 1 * 1024 * 1024) {(data, error) in
if let error = error {
print(error.localizedDescription)
return
}
if let data = data {
print(data.description)
myImage?.image = UIImage(data: data)
}
}
return myImage?.image
}
//Call the function
getImageEvent (imagePath :"9U4BoXgBgTTgbbJCz0zy/eventMainImage.jpg")
In the console, I can well see a value for print(data.description).
By default, there is an image in the UIImageView. When call the function, the default image is replaced by a black screen.
Could you please help me to understand the mistake ?
many thanks
There's a number of ways to go about this but a brief description of the issue first:
The return statement within the closure will execute way before that image is downloaded - Firebase functions are asynchronous and code has to be crafted in a way that allows for time to download and get data from the internet. So - don't try to return data from asynchronous functions.
Here's the code re-written with a completion handler. That handler will be called only after the image is fully downloaded.
func getImageEvent (imagePath: String, completion: #escaping(UIImage) -> Void) {
var myImage : UIImageView?
let storageRef = Storage.storage().reference(withPath: imagePath)
storageRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print(error.localizedDescription)
return
}
if let data = data {
if let myImage = UIImage(data: data) {
completion(myImage)
}
}
}
}
and the key is how to call that function. Note this code awaits the data (UIImage) to be passed back to it within it's closure and lets you know that getting the image was complete.
self.getImageEvent(imagePath: "9U4BoXgBgTTgbbJCz0zy/eventMainImage.jpg", completion: { theImage in
print("got the image!")
})
You should add additional error checking in case the image was not downloaded or myImage was nil. Passing back an error message along with the nil myImage would be one option, or making the object passed back as an optional and then checking for nil within self.downloadImageAtPath would be another.
To complete the solution, below the code used to in tableView to get picture in a particular cell
getImageEvent(imagePath: myArray[indexPath.row].partyImagePath) { (image) in
cell.partyImage.image = image
}
Related
I am parsing a JSON within my viewDidLoad method. One of the keys within this JSON is the image URL, which goes into a a string array called "allCImages"
This is just a string. Therefore to populate the image into the cell, in my cellForRowAt method, I have the following:
cell.vcCellImage.downloadImage(from: allCImages[indexPath.section])
Note: vcCellImage is the IBOutlet of my cell image view.
The "downloadImage" method is part of the following extension:
extension UIImageView {
func downloadImage(from imgURL: String!) {
let theUrl = URLRequest(url: URL(string: imgURL)!)
// set initial image to nil so it doesn't use the image from a reused cell
image = nil
// check if the image is already in the cache
if let imageToCache = vc1ImageCache.object(forKey: imgURL! as NSString) {
self.image = imageToCache
print("Image is in Cache")
return
}
// download the image asynchronously
let task = URLSession.shared.dataTask(with: theUrl) { (data, response, error) in
if error != nil {
print(error!)
return
}
DispatchQueue.main.async {
// create UIImage
let imageToCache = UIImage(data: data!)
// add image to cache
vc1ImageCache.setObject(imageToCache!, forKey: imgURL! as NSString)
self.image = imageToCache
}
}
task.resume()
}
This is working almost perfectly. For example:
1) If I scroll down my tableview slowly, all the cells contain the correct image
2) If I scroll up my tableview, slowly or quickly, all the cells contain the correct image. This is proven by the fact that my console is printing the following:
Image is in Cache
Image is in Cache
Image is in Cache
I.e, the tableview is getting my image from the cache (since to scroll up, I must have scrolled down before)
3) The issue is if I scroll down my tableview really quickly, on the first attempt. Since the image has not cached yet, the cell will display the wrong image, before changing to the correct image. Classic problem
Therefore I am missing this small piece of logic. How to resolve this?
EDIT: I tried this but the issue remains:
class VCTableViewCell: UITableViewCell {
override func prepareForReuse() {
super.prepareForReuse()
vcCellImage.image = nil
}
This occurs because of
1- cell dequeueing : cells are re-used inside the tableView
2- when you scroll before a request happens it may cause a new 1 with same url
Best option is using SDWebImage
I have faced the similar issue.
I have fixed this issue by cancelling the image request in the prepareForReuse method.
Can you try the same?
first off all if you are appending your api or any data like this just remove this
var arr = [string]()
viewDidLoad()
{
arr.append("s","sd","sd)
}
accept this
var arr = [string]()
viewWillAppear()
{
arr.removeAll()
//call api
//append Data
arr.append("s","sd","sd)
}
I have similar problem then I solve it like this, may be it helpful for you also.
I am trying to convert image snapshots into a video but I am facing UI Thread problems: my view controller is locked. I would like to know how to handle this because I did a lot of research and tried to detach the process into different DispatchQueues but none of them worked. So, it explains why I am not using any Queue on the code below:
class ScreenRecorder {
func renderPhotosAsVideo(callback: #escaping(_ success: Bool, _ url: URL)->()) {
var frames = [UIImage]()
for _ in 0..<100 {
let image = self.containerView.takeScreenshot()
if let imageData = UIImageJPEGRepresentation(image, 0.7), let compressedImage = UIImage(data: imageData) {
frames.append(compressedImage)
}
}
self.generateVideoUrl(frames: frames, complete: { (fileURL: URL) in
self.saveVideo(url: fileURL, complete: { saved in
print("animation video save complete")
callback(saved, fileURL)
})
})
}
}
extension UIView {
func takeScreenshot() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: self.bounds.size)
let image = renderer.image { _ in
self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
}
return image
}
}
class ViewController {
let recorder = ScreenRecorder()
recorder.renderPhotoAsVideo { success, url in
if (success) {
print("ok")
} else {
self.alert(title: "Erro", message: "Nao foi possivel salvar o video", block: nil)
}
}
}
PS: I used this tutorial as reference -> http://www.mikitamanko.com/blog/2017/05/21/swift-how-to-record-a-screen-video-or-convert-images-to-videos/
It really looks like this is not possible, at least not the way you are trying to do it.
There are quite a few ways to render a UIViews content into an image, but all of them must be used from the main thread only. This applies also to the drawInHierarchy method you are using.
As you have to call it on the main thread and the method is just getting called so many times, I think this will never work in a performant way.
See profiling in Instruments:
Sources:
How to render view into image faster?
Safe way to render UIVIew to an image on background thread?
I'm trying to download images from my firebase database and load them into collectionviewcells. The images download, however I am having trouble having them all download and load asynchronously.
Currently when I run my code the last image downloaded loads. However, if I update my database the collection view updates and the new last user profile image also loads in but the remainder are missing.
I'd prefer to not use a 3rd party library so any resources or suggestions would be greatly appreciated.
Here's the code that handles the downloading:
func loadImageUsingCacheWithUrlString(_ urlString: String) {
self.image = nil
// checks cache
if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
self.image = cachedImage
return
}
//download
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
//error handling
if let error = error {
print(error)
return
}
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString as NSString)
self.image = downloadedImage
}
})
}).resume()
}
I believe the solution lies somewhere in reloading the collectionview I just don't know where exactly to do it.
Any suggestions?
EDIT:
Here is where the function is being called; my cellForItem at indexpath
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: userResultCellId, for: indexPath) as! FriendCell
let user = users[indexPath.row]
cell.nameLabel.text = user.name
if let profileImageUrl = user.profileImageUrl {
cell.profileImage.loadImageUsingCacheWithUrlString(profileImageUrl)
}
return cell
}
The only other thing that I believe could possibly affect the images loading is this function I use to download the user data, which is called in viewDidLoad, however all the other data downloads correctly.
func fetchUser(){
Database.database().reference().child("users").observe(.childAdded, with: {(snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let user = User()
user.setValuesForKeys(dictionary)
self.users.append(user)
print(self.users.count)
DispatchQueue.main.async(execute: {
self.collectionView?.reloadData()
})
}
}, withCancel: nil)
}
Current Behavior:
As for the current behavior the last cell is the only cell that displays the downloaded profile image; if there are 5 cells, the 5th is the only one that displays a profile image. Also when I update the database, ie register a new user into it, the collectionview updates and displays the newly registered user correctly with their profile image in addition to the old last cell that downloaded it's image properly. The rest however, remain without profile images.
I know you found your problem and it was unrelated to the above code, yet I still have an observation. Specifically, your asynchronous requests will carry on, even if the cell (and therefore the image view) have been subsequently reused for another index path. This results in two problems:
If you quickly scroll to the 100th row, you are going to have to wait for the images for the first 99 rows to be retrieved before you see the images for the visible cells. This can result in really long delays before images start popping in.
If that cell for the 100th row was reused several times (e.g. for row 0, for row 9, for row 18, etc.), you may see the image appear to flicker from one image to the next until you get to the image retrieval for the 100th row.
Now, you might not immediately notice either of these are problems because they will only manifest themselves when the image retrieval has a hard time keeping up with the user's scrolling (the combination of slow network and fast scrolling). As an aside, you should always test your app using the network link conditioner, which can simulate poor connections, which makes it easier to manifest these bugs.
Anyway, the solution is to keep track of (a) the current URLSessionTask associated with the last request; and (b) the current URL being requested. You can then (a) when starting a new request, make sure to cancel any prior request; and (b) when updating the image view, make sure the URL associated with the image matches what the current URL is.
The trick, though, is when writing an extension, you cannot just add new stored properties. So you have to use the associated object API to associate these two new stored values with the UIImageView object. I personally wrap this associated value API with a computed property, so that the code for retrieving the images does not get too buried with this sort of stuff. Anyway, that yields:
extension UIImageView {
private static var taskKey = 0
private static var urlKey = 0
private var currentTask: URLSessionTask? {
get { objc_getAssociatedObject(self, &UIImageView.taskKey) as? URLSessionTask }
set { objc_setAssociatedObject(self, &UIImageView.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
private var currentURL: URL? {
get { objc_getAssociatedObject(self, &UIImageView.urlKey) as? URL }
set { objc_setAssociatedObject(self, &UIImageView.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
func loadImageAsync(with urlString: String?, placeholder: UIImage? = nil) {
// cancel prior task, if any
weak var oldTask = currentTask
currentTask = nil
oldTask?.cancel()
// reset image view’s image
self.image = placeholder
// allow supplying of `nil` to remove old image and then return immediately
guard let urlString = urlString else { return }
// check cache
if let cachedImage = ImageCache.shared.image(forKey: urlString) {
self.image = cachedImage
return
}
// download
let url = URL(string: urlString)!
currentURL = url
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
self?.currentTask = nil
// error handling
if let error = error {
// don't bother reporting cancelation errors
if (error as? URLError)?.code == .cancelled {
return
}
print(error)
return
}
guard let data = data, let downloadedImage = UIImage(data: data) else {
print("unable to extract image")
return
}
ImageCache.shared.save(image: downloadedImage, forKey: urlString)
if url == self?.currentURL {
DispatchQueue.main.async {
self?.image = downloadedImage
}
}
}
// save and start new task
currentTask = task
task.resume()
}
}
Also, note that you were referencing some imageCache variable (a global?). I would suggest an image cache singleton, which, in addition to offering the basic caching mechanism, also observes memory warnings and purges itself in memory pressure situations:
class ImageCache {
private let cache = NSCache<NSString, UIImage>()
private var observer: NSObjectProtocol?
static let shared = ImageCache()
private init() {
// make sure to purge cache on memory pressure
observer = NotificationCenter.default.addObserver(
forName: UIApplication.didReceiveMemoryWarningNotification,
object: nil,
queue: nil
) { [weak self] notification in
self?.cache.removeAllObjects()
}
}
deinit {
NotificationCenter.default.removeObserver(observer!)
}
func image(forKey key: String) -> UIImage? {
return cache.object(forKey: key as NSString)
}
func save(image: UIImage, forKey key: String) {
cache.setObject(image, forKey: key as NSString)
}
}
A bigger, more architectural, observation: One really should decouple the image retrieval from the image view. Imagine you have a table where you have a dozen cells using the same image. Do you really want to retrieve the same image a dozen times just because the second image view scrolled into view before the first one finished its retrieval? No.
Also, what if you wanted to retrieve the image outside of the context of an image view? Perhaps a button? Or perhaps for some other reason, such as to download images to store in the user’s photos library. There are tons of possible image interactions above and beyond image views.
Bottom line, fetching images is not a method of an image view, but rather a generalized mechanism of which an image view would like to avail itself. An asynchronous image retrieval/caching mechanism should generally be incorporated in a separate “image manager” object. It can then detect redundant requests and be used from contexts other than an image view.
As you can see, the asynchronous retrieval and caching is starting to get a little more complicated, and this is why we generally advise considering established asynchronous image retrieval mechanisms like AlamofireImage or Kingfisher or SDWebImage. These guys have spent a lot of time tackling the above issues, and others, and are reasonably robust. But if you are going to “roll your own,” I would suggest something like the above at a bare minimum.
I am using Firebase to host and download images for my app. Each image on Firebase ranges from 200kb-400kb, and the user downloads about 12 at a time as they scroll through a collectionView. When I launch the VC that downloads the images, my app goes from using about 100mb of memory to 650mb of memory from downloading 19 images total. The images in questions are JPEGs and have been compressed quite heavily before their upload to Firebase. These images are stored in an NSCache, and clearing the cache brings the memory usage back down to around 100mb.
What is going on? Here is some code that may help?:
class TripOverviewCell: UICollectionViewCell {
#IBOutlet weak var imageView: UIImageView!
func updateUI(photo:Photo, image:UIImage? = nil) {
//Call when preparing to show image
if image != nil {
print("Loaded from cache")
imageView.image = image
photo.assignImage(image: image!)
} else {
let url = photo.imageUrl
let ref = FIRStorage.storage().reference(forURL: url)
ref.data(withMaxSize: 5*1024*1024, completion: { [weak self] (data, error) in
if error != nil {
print("Unable to download image")
} else {
print("Image downloaded")
if let imageData = data {
if let image = UIImage(data: imageData) {
self?.imageView.image = image
photo.assignImage(image: image)
TripsVC.imageCache.setObject(image, forKey: photo.uid as NSString)
}
}
}
})
}
}
}
As Frank commented, displayed images take up a LOT more space than just the data for the images. My error lay in the fact that I was caching UIImages, which is the full displayed image. Instead, I should have cached the data and then created images from that data where I need them. Memory usage is down from 550mb to about 20mb.
Alright so I've made some handlers and classes to grab data from a URL, its all returning fine, I've checked the URLs are valid and everything.
Anyways, I'm trying to do an NSData(contentsOfURL) on a stored URL in my class for a UIViewController. I'm successfully printing out String Variables like name, type, description, but I'm having difficulty displaying an UIImage into a Image View on the ViewController.
Here is my Code, it's run when the View loads:
func configureView() {
// Update the user interface for the detail item.
if let card = detailCard {
title = card.cardTitle
//Assign Elements
detailDescriptionLabel?.text = card.description
typeLabel?.text = card.cardType
cardTitleLabel?.text = card.cardTitle
costLabel?.text = card.cardCost
print(card.cardImageURL)
if let url = NSURL(string: card.cardImageURL){
let data = NSData(contentsOfURL: url)
imageView.image = UIImage(data: data!)// <--- ERROR HERE: EXC_BAD_INSTRUCTION
}
//Print Log
//print(card.logDescription)
}
}
Like the comment says above, I get the error on the imageView.image = UIImage(data: data!) line:
Thread 1: EXC_BAD_INSTRUCTION
Here is the code for cardImageURL:
var cardImageURL: String {
return String(format: "http://testyard.example.net/display/images/cards/main/%#", image)
}
//This is a computed variable when a "Card" class object is created.
It returns the correct url in a string:
http://testyard.example.net/display/images/cards/main/armor-of-testing.jpg
I've used this code elsewhere, and it worked fine, why is it throwing errors here?
Thanks in advance.
Are you sure the data being downloaded is an image? You should probably use this line:
if let image = UIImage(data: data)`
This should stop the error, though you might need to test that your data actually contains an image.