UITableView load ONCE all cells in background - uitableview

Good day! Very simple question.
I load custom cells in background with dequeueReusableCell 'cos I have images loading from url.
The problem is when I start to scroll. TableView synchronously starts to reload cells, so they "blink" with different content.
If I scroll slowly, it blinks and finally fills cells with the right content, but if I do it faster, it can shuffle content. It's funny to see, but it is still a problem,
I think the best solution is to load cells once and just scroll static table. Is it possible??
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if self.dialogs[indexPath.item].fromID == profileID {
let cell = tableView.dequeueReusableCell(withIdentifier: "dialogMeCell", for: indexPath) as! CellForDialogMe
DispatchQueue.global().async {
let photoURL = self.partners[indexPath.item].userPhoto as! String
let imageData = try? Data(contentsOf: URL(string: photoURL.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)!)!)
let partnerPhoto = UIImage(data: imageData!)
DispatchQueue.main.async {
let partnerName = "\(self.partners[indexPath.item].userName!) " + "\(self.partners[indexPath.item].userSurname!)"
var messageText = ""
if self.dialogs[indexPath.item].hasAttachments! == true {
messageText = "Attachment"
}
else {
messageText = self.dialogs[indexPath.item].text!
}
if self.dialogs[indexPath.item].readState == false {
cell.message.textColor = UIColor.white
cell.message.backgroundColor = UIColor(red: 0.561, green: 0.651, blue: 0.757, alpha: 1.00)
}
cell.fillWithContent(partnerPhoto: partnerPhoto!, partnerName: partnerName, message: messageText, selfPhoto: self.profilePhoto!)
}
}
return cell
}
else {
let cell = tableView.dequeueReusableCell(withIdentifier: "dialogHimCell", for: indexPath) as! CellForDialogHim
DispatchQueue.global().async {
let photoURL = self.partners[indexPath.item].userPhoto as! String
let imageData = try? Data(contentsOf: URL(string: photoURL.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)!)!)
let partnerPhoto = UIImage(data: imageData!)
DispatchQueue.main.async {
let partnerName = "\(self.partners[indexPath.item].userName!) " + "\(self.partners[indexPath.item].userSurname!)"
var messageText = ""
if self.dialogs[indexPath.item].hasAttachments! == true {
messageText = "Attachment"
}
else {
messageText = self.dialogs[indexPath.item].text!
}
if self.dialogs[indexPath.item].readState == false {
cell.partnerName.textColor = UIColor.white
cell.message.textColor = UIColor.white
cell.backgroundColor = UIColor(red: 0.561, green: 0.651, blue: 0.757, alpha: 1.00)
}
cell.fillWithContent(partnerPhoto: partnerPhoto!, partnerName: partnerName, message: messageText)
}
}
return cell
}
}
Thanks!

The best solution is to cache your images. You only make the asynch request if the image is not in your cache. If you have the image in the cache then just show the cached image. While you could just hold a dictionary of [String : UIImage] and use the URL's as keys, this isn't very memory friendly or bandwidth friendly since its not persisted. You can write a much more sophisticated cache, but other people have already done it so I suggest just using one of theirs: SDwebImage AlamofireImage

Related

How can I improve my cellForRowAt function?

