I want to load my images from Firebase to my Table View but I get the error:
Cannot convert value of type 'String' to expected argument type 'URL'
When I print the object on its own it is definitely a URL.
This is what my code looks like:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "FeedItem", for: indexPath) as! FeedItem
//TODO: Guard...
let postImage = postArray [indexPath.row]
let postImageURL = postImage.postImageURL
let data = Data(contentsOf: postImageURL) // Line with Error
cell.postImage.image = UIImage (data: data)
return cell
}
To display the image in your cell, you need to convert the URL string into an actual URL object, which you can do via:
let postImage = postArray[indexPath.row]
if let postImageURL = URL(string: postImage.postImageURL)
{
do {
let data = try Data(contentsOf: postImageURL)
cell.postImage.image = UIImage (data: data)
} catch {
print("error with fetching from \(postImageURL.absoluteString) - \(error)")
}
}
And as rmaddy implies, your performance is not going to be very good (because depending on how far away the remote server is or how slow the internet is), the synchronous "Data(contentsOf:" call might take an unacceptably long time to succeed. I'm just providing this answer so you will be able to see something in your own testing, but I wouldn't use this in production code.
Try to replace the Data fetch with an asynchronous URLSession task, and you can find much more information in this very related question.
Related
Note: Please no libraries. This is important for me to learn. Also, there are a variety of answers on this but none that I found solves the issue nicely. Please don't mark as duplicate. Thanks in advance!
The problem I have is that if you scroll really fast in the table, you will see old images and flickering.
The solution from the questions I read is to cancel the URLSession
data request. But I do not know how to do that at the correct place
and time. There might be other solutions but not sure.
This is what I have so far:
Image cache class
class Cache {
static let shared = Cache()
private let cache = NSCache<NSString, UIImage>()
var task = URLSessionDataTask()
var session = URLSession.shared
func imageFor(url: URL, completionHandler: #escaping (image: Image? error: Error?) -> Void) {
if let imageInCache = self.cache.object(forKey: url.absoluteString as NSString) {
completionHandler(image: imageInCache, error: nil)
return
}
self.task = self.session.dataTask(with: url) { data, response, error in
if let error = error {
completionHandler(image: nil, error: Error)
return
}
let image = UIImage(data: data!)
self.cache.setObject(image, forKey: url.absoluteString as NSString)
completionHandler(image: image, error: nil)
}
self.task.resume()
}
}
Usage
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let myImage = images[indexPath.row]
if let imageURL = URL(string: myImage.urlString) {
photoImageView.setImage(from: imageURL)
}
return cell
}
Any thoughts?
Swift 3:
Flickering can be avoided by this way:
Use the following code in public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
cell.photoImageView.image = nil //or keep any placeholder here
cell.tag = indexPath.row
let task = URLSession.shared.dataTask(with: imageURL!) { data, response, error in
guard let data = data, error == nil else { return }
DispatchQueue.main.async() {
if cell.tag == indexPath.row{
cell.photoImageView.image = UIImage(data: data)
}
}
}
task.resume()
By checking cell.tag == indexPath.row, we are assuring that the imageview whose image we are changing, is the same row for which the image is meant to be. Hope it helps!
A couple of issues:
One possible source of flickering is that while you're updating the image asynchronously, you really want to clear the image view first, so you don't see images for prior row of reused/dequeued table view cell. Make sure to set the image view's image to nil before initiating the asynchronous image retrieval. Or, perhaps combine that with "placeholder" logic that you'll see in lots of UIImageView sync image retrieval categories.
For example:
extension UIImageView {
func setImage(from url: URL, placeholder: UIImage? = nil) {
image = placeholder // use placeholder (or if `nil`, remove any old image, before initiating asynchronous retrieval
ImageCache.shared.image(for: url) { [weak self] result in
switch result {
case .success(let image):
self?.image = image
case .failure:
break
}
}
}
}
The other issue is that if you scroll very quickly, the reused image view may have an old image retrieval request still in progress. You really should, when you call your UIImageView category's async retrieval method, you should cancel and prior request associated with that cell.
The trick here is that if you're doing this in a UIImageView extension, you can't just create new stored property to keep track of the old request. So you'd often use "associated values" to keep track of prior requests.
Im downloading the image link from a json and then creating the image once the table view start creating its cells:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCellController
DispatchQueue.main.async(execute: { () -> Void in
if let url = NSURL(string: self.movies[indexPath.row].image)
{
if let data = NSData(contentsOf: url as URL)
{
let imageAux = UIImage((data: data as Data))
cell.movieImage.image = imageAux
self.tableView.reloadData()
}
}
})
cell.name = self.movies[indexPath.row].name
cell.date = self.movies[indexPath.row].date
return cell
}
And this works fine, but the table view becomes really slow, not at rendering but at scrolling. I keep checking the RAM and CPU and both are really low but my network use keeps rising BUT the images are already on the cell so it means its already done. (For this test im calling the JSON for only 2 movies, so 2 images)
Before i started doing this my total download was about 200kb (with images), now its getting over 2MB before i stop the project.
What im doing wrong?
You'll probably want to designate a separate queue for background activities. In this instance, your heavy network task is in:
NSData(contentsOf: url as URL)
This is what is "freezing" the UI. The best solution would be to define something like DispatchQueue.background and perform the network calls there, while then later performing the UI tasks back on the main thread so as not to lock up your display:
DispatchQueue.background.async(execute: { () -> Void in
if let url = NSURL(string: self.movies[indexPath.row].image) {
//Do this network stuff on the background thread
if let data = NSData(contentsOf: url as URL) {
let imageAux = UIImage(data: data as Data)
//Switch back to the main thread to do the UI stuff
DispatchQueue.main.async(execute: { () -> Void in
cell.movieImage.image = imageAux
})
}
}
})
Let me know if this makes sense.
I have data coming from Json and populating it inside a TableView . One of those elements that I am getting back from Json is a String that has a URL to an image . If a particular Json string is null or blank I get the following error
fatal error: Index out of range
Every post will not have an Image but I do not know how to tell swift to ignore a certain section of code if the String is blank . This is my code
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HomePageTVC", for: indexPath) as! HomePageTVC
cell.post.text = Posts[indexPath.row]
// Start Display Image
// right here I get the fatal out of index if there is no image
if profile_image_string[indexPath.row] != nil {
let imgURL: NSURL = NSURL(string: profile_image_string[indexPath.row])!
let request:NSURLRequest = NSURLRequest(url: imgURL as URL)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
DispatchQueue.main.async(execute: { () -> Void in
if data != nil {
cell.profile_image.image = UIImage(data: data!)
}
})
});
task.resume()
}
// End Display Image
return cell
}
This is my Json being returned
{"post":"Check check","profile_image":null},
{"post":"check check","profile_image":"http://www.myurl.com/cats.jpg}
notice that if no image url exist then the default value is null . How can I check for that inside my TableView code ? Because what I am sure that is happening is that it's taking that null value and trying to convert it into an image thus I am getting the error. The value of the string URL is kept inside this string Array
profile_image_string[indexPath.row]
and this is how I append it
if let profile_picture = Stream["profile_image"] as? String {
self.profile_image_string.append(profile_picture)
}
anyways as stated before I am successfully getting the URL String from Json and if it is a URL then the image shows, I just want to know how can I check for Nulls that way I stop getting that error any help would be great .
This part is pretty wrong
if let profile_picture = Stream["profile_image"] as? String {
self.profile_image_string.append(profile_picture)
}
If you want consistent array value, you have to make your profile_image_string can contains nil by make it's type [String?] and if the if let fail, append nil value into the array like:
if let....else {
self.profile_image_string.append(nil)
}
Still, this way is very messy and not advised, i suggest you create proper object to hold your JSON data
I am trying to add an image to a tableview cell but not having much luck getting it to display.
The image is being loaded from the file system (I println() the result) as a UIImage but I cannot seem to get it into the cell. Placing a println() after the closure shows me that the images are all loaded after the cell has been returned.
Here is my code:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! UITableViewCell
let note = notes[indexPath.row]
let nsURL = NSURL(string: note.valueForKey("url") as! String)!
var loadError: NSError?
var image: UIImage?
assetsLibrary!.assetForURL(nsURL, resultBlock: { (asset) -> Void in
if let ast = asset {
let iref = ast.defaultRepresentation().fullResolutionImage().takeUnretainedValue()
image = UIImage(CGImage: iref)
println("The loaded image: \(image)")
}
}, failureBlock: {(error) -> Void in
loadError = error
})
cell.textLabel!.text = note.valueForKey("title") as? String
cell.imageView!.image = image
return cell
}
When I replace the closure with the following to load a image from the project itself it shows in the table. This leads me to believe it not due to an issue with the way the story board is set up.
UIImage(named: transportItems[indexPath.row])
Any help would be much appreciated.
It doesn't work that way. cellForRowAtPathIndex is a synchronous routine. assetForURL is an asynchronous routine. It will return the data long time after cellForRowAtPathIndex has returned.
Here's what you should do: Have a method cachedAssetForURL which returns the asset immediately, or returns nil. If it returns an asset, store it. Remember this has to be as efficient as possible, because this is called while the user scrolls up and down through the images.
If the method returns nil, trigger a download in the background. When that download finishes, don't even try to store the image in the cell - by this time, the same cell could display an entirely different object! Instead store the data so that cachedAssetForURL will be able to return the asset, and invalidate the row of your table view.
Fire a notification in the block (it's asynchronous), manage it by setting your imageview and reload your view.
The underlying issue I was encountering, as pointed out by #gnasher729, was the mishmash of synchronous and asynchronous calls. Rewriting the code to take this into account and to include a cache, I have the following working solution.
var cachedImages = [String:UIImage]()
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! UITableViewCell
let note = notes[indexPath.row]
let url = note.valueForKey("url") as! String
cell.textLabel!.text = note.valueForKey("title") as? String
cell.imageView!.image = UIImage(named: "note-icon")
if let image = cachedImages[url]{
cell.imageView!.image = image
} else {
let nsURL = NSURL(string: url)!
var loadError: NSError?
assetsLibrary!.assetForURL(nsURL, resultBlock: { (asset) -> Void in
if let ast = asset {
let image = UIImage(CGImage: ast.defaultRepresentation().fullResolutionImage().takeUnretainedValue())
self.cachedImages[url] = image
dispatch_async(dispatch_get_main_queue(), {
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
cellToUpdate.imageView?.image = image
}
})
}
},failureBlock: {(error) -> Void in
loadError = error
})
}
return cell
}
This could be further improved to take more off of the main thread.
I'm new in swift programming and I have searched a lot about storing images with NSCache using Swift.
What I have done so far is that I'm getting ids and imageNames with JSON and I have my data in array and I was able to display image in cells with no problem. Now I want to cache images.
This is the code that I have written:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as TableViewCell
//cell.imageView
nameID = self.hoge[indexPath.row]["cars"]["carid"].string!
cell.nameLabel.text = nameID
if let imageFileURL = imageCache.objectForKey(self.hoge[indexPath.row]["cars"]["carid"].intValue) as? NSURL {
println("Get image from cache")
} else {
imageName = self.hoge[indexPath.row]["cars"]["pic_name"].string!
// If the image does not exist, we need to download it
var imgURL: NSURL = NSURL(string: "http://192.168.1.35/car/uploads/" + imageName )!
var image:UIImage = UIImage(named: "pen")!
// Download an NSData representation of the image at the URL
let request: NSURLRequest = NSURLRequest(URL: imgURL)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {(response: NSURLResponse!,data: NSData!,error: NSError!) -> Void in
if error == nil {
image = UIImage(data: data)!
cell.viewCell.image = image
}
else {
println("Error: \(error.localizedDescription)")
}
})
}
return cell
}
SO, How can I store and retrieve images with cache?
I recommend you to check this library HanekeSwift (It provides a memory and LRU disk cache for UIImage, NSData, JSON, String or any other type that can be read or written as data), at least to understand how they are dealing with cache, and you can decide to use it or create your own solution.
Using a very easy/simple API:
// Setting a remote image
imageView.hnk_setImageFromURL(url)
// Setting an image manually. Requires you to provide a key.
imageView.hnk_setImage(image, key: key)
Using the cache
let cache = Shared.dataCache
cache.set(value: data, key: "image.png")
// Eventually...
cache.fetch(key: "image.png").onSuccess { data in
// Do something with data
}