Swift TableView data only loading after scrolling to bottom - swift

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
}

Related

Loading an Image file from Firebase Storage using SDWebImage

I am trying to load an image from my Firebase Storage to be displayed on my app view.
I have 2 classes mainly
Main "ClassView" that includes a tableView.
A custom tableViewCell - "ClassCell" (This class includes an imageView where I want to display the picture from firestore. - imageView is called classImage.
Below is the tableView Data Source Extension.
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.register(UINib(nibName: K.tableCellNibName, bundle: nil), forCellReuseIdentifier: K.tableCellIdentifier)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let storageRef = storage.reference()
let reference = storageRef.child(K.FStore.classImagesPath)
let placeholderImage = UIImage(named: "placeholder.png")
let imageView: UIImageView = self.imageView
let cell = tableView.dequeueReusableCell(withIdentifier: K.tableCellIdentifier, for: indexPath) as! ClassCell
//below is where I try to set the my image but it is not changing anything
cell.classImage?.sd_setImage(with: reference, placeholderImage: placeholderImage)
//This I tried to make sure that I am can access the image right and it worked just fine
// cell.classImage?.backgroundColor = .black
return cell
}
The Image is not displayed using the sd_setImage method. Any help would be highly appreciated if I have any error in my code or missing a declaration anywhere.
Below is what I am getting when I run the simulator. Instead of the image displayed in these black boxes, they are empty. Also, the imageView.image is returning nil so most probably the sd_setImage is not placing the image right.
self.imgSidebarMenuImage.sd_imageIndicator = SDWebImageActivityIndicator.whiteLarge
self.imgSidebarMenuImage.sd_setImage(with: URL(string: (person["image"] as! String)), placeholderImage: UIImage(named: "logo"))
try this
#1
let storage = Storage.storage().reference(withPath: “YOUR_PATH”)
storage.downloadURL { (url, error) in
if error != nil {
print((error?.localizedDescription)!)
return
}
print(“Download success”)
//url = your will get an image URL
self.someImageURL = url!
}
#2
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// cell identifier methods
var item = itemData[indexPath.row]
let referenceImage: StorageReference = storageRef.child("YOUR_PATH")
let url = URL(string: referenceImage!)!
cell.itemImage?.sd_setImage(with: url, placeholderImage: #imageLiteral(resourceName: "placeholder"))
}
}
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)

My TableView initializes before JSON data is received. How do you prevent this?

I am coding an app that receives air pollution data from API. The code passes JSON from aqicn.com. The data is updated too slowly and the tableview is displayed first. So there isn't any data displayed for the data view. I have tried DispatchQueue.main.async but it did not work. Delays using DispatchQueue.main.asyncAfter did not work. Been stuck on this for several days now. Please give me suggestions or solutions!
import UIKit
import MapKit
class ViewController: UIViewController, CLLocationManagerDelegate, UITableViewDataSource{
#IBOutlet weak var tableView: UITableView!
let searchController = UISearchController(searchResultsController: nil)
let locationManager = CLLocationManager()
var pm25Data:Int?
var pm10Data:Int?
override func viewDidLoad() {
super.viewDidLoad()
performRequest(with: "https://api.waqi.info/feed/Tainan/?token=\(C.APIkey)")
tableView.dataSource = self
}
func performRequest(with typeURL: String)
{
if let url = URL(string: typeURL)
{
print("pass")
let session = URLSession.shared
print("pass")
let dataTask = session.dataTask(with: url) { (data, response, error) in
if error == nil && data != nil
{
let decoder = JSONDecoder()
do
{
let info = try decoder.decode(AirData.self, from: data!)
self.pm25Data = info.data.iaqi.pm25.v
self.pm10Data = info.data.iaqi.pm10.v
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch
{
print("we have an error")
}
}
}
dataTask.resume()
}
}
override func viewWillAppear(_ animated: Bool) {
tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "cellId")
}
override open var shouldAutorotate:Bool
{
return false
}
func didFailWithError(error: Error) {
print(error)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as! TableViewCell
if(indexPath.row == 0)
{
cell.descriptionName?
.text = "PM 2.5"
cell.valueNumber?.text = "\(pm25Data)"
}
else
{
cell.descriptionName?.text = "PM 10"
cell.valueNumber?.text = "\(pm10Data)"
}
return cell
}
}
One possible way...
Maintain an array of objects containing the data for each row, I will call it dataArray so that each object in the array would contain the information required to populate each respective row. Then, in your cellForRowAt function, after dequeueing the cell like you currently have implemented, do something like this:
if indexPath.row == 0 {
cell.descriptionName?.text = "PM 2.5"
cell.valueNumber?.text = "\(pm25Data)"
// Other setup unique to the first cell
} else {
cell.descriptionName?.text = dataArray[indexPath.item - 1].description
cell.valueNumber?.text = dataArray[indexPath.item - 1].description
// Other setup unique to all following cells
}
Also, implement your numberOfRowsInSection with one line, simply return dataArray.count
Then, once your dataArray is filled with objects containing the data, call self.tableView.reloadData()
In a broader but still simplified sense:
Every time you call reloadData(), a few things happen:
The tableView will call its numberOfRowsInSection function
The tableView will call its cellForRowAt function repeatedly the number of times returned from numberOfRowsInSection and set up each row individually
The tableView will call other functions like heightForRowAt as necessary to set up each row
Some things to keep in mind:
Call reloadData() every time you want to refresh the tableView (with new data, for example)
You don't really set up each cell individually, you set up the logic to populate cells with data pulled from an itemized source (usually an array)
All you then have to do is maintain the dataArray and then call reloadData() every time you want to refresh the tableView. The cellForRowAt function handles populating each cell with data.
A useful tutorial:
https://www.youtube.com/watch?reload=9&v=VFtsSEYDNRU

How to bind Switch state to an object in Swift?

In my app I have a switch and I want it to be an indicator for saving images. For now I have just a button that saves all the images.
It just makes more sense with an example
What I've tried:
func saveTapped() {
let cell = collectionView?.cellForItem(at: indexPath) as! CustomCell
for image in images where cell.savingSwitch.isOn {
...
But I can't access indexPath. How should I call this Save method in order to access a particular row in my collectionView? Or is there another way?
First, you'll need a way to store the "save" settings alongside each image in your table, e.g. by keeping the image and the flag in a struct:
struct TableEntry {
let image: UIImage
var save = false
}
and use a var images: [TableEntry] in the data source of your tableview.
You can then use the tag property of each UISwitch to store the row it's in, e.g. in
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(...)
cell.switch.tag = indexPath.row
cell.switch.isOn = self.images[indexPath.row].save
cell.imageView.image = self.images[indexPath.row].image
return cell
}
and then use the tag in the method that's called when the switch value changes to know which image is being referred to:
#IBAction func switchChanged(_ sender: UISwitch) {
self.images[sender.tag].save = sender.isOn
}
func saveTapped() {
let imagesToSave = self.images.filter { $0.save }
}
In your CustomCell, you can add a closure to be fired when switch state is changed like below,
class CustomCell: UITableViewCell {
var onSwitchStateChange: ((Bool) -> Void)?
#IBAction func switchTapped(_ sender: UISwitch) {
self.onSwitchStateChange?(sender.isOn)
}
}
then you can update your cellForRowAt like below,
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
cell.onSwitchStateChange = { state in
guard state else { return }
let image = images[indexPath.row]
// Upload Image
}
}

Swift TableView making multiple network requests for images

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