UITableView not showing remote image until scroll or reload of tableviewcell - ios

I am using Kingfisher to download and cache remote images.
I would like to render those images in a UITableViewCell
Currently the images do not show until I scroll.
I suspect this is because the initial image size is nil, once I scroll the cell is redrawn and the image is rendered.
To negate this issue I have called tableView.reloadRows(at: [indexPath], with: .none) in the completion handler of Kingfisher
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "FeedTableViewCellId", for: indexPath) as! FeedGifCell
let url = URL(string: items[indexPath.item])!
let imageView = ImageResource(downloadURL: url, cacheKey: "\(url)-imageview")
let placeHolderImage = UIImage.from(hex: "ffffff").resize(width: 1, height: 190)
cell.gifImage.kf.setImage(with: imageView, placeholder: placeHolderImage) { _ in
tableView.reloadRows(at: [indexPath], with: .none)
}
return cell
}
I have applied a simple placeholder of a fixed height and width to give the cell some height before the image is applied.
I am not sure this is the best approach, I do not like calling tableView.reloadRows(at: [indexPath], with: .none) this feels a little.....hacky.
Also, before the cell is redrawn I have the white space, my placeholder, then a jump as the correct image / size is applied.
Is it possible to simply prevent the cell from being visible at all until the image has been applied?

No! You don't have to reload the tableView in any way after the image in a certain cell gets done downloading. Other than that, you're doing everything correctly. I use Kingfisher too.
But in the future, you could just make a function in your cell class, say setupCell(_ imageURL: URL?). Anyways, going on, in your cellForRow method, just do it like this:
cell.gifImage.kf.setImage(with: imageView, placeholder: placeHolderImage, completionHandler: nil)
Now, in case that there's no image downloaded or you have no image URL string to be made to URL object, you could catch it in Kingfisher's error block, then you will need to pass your error image (a UIImage) object to your imageView so that there would be no duplicate image.
For example, I have the following in my cell's setupCell() function:
if let url = vendor.image?.URLEscaped {
let resource = ImageResource(downloadURL: url, cacheKey: url.absoluteString)
self.imageView_Vendor.kf.setImage(with: resource, options: [.transition(.fade(0.2)), .cacheOriginalImage])
return
}
self.imageView_Vendor.image = nil
I hope this helps!
EDIT:
For the handling of image height, this could help: https://stackoverflow.com/a/52787062/3231194

Related

How to reload UIImage loaded asynchronously with SDWebImage in UITableView cell

