Swift TableView making multiple network requests for images - swift

I have a table in swift. Whenever I scroll up and down the tableview it runs the getImageForCell function again even though the image has already been loaded. Is there a way for this not to happen. Below is my code.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ReviewTableViewCell", for: indexPath) as! ReviewTableViewCell
let review = json[reviewType][indexPath.row]
cell.nameLabel.text = review[userType]["name"].stringValue
cell.reviewLabel.text = review["message"].stringValue
cell.dateLabel.text = review["created_at"].stringValue
cell.ratingStars.rating = Double(review["rating"].intValue)
getImageForCell(url: review[userType]["photo_url"].stringValue, cell: cell)
return cell
}
func getImageForCell(url: String, cell: ReviewTableViewCell) {
Alamofire.request(url).responseImage { response in
if let downloadedImage = response.result.value {
print("downloaded image \(downloadedImage)")
DispatchQueue.main.async {
cell.profileImageView.image = downloadedImage
}
}
}
}

This is due to both not cacheing the image, and also not reusing the cell - in your view you are recreating the cell each time - this would not specifically fix the image issue, but will improve performance. My favourite cache image extension is kingfisher (no affiliation), although alamofire can be used to cache images too.
https://github.com/onevcat/Kingfisher

Related

Swift TableView data only loading after scrolling to bottom

I have a code where I am fetching images from URLs and displaying them in a tableview, however the images only show up inside tableview when scrolling down to far bottom of the screen, as show in the image below.
TableView before scrolling to bottom, screenshot
TableView after scrolling to bottom, screenshot
Code for the fetching images and tableView
#IBOutlet weak var tableView: UITableView!
let imageArray = [UIImage()]
let urlArray = ["https://media.api-sports.io/football/teams/50.png","https://media.api-sports.io/football/teams/47.png","https://media.api-sports.io/football/teams/49.png","https://media.api-sports.io/football/teams/46.png","https://media.api-sports.io/football/teams/48.png"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.register(UINib(nibName: "ImageCell", bundle: nil), forCellReuseIdentifier: "imageCell") // Do any additional setup after loading the view.
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "imageCell", for: indexPath) as! ImageCell
cell.clubImage.load(url: URL(string: urlArray[indexPath.row])!)
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return urlArray.count
}
}
extension UIImageView {
func load(url: URL) {
DispatchQueue.global().async { [weak self] in
if let data = try? Data(contentsOf: url) {
if let image = UIImage(data: data) {
DispatchQueue.main.async {
self?.image = image
}
}
}
}
}
}
How can I show the images as soon as the app loads?
Thanks
The issue is that the table view is calculating the size of the cell before an image has been loaded. The image view in the cell defaults to a height of zero since it has no content yet. I'd suspect something in the table view implementation causes the cells sizes to be recalculated when reaching the bottom which allows them to appear.
One solution could be to add a constraint to the image view so that it has a fixed size before an image is loaded. The simplest is if all cells can have the same size. If they need different sizes, that would need to be set in cellForRowAt either before loading the image, or the image loading would need to be moved so it's done outside of the cell allowing the cell to be reloaded with the correct size later.
Make your load method with completion:
func load(url: URL, completion: #escaping (UIImage?) -> Void) {
DispatchQueue.global().async { in
if let data = try? Data(contentsOf: url) {
if let image = UIImage(data: data) {
DispatchQueue.main.async {
completion(image)
}
}
}
}
}
Then in cellForRawAt call the method and add your loaded imaged to your imageView:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "imageCell", for: indexPath) as! ImageCell
cell.clubImage.load(url: URL(string: urlArray[indexPath.row])!) { image in
// display your image here...
}
return cell
}

How to properly display pictures in a TableView from Firebase Storage in SWIFT