I have read that the cellForRowAt method should not be to resource-intensive as it is called frequently while a user is scrolling through a TableView I tried to limit how much went into my cellForRowAt but seem to have overused it because anytime it is called scrolling is interrupted as it loads more data.
Here is my function:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print(indexPath)
var post: PostStruct
var peopleUserIsFollowing: [String] = []
let cell = tableView.dequeueReusableCell(withIdentifier: K.cellIdentifier, for: indexPath) as! PostCell
cell.delegate = self
if postArray.count == 0 {
let instructions = cell.textLabel
instructions?.text = "Press the camera to start Piking!"
instructions?.textAlignment = .center
clearPosts(cell)
}else {
post = postArray[indexPath.row]
if let leftPostArray = userDefaults.array(forKey: fbLeftKey) as? [String]{
votedLeftPosts = leftPostArray
}
if let rightPostArray = userDefaults.array(forKey: fbRightKey) as? [String]{
votedRightPosts = rightPostArray
}
let firstReference = storageRef.child(post.firstImageUrl)
let secondReference = storageRef.child(post.secondImageUrl)
cell.firstImageView.sd_setImage(with: firstReference)
cell.secondImageView.sd_setImage(with: secondReference)
//For FriendsTableView query
let db = Firestore.firestore()
let followingReference = db.collection("following")
.document(currentUser!)
.collection("UserIsFollowing")
followingReference.getDocuments(){(querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
peopleUserIsFollowing.append(document.documentID)
}
}
}
//Fill in labels and imageViews
cell.timer = createTimer(post, cell)
cell.leftTitle.text = post.firstTitle
cell.rightTitle.text = post.secondTitle
cell.postDescription.text = post.postDescription
if post.userPic == "" {
userPic =
"Placeholder"
} else{
userPic = post.userPic
}
let url = URL(string: userPic)
let data = try? Data(contentsOf: url!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
cell.profilePic.image = UIImage(data: data!)
let votesCollection = db.collection("votes").document(post.postID)
getCount(ref: votesCollection, cell: cell)
if(post.uid != currentUser){
cell.userName.text = post.poster
}else{
cell.userName.text = "Me"
cell.tapLeft.isEnabled = false
cell.tapRight.isEnabled = false
}
cell.textLabel?.text = ""
if(post.poster == Auth.auth().currentUser!.uid || post.endDate - Int(NSDate().timeIntervalSince1970) <= 0){
cell.tapRight.isEnabled = false
cell.tapLeft.isEnabled = false
cell.firstImageView.layer.borderWidth = 0
cell.secondImageView.layer.borderWidth = 0
}
else if(votedRightPosts.contains(post.firstImageUrl)){
cell.secondImageView.layer.borderColor = UIColor.green.cgColor
cell.secondImageView.layer.borderWidth = 4
cell.firstImageView.layer.borderWidth = 0
cell.tapRight.isEnabled = false
cell.tapLeft.isEnabled = true
}
else if (votedLeftPosts.contains(post.firstImageUrl)){
cell.firstImageView.layer.borderColor = UIColor.green.cgColor
cell.firstImageView.layer.borderWidth = 4
cell.secondImageView.layer.borderWidth = 0
cell.tapLeft.isEnabled = false
cell.tapRight.isEnabled = true
}
}
return cell
}
I felt that the biggest issue would probably be the three images that are loaded into the cell but I am utilizing caching and when I commented out their loading I experienced the same issue. I have also read that utilizing a model can help which I believe that I did with the PostStruct object. I think I may need to pre-fetch however I cannot figure out where to even start with that. How can I improve my function so that scrolling in my app is smoother?
This would be a great place to use the Instruments tool. Run the time profiler on your app as you scroll through your table view. It will show you where the bulk of the delay is coming from.
Where are your images coming from? Are they loading from your local file system, or are those URLs network URLS?
You might need to refactor your code to return cells without the images, and trigger async code that loads the images in a background thread, installs them into your model, and then updates the cell once the images have finished loading.
I found out that this line of code was the issue:
if post.userPic == "" {
userPic =
"Placeholder"
} else{
userPic = post.userPic
}
let url = URL(string: userPic)
let data = try? Data(contentsOf: url!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
cell.profilePic.image = UIImage(data: data!)
In stead I changed it to utilize SDWebImage like the other two images in the post.

How to hide tableview until images are completely loaded iOS Swift?

My images take a second to load before they appear, which looks bad. On apps such as instagram, the tableview is hidden until the tableview is loaded... how do they do this? I have a loader that I want to display but don't know when to stop it and show tableview and detect the images have first finished loading? Where do I put stopTimer() ?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MainTableViewCell",
for: indexPath) as! MainTableViewCell
let payment = self.payments[indexPath.row]
cell.profilePicture.layer.cornerRadius = cell.profilePicture.frame.size.width / 2
cell.profilePicture.clipsToBounds = true
if let profileImageUrl = payment.picture {
cell.profilePicture.loadImageUsingCacheWithUrlString(profileImageUrl)
}
if payment.message == "none" {
cell.detailsLabel.text = "No Message"
} else {
cell.detailsLabel.text = "\"\(payment.message ?? "")\""
}
}
MY CODE TO FETCH IMAGE IN TABLEVIEW:
let imageCache = NSCache<NSString, AnyObject>()
extension UIImageView {
func loadImageUsingCacheWithUrlString(_ urlString: String) {
self.image = nil
//check cache for image first
if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
self.image = cachedImage
return
}
//otherwise fire off a new download
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
//download hit an error so lets return out
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()
}
}
You can simple use SDWebImage with cocoaPods and use it for async image downloader with cache support. Your cell will look like after ad SDWebImage.
import SDWebImage
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MainTableViewCell",
for: indexPath) as! MainTableViewCell
let payment = self.payments[indexPath.row]
cell.profilePicture.layer.cornerRadius = cell.profilePicture.frame.size.width / 2
cell.profilePicture.clipsToBounds = true
if let profileImageUrl = payment.picture {
cell.profilePicture.sd_setImage(with: profileImageUrl, placeholderImage: UIImage(named: "yourPlaceholderImage.png"))
}
if payment.message == "none" {
cell.detailsLabel.text = "No Message"
} else {
cell.detailsLabel.text = "\"\(payment.message ?? "")\""
}
}
There is no need to hide tableView for downloading image.