The problem is that I don't know how to refresh images loaded asynchronously from url with SDWebImage.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let item = items[indexPath.row]
if let url = URL(string: item.stringUrl) {
let placeholderImage = UIImage(named: "noPhoto.png")
itemImage.sd_setImage(with: url, placeholderImage: placeholderImage)
}
cell.imageView?.image = itemImage.image
cell.textLabel?.text = item.title
cell.detailTextLabel?.text = "item id: \(item.id)"
return cell
}
The images will only refresh after scrolling through the table. Until then the table only shows placeHolder image which is expected as it only reloads images when cells are reused. I tried to use the completion handler to refresh images when loaded using tableview.reload() as shown below:
itemImage.sd_setImage(with: url, placeholderImage: placeholderImage, completed: {image,error,cache,url in
if image != nil {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
})
This solution results in tableView loading all the images in a second which is what I want but it reloads the whole table basically every time I try to scroll through it so it "pushes" the whole table to the top position instantly.
The last solution I tries is this:
itemImage.sd_setImage(with: url, placeholderImage: placeholderImage, completed: {image,error,cache,url in
if image != nil {
DispatchQueue.main.async {
self.tableView.reloadRows(at: [indexPath], with: .none)
}
}
})
This also doesn't and makes tableview randomly showing and hiding images.
I suppose the problem is that I try to reload images in a function that creates cells but I can't find a solution to how resolve this issue. I will be very grateful for any help.

First TableView cells are not dequeuing properly

I'm working on the app, which loads flags to a class using https://www.countryflags.io/ API. I am loading a flag when initializing the object using Alamofire get request.
The problem is that the first few TableView cells that are dequeued when starting the app are loaded without flags.
But when I scroll back after scrolling down, they load perfectly.
I thought that it is happening because the request is not processed quickly enough and the first flags are not ready to load before the start of dequeuing cells. But I have no idea how to setup something inside the getFlag() method to help me reload TableView data upon completion or delay dequeuing to the point when all flags are loaded.
Country class with getflag() method
import UIKit
import Alamofire
final class Country {
let name: String
let code: String
var flag: UIImage?
var info: String?
init(name: String, code: String, flag: UIImage? = nil, info: String? = nil) {
self.name = name
self.code = code
if flag == nil {
getFlag()
} else {
self.flag = flag
}
self.info = info
}
func getFlag() {
let countryFlagsURL = "https://www.countryflags.io/\(code.lowercased())/shiny/64.png"
Alamofire.request(countryFlagsURL).responseData { response in
if response.result.isSuccess {
if let data = response.data {
self.flag = UIImage(data: data)
}
}
}
}
}
cellForRowAt method
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let country = countries[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "Country", for: indexPath)
cell.textLabel?.text = country.name
if let flag = country.flag {
cell.imageView?.image = flag
} else {
cell.imageView?.image = .none
}
return cell
}
The init method of Country should not be initiating the asynchronous image retrieval. Instead, you should have the cellForRowAt initiate the asynchronous image retrieval.
But you shouldn’t just blithely update the cell asynchronously, either, (because the row may have been reused by the time your Alamofire request is done). And, a more subtle point, you’ll want to avoid having image requests getting backlogged if you scroll quickly to the end of the tableview, so you want to cancel pending requests for rows that are no longer visible. There are a number of ways of accomplishing all three of these goals (async image retrieval in cellForRowAt, don’t update cell after it has been used for another row, and don’t let it get backlogged if scrolling quickly).
The easiest approach is to use AlamofireImage. Then all of this complexity of handling asynchronous image requests, canceling requests for reused cells, etc., is reduced to something like:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: “Country", for: indexPath)
cell.imageView.af_setImage(withURL: objects[indexPath.row].url, placeholderImage: placeholder)
return cell
}
Note, I’d suggest if you’re going to use the default table view cell, that you supply a placeholder image in this routine, like shown above. Just create a blank image (or whatever) that is the same size as your flags. This ensures that the cell will be properly laid out.
By the way, if you’d like to generate that placeholder image programmatically, you can do something like:
let imageSize = CGSize(width: 44, height: 44)
lazy var placeholder: UIImage = UIGraphicsImageRenderer(size: imageSize).image { _ in
UIColor.blue.setFill()
UIBezierPath(rect: CGRect(origin: .zero, size: imageSize)).fill()
}
Now, that creates a blue placeholder thumbnail that is 44×44, but you can tweak colors and size as suits your application.

How to find the height and width of image and fit the tableviewcell with aspect ratio?

I have stretched images displayed in TableViewCell. I need to find the height and width of image from image URL. I need to find the aspect ratio and fit the image on the cell according to the height of dynamic cell in Swift 4.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier.ManCell) as! ManCell
let image = self.posts[indexPath.row]["thumbnail_images"]["medium_large"]["url"].stringValue
let cleanString = removeHtmlTagsFromResponse(index: indexPath.row)
cell.nutriTitle.text = self.posts[indexPath.row]["title"].stringValue
print("labels: \(self.posts[indexPath.row]["title"].stringValue)")
cell.nutriDetail.text = cleanString
let lCount = calculateHeightOfLable(cellLabel: cell.nutriDetail)
print("string: \(cleanString)")
print("HeightCount: \(lCount)")
cell.nutriImage.sd_setImage(with: URL(string: image), placeholderImage: UIImage(named: "no_image"))
let heightContentSize = cell.bounds.height
print("HeightContentSize= ",heightContentSize)
return cell
}
It looks like you are using SDWebImage to load your images from a URL. In the documentation you will find that there is another method on UIImage called: sd_setImageWithURL:placeholderImage:options:completed: which takes a completion handler with 4 parameters: image: UIImage, error: NSError, isCache: Bool, and url: URl. Use the first parameter to get the size of the image. Note that this is asynchronous; you don't get the size until the image is downloaded (assuming you don't already have it in the cache), so then cell may resize if you are dynamically sizing it.

iOS load image into table view cell