I have a chat app where people can talk in a group and a little picture is displayed in each cell to show who is talking. I managed to display these pictures from Firebase storage but it is not always the right picture which is displayed at the right place.
It only works when I go to the previous View Controller and coming back the chat View to see the pictures displayed properly in each cell.
I tried to use DispatchQueue.main.async {} probably not in the good way cause it did not work for me.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let message = messageArray[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "customMessageCell", for: indexPath) as! CustomMessageCell
cell.selectionStyle = .none
// CHANGE TEXT ACCORDING TO SENDER
if message.sender == Auth.auth().currentUser?.email{
cell.messageBubble.backgroundColor = UIColor(red:0.30, green:0.68, blue:1.5, alpha:1.0)
// ...
} else {
cell.messageBubble.backgroundColor = UIColor(red:0.94, green:0.94, blue:0.94, alpha:1.0)
// ...
}
let theTimeStamp = messageArray[indexPath.row].createdAt
let doubleTime = Double(theTimeStamp)
let myDate = Date(timeIntervalSince1970: doubleTime )
let dateToShow = myDate.calenderTimeSinceNow()
cell.messageBodyTextView.text = messageArray[indexPath.row].messageBody
cell.usernameLabel.text = messageArray[indexPath.row].name
cell.timeLabel.text = dateToShow
let imagePath = self.storageRef.reference(withPath:"\(message.uid)/resizes/profilImage_150x150.jpg")
imagePath.getData(maxSize: 10 * 1024 * 1024) { (data, error) in
if let error = error {
cell.userPicture.image = UIImage(named: "emptyProfilPic")
cell.userPicture.layer.cornerRadius = cell.userPicture.frame.height / 2
cell.userPicture.clipsToBounds = true
print("Got an error fetching data : \(error.localizedDescription)")
return
}
if let data = data {
cell.userPicture.image = UIImage(data: data)
cell.userPicture.layer.cornerRadius = cell.userPicture.frame.height / 2
cell.userPicture.clipsToBounds = true
}
}
return cell
}
Thank you for your help !
You have to prepare the cell to be reusable with the proper override prepareForReuse().
For more clean code I suggest to you to implement the cells in separate cocoa Touch classes so it's easier to override and prepare for next data incoming, avoiding your problem.
What I mean it's a sort of this:
class mineCell:UITableViewCell {
#IBOutlet weak var text:UILabel!
#IBOutlet weak var img:UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
}
func updateCell(dataIn){
.
.
}
override func prepareForReuse() {
text.text = ""
img.image = nil
}
In your cellForRowAt table implementation just call the update function and pass your data like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = "mineCell"
if let cell = mineTable.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as? mineCell {
updateCell(dataIn)
return cell
}
return mineCell()
}
In this way you are always sure that your cell will be ready for every reuse and not loading wrong data from the cell above.
Just to let you know, the problem was thaT I was reloading the table View after each message loaded. Instead, the best solution was to add a row to the tableview without reloaded the tableview after each message :
self.ConvertationTableView.insertRows(at: [IndexPath(row: self.messageArray.count - 1, section: 0)], with: .automatic)

Why are the icons from my cell changing after scrolling? [duplicate]