How to stop tableview cells from glitching and flickering image when scrolling in swift?

I have a tableview that displays data from Firebase. It displays fine in the tableview but when scrolling begins, the table starts glitching or having the cells image and text reload and flicker which is very ugly. Help! Here's my code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MainTableViewCell", for: indexPath) as! MainTableViewCell
func downloadPicture(finished: () -> Void) {
cell.profilePicture.image = nil
if let imageUrlString = self.payments[indexPath.row].picture,
let imageUrl = URL(string: imageUrlString) {
do {
let imageData = try Data(contentsOf: imageUrl)
cell.profilePicture.image = UIImage(data: imageData)
cell.profilePicture.layer.cornerRadius = cell.profilePicture.frame.size.width / 2
cell.profilePicture.clipsToBounds = true
cell.profilePicture.alpha = 1
}
catch {
print("Error fetching image - \(error)")
}
}
finished()
}
downloadPicture {
print("success")
}
cell.amountLabel.text = "$\(self.payments[indexPath.row].amount ?? "")"
cell.detailsLabel.text = self.payments[indexPath.row].amount ?? ""
return cell
}
You can simply go with SDWebImage An asynchronous image downloader with cache management and efficient to use.
i.e. :
import SDWebImage
yourImageView.sd_setImage(with: URL(string: "yourImageURLFromFirebase.jpg"), placeholderImage: UIImage(named: "placeholder.png"))
#Lukeksaunders just go to GitHub Kinfisher. This library contains all functionality you want.
import Kingfisher
let url = URL(string: "https://example.com/image.png")
imageView.kf.setImage(with: url)
This library cache your images
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MainTableViewCell", for: indexPath) as! MainTableViewCell
if let imageUrlString = self.payments[indexPath.row].picture,
let imageUrl = URL(string: imageUrlString) {
cell.profilePicture.kf.setImage(with: imageUrl)
}
cell.amountLabel.text = "$\(self.payments[indexPath.row].amount ?? "")"
cell.detailsLabel.text = self.payments[indexPath.row].amount ?? ""
return cell
}

Multiple UICollection View on single UIView Scrolling is not working while cell data loading from API

In the home screen section I have three different UICollection view two of them(Top News and Accommodation) getting data from API and last one(Category) have static data and the problem is that while loading the data from API even I am not able to scroll the static section of UICollection view cell but as data loading complete every thing working fine I am not able to find the problem's solution please help me
override func viewDidLoad() {
super.viewDidLoad()
topNewCV.delegate = self
topNewCV.dataSource = self
accommodationCV.delegate = self
accommodationCV.dataSource = self
categoryCV.dataSource = self
categoryCV.delegate = self
//Loading getNearByPlace function
self.getNearByPlace()
}
//cellForItemAt indexPath function
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView == accommodationCV {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "AccommodationCollectionViewCell", for: indexPath) as! AccommodationCollectionViewCell
cell.titleContainer.text = self.accommodationObject.titleArray[indexPath.row]
if self.accommodationObject.titleArray.count == self.accommodationObject.imgArray.count {
if let img = cache.object(forKey: self.accommodationObject.imgArray[indexPath.row] as AnyObject) {
DispatchQueue.global().async {
DispatchQueue.main.async {
cell.imgContainer.image = img as? UIImage
}
}
} else {
DispatchQueue.global().async {
DispatchQueue.main.sync {
cell.imgContainer.image = UIImage(url: URL(string: "\(self.accommodationObject.imgArray[indexPath.row])"))
self.cache.setObject(UIImage(url: URL(string: "\(self.accommodationObject.imgArray[indexPath.row])"))!, forKey: self.accommodationObject.imgArray[indexPath.row] as AnyObject)
}
}
}
} else {
print("Both have not equal data")
}
return cell
} else if collectionView == categoryCV {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CategoryCollectionViewCell", for: indexPath) as! CategoryCollectionViewCell
cell.categorymodel = self.categoryModels?[indexPath.item]
if indexPath.row % 2 == 0 {
cell.categoryCVViewContainer.backgroundColor = colorLiteral(red: 0.3333333333, green: 0.7844525506, blue: 0.6620362924, alpha: 1)
} else {
cell.categoryCVViewContainer.backgroundColor = colorLiteral(red: 1, green: 0.4039215686, blue: 0.4039215686, alpha: 1)
}
return cell
}
return cell
}
// this fun is for getting data from api
func getNearByPlace() {
var strGoogleApi = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=\(user_latitude!), \(user_longitude!)&radius=1000&keyword=hotel&sensor=true&key=abc”
strGoogleApi = strGoogleApi.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
print(strGoogleApi)
var urlRequest = URLRequest(url: URL(string: strGoogleApi)!)
urlRequest.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if error == nil {
if let json = try? JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String: Any]{
if let allResults = json!["results"] as? [[String: Any]] {
print(allResults)
for result in allResults {
var geometry = [String: Any]()
geometry = result["geometry"] as! [String: Any]
var location = [String: Any]()
location = geometry["location"] as! [String: Double]
self.latitudeArray.append(location["lat"] as! Double)
self.longitudeArray.append(location["lng"] as! Double)
let name = result["name"]
var image = [[String: Any]]()
if result["photos"] != nil {
image = result["photos"] as! [[String: Any]]
var img = image[0]
let url = self.getImageFromApi(image: img["photo_reference"] as! String)
self.imgReferenceArray.append(url)
} else {
self.imgReferenceArray.append(self.icons)
}
let place_id = result["place_id"]
let address = result["vicinity"]
if result["name"] != nil {
self.nameArray.append(name as! String)
self.accommodationObject.titleArray = self.nameArray
}
if result["place_id"] != nil {
self.placeIdArray.append(place_id as! String)
} else if result["vicinity"] != nil {
self.addressArray.append(address as! String)
}
}
}
}
OperationQueue.main.addOperation({
if self.nameArray.count != 0 {
DispatchQueue.main.async {
self.accommodationCV.reloadData()
self.categoryCV.reloadData()
}
}
})
self.accommodationObject.imgArray = self.imgReferenceArray
}
}
task.resume()
}
Some very basic tips:
a) take in account you are dealing with multiple threads.. so adding to arrays must be done with a lot of care.
b) STOP previous calls of "task" var if reloading.. for example saving task in an instance var: call task.cancel()

Image in table view [duplicate]

This question already has an answer here:
Asynchronously load image in uitableviewcell
(1 answer)
Closed 5 years ago.
I have a problem with the image in table view cell
The images are downloaded form the Google Firebase and in every cell there is one of that
But when I scroll up or down the images change automatically the index
Here is my code, someone can help me? thanks a lot!
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if postsArray[indexPath.row].imageNoImage == true{
let cell = tableView.dequeueReusableCell(withIdentifier: "imageLook", for: indexPath) as! imageLookTableViewCell
cell.authorLabel.text = self.postsArray[indexPath.row].author
cell.likesCountLabel.text = "\(self.postsArray[indexPath.row].likes!)"
cell.postID = postsArray[indexPath.row].postId
cell.textViewPost.text = self.postsArray[indexPath.row].textPost
let url = URL(string: postsArray[indexPath.row].pathToImage as! String)
if url != nil {
DispatchQueue.global().async {
let data = try? Data(contentsOf: url!)
DispatchQueue.main.async {
if data != nil {
cell.imagePost.image = UIImage(data:data!)
}else{
}
}
}
}
for person in self.postsArray[indexPath.row].peopleWhoLike {
if person == FIRAuth.auth()!.currentUser!.uid {
cell.likesBtn.isHidden = false
break
}
}
return cell
}
Your question is not very descriptive/well written, but I think your problem is that you are not caching your images.
Try this:
let imageCache = NSCache()
let cell = tableView.dequeueReusableCell(withIdentifier: "imageLook", for: indexPath) as! imageLookTableViewCell
cell.authorLabel.text = self.postsArray[indexPath.row].author
cell.likesCountLabel.text = "\(self.postsArray[indexPath.row].likes!)"
cell.postID = postsArray[indexPath.row].postId
cell.textViewPost.text = self.postsArray[indexPath.row].textPost
let url = URL(string: postsArray[indexPath.row].pathToImage as! String)
if url != nil {
if let cachedImage = imageCache.objectForKey(url) as? UIImage {
cell.imagePost.image = cachedImage
return
}
DispatchQueue.global().async {
let data = try? Data(contentsOf: url!)
DispatchQueue.main.async {
if data != nil {
if let myImageName = UIImage(data:data!){
imageCache.setObject(myImageName, forKey: url)
}
cell.imagePost.image = UIImage(data:data!)
}else{
}
}
}
}
for person in self.postsArray[indexPath.row].peopleWhoLike {
if person == FIRAuth.auth()!.currentUser!.uid {
cell.likesBtn.isHidden = false
break
}
}
return cell
}

Resources