update
now images displays as they are loaded, but there still a problem I can't solve.
The images overlaps the content of the cell even the three views are in a vertical stack (first is the image, the next two are label views).
I wonder how can a view overlap other views in a stackview
now my talbe view delegate method looks like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: BaseTableViewCell!
if let contents = contents {
let content = contents[indexPath.row]
if let imageUrl = content.imageUrl, let url = URL(string: imageUrl) {
cell = tableView.dequeueReusableCell(withIdentifier: "ImageContentRow", for: indexPath) as! ContentWithImageTableViewCell
Alamofire.request(url).responseImage(completionHandler: { (response) in
if let image = response.result.value {
cell.imageView?.image = image
cell.setNeedsLayout()
}
})
}else{
cell = tableView.dequeueReusableCell(withIdentifier: "ContentRow", for: indexPath) as! ContentTableViewCell
}
cell.contentTitleVeiw.text = content.title
cell.contentLeadView.text = content.lead
}
return cell!
}
I'm very new to iOS. I've already tutorials and watched video courses and now I would like to implement my very first app.
I try to read a feed and display the content's title, lead and image (if it has any).
I defined a cell, with an UIMageVeiw, and two UILables, at last I embedded them into a vertical StackView (I set distribution to Fill).
Everything works well, but images don't display automatically, just if I click on the cell, or sroll the TableVeiw.
And even if I set the image view to Aspect Fit (and embedded it into the top tof the vertical stack view), when the image displays it keeps it's original size and overlaps the cell content.
I've trying to find out what I do wrong for two days, but I can't solve it.
I try display data like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: BaseTableViewCell!
if let contents = contents {
let content = contents[indexPath.row]
if let imageUrl = content.imageUrl, let url = URL(string: imageUrl) {
cell = tableView.dequeueReusableCell(withIdentifier: "ImageContentRow", for: indexPath) as! ContentWithImageTableViewCell
cell.imageView?.af_setImage(withURL: url)
}else{
cell = tableView.dequeueReusableCell(withIdentifier: "ContentRow", for: indexPath) as! ContentTableViewCell
}
cell.contentTitleVeiw.text = content.title
cell.contentLeadView.text = content.lead
}
return cell!
}
The image view cell's view hierarchy looks like this:
My list looks like this after I start my app and data displayed:
After I click on a cell that should display an image or scroll it out and back the result is this:
At least this is my list view controller with the two prototype cells
(one for contents with image, one for without image)
You need to notify the tableView that the image was loaded and that it has to redraw the cell.
Try changing
cell.imageView?.af_setImage(withURL: url)
to
cell.imageView?.af_setImage(withURL: url, completion: { (_) in
self.tableView.beginUpdates()
self.tableView.setNeedsDisplay()
self.tableView.endUpdates()
})
Take a look at my answer here - in the end you might need to explicitly set height on the imageView.

UITableViewCell image loading wrong images

For now i download the image, story it in a mutable dictionary and then verify if the image was already downloaded and if not, download it and store it. As a key i use the indexPath.
This code kinda works, but from the tests i did if i scroll too fast the cell image will load the wrong one and after a split of a second it will load the right one (replacing the wrong image).
Im always clearing my thumbnail (imageView) after i call the method so i don't know why im getting this bug.
I though that maybe the if(self.imageCache.object(forKey: cacheKey) != nil) statement was true and thats why i would get multiple images, but the breakpoint didn't stop at once when i was scrolling down.
Any ideas?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MovieCellController
cell.thumbnail.image = UIImage()
let cacheKey = indexPath.row
if(self.imageCache.object(forKey: cacheKey) != nil)
{
cell.thumbnail.image = self.imageCache.object(forKey: cacheKey) as? UIImage
}
else
{
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
if let url = NSURL(string: self.moviesCollection[indexPath.row].imageLink) {
if let data = NSData(contentsOf: url as URL) {
let image: UIImage = UIImage(data: data as Data)!
self.imageCache.setObject(image, forKey: cacheKey as NSCopying)
DispatchQueue.main.async(execute: {
cell.thumbnail.image = image
})
}
}
}
}
cell.name.text = moviesCollection[indexPath.row].name
return cell
}
It is happening because the cells are reused due to which when scrolling fast the image of another cell seems to be assigned, but if fact it is the previous cell's image which is reused.
In cell's prepareForReuse method set your imageView's image to nil. Like, imageView.image = nil
Because the cell is reused.
The reused-cell keeps its old data.
The new image downloading will cost few seconds so that the reused -cell cannot change the image immediately.
You can use a placeholder-image when downloading the new image.
Or you can use the 3rd part library - SDWebImage.

Resources