This question already has answers here:
Duplication in values of tableView when Scrolling Swift
(3 answers)
Closed 2 years ago.
I made a list where a user can see if an event is started, paused or stopped.
An event which is just created has no icon and no state and is returned with a created timestamp.
The problem which I have is that blank cells without an icon change their icon to "stop" when scrolling out of the view.
events.asObservable()
.bind(to:tableView.rx.items) { (tableView, row, event) in
let cell = tableView.dequeueReusableCell(withIdentifier: "EventCell") as! EventCell
cell.detailLabel.text = "created: \(self.dateFormat.string(from: event.created!)) Uhr"
cell.titleLabel.text = event.name
if let date = event.started {
cell.icon.image = UIImage(named: "start")
var dateStr = "started: \(self.dateFormat.string(from: date))"
if let paused = self.isPaused(event: event) {
if paused {
dateStr = "\(dateStr), paused"
cell.icon.image = UIImage(named: "pause")
}
}
if let dateEnded = event.ended {
dateStr = "\(dateStr), ended: \(self.dateFormat.string(from: dateEnded))"
cell.icon.image = UIImage(named: "stop")
}
}
return cell
}.disposed(by: disposeBag)
What is happening?
This occurs because of cell reuse. You should probably override the prepareForReuse method in EventCell and set the image to an initial image or nil before it's being reused at the next cellForRow (or cellForItem in UICollectionView) to fix this issue.
class EventCell: UITableViewCell { // (or UICollectionViewCell if issue is in UICollectionView)
//...
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil // or a default placeholder image.
}
}
Alternate Approach: You could also fix this issue in the cellForRow in UITableView (or cellForItem in UICollectionView) method by setting the imageView.image to nil or the placeholder image at the top and proceed.
For UITableView:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath)
cell.imageView.image = nil // or a default placeholder image.
//...
return cell
}
For UICollectionView:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath)
cell.imageView.image = nil // or a default placeholder image.
// ...
return cell
}

How to set Image at current Cell (Kingfisher)? Swift

I have an TableView with custom cells. Label smiles contain links.
How can I put Image from link to current ImageView'cell? My code
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = "ClientCell"
self.cell = self.tableView.dequeueReusableCell(withIdentifier: identifier) as? customChatCell
let text = message[Constants.MessageFields.text] ?? ""
let selectedCell = self.tableView.cellForRow(at: indexPath) as? customChatCell
***
if text.range(of:"smiles") != nil {
let url = URL(string: text)
self.cell![indexPath.row].smile.kf.setImage(with: url)
}
***
}
not working. I'm getting error for line self.cell![indexPath.row].smile.kf.setImage(with: url)
Type 'customChatCell' has no subscript members
I'm using Kingfisher. If I use code
self.cell.smile.kf.setImage(with: url)
image putting into all cells, not for current.
Please help me fix it.
You should remove keeping the cell reference at class level. Your cellForRow should look like this,
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = "ClientCell"
let cell = tableView.dequeueReusableCell(withIdentifier: identifier) as? customChatCell
let text = message[Constants.MessageFields.text] ?? ""
if text.range(of:"smiles") != nil {
let url = URL(string: text)
cell.smile.kf.setImage(with: url)
} else {
// Reset image to nil here if it has no url
cell.smile.image = nil
}
}
Remember, you are using a single UIView(i.e, customChatCell) for each cell in UITableView so when you dequeue a cell it's your responsibility to update/reset the UI elements according to your data for each cell.

How do I let my TodayWidget read out of CoreData in Swift?

So I'm making this app in which the user can store data in CoreData and see it in a table view.
This works like it should, but now I want the data to been seen in a table view, which is located in a today widget.
I already tried to do this the "regular way" by trying to fetch, but the widget isn't able to read it via the AppDelegate file.
The error that I get says the following : " 'shared' is unavailable: Use view controller based solutions where appropriate instead."
I also get the error : "Use of undeclared type 'AppDelegate'"
Is anyone capable in helping me?
Thanks,
EDIT :
This is the code I use to fetch the data in the ViewController :
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let request = NSFetchRequest<NSManagedObject>(entityName: "ToDoList")
do {
toDoItems = try managedContext.fetch(request)
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
toDoTableView.tableFooterView = UIView()
self.toDoTableView.reloadData()
toDoTableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
let cell = UITableViewCell()
cell.contentView.backgroundColor = UIColor.clear
cell.backgroundColor = UIColor.clear
toDoTableView.backgroundColor = UIColor.clear
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return toDoItems.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let activities = toDoItems[indexPath.row]
let cell = toDoTableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text =
activities.value(forKeyPath: "todoitems") as? String
return cell
}
}
Please note that this is not my entire code, but some fragments which all together create the fetch function