Is this the proper way to use heightForRowAt? - ios

Im trying to implement dynamically sized row heights based on the size of downloaded images. The problem I am encountering is that the images are not downloaded when the function heightForRowAt is run. What is the proper way to implement this code. images is an array of UIImage, rowHeights is an array of type CGFloat and imageURLS is a string array of imageURLS.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Reuse", for: indexPath) as! TableViewCell
// Configure the cell...
///////////////////////
if(cell.cellImageView.image == nil){
let downloadURL = URL(string: self.imageURLS[indexPath.row])
URLSession.shared.dataTask(with: downloadURL!) { (data, _, _) in
if let data = data {
let image = UIImage(data: data)
DispatchQueue.main.async {
cell.cellImageView.image = image
cell.cellImageView.contentMode = .scaleAspectFit
self.images.insert(image!, at: 0)
let aspectRatio = Float((cell.cellImageView?.image?.size.width)!/(cell.cellImageView?.image?.size.height)!)
print("aspectRatio: \(aspectRatio)")
tableView.rowHeight = CGFloat(Float(UIScreen.main.bounds.width)/aspectRatio)
print("tableView.rowHeight: \(tableView.rowHeight)")
self.rowHeights.insert(CGFloat(Float(UIScreen.main.bounds.width)/aspectRatio), at: 0)
tableView.reloadRows(at: [indexPath], with: .top)
}
}
}.resume()
}
///////////////////////
return cell
}
//What is the proper way to implement this function
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
print("Im in height for row")
return CGFloat(0.0)
}

If your asynchronous request may change the height of the cell, you should not update the cell directly, but rather you should completely reload the cell.
So, heightForRowAt and cellForRowAt will be called once for each visible cell before the image is retrieved. Since the image hasn't been retrieved yet, heightForRowAt will have to return some fixed value appropriate for a cell with no image. And cellForRowAt should detect that the image has not been retrieved and initiate that process. But when the image retrieval is done, rather than updating the cell directly, cellForRowAt should call reloadRows(at:with:). That will start the process again for this row, including triggering heightForRowAt to be called again, too. But this time, the image should be there, so heightForRowAt can now return an appropriate height and cellForRowAt can now just update the image view with no further network request.
For example:
class ViewController: UITableViewController {
private var objects: [CustomObject]!
override func viewDidLoad() {
super.viewDidLoad()
objects = [
CustomObject(imageURL: URL(string: "https://upload.wikimedia.org/wikipedia/commons/e/e8/Second_Life_Landscape_01.jpg")!),
CustomObject(imageURL: URL(string: "https://upload.wikimedia.org/wikipedia/commons/7/78/Brorfelde_landscape_2.jpg")!)
]
}
let imageCache = ImageCache()
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CustomCell
let imageURL = objects[indexPath.row].imageURL
if let image = imageCache[imageURL] {
// if we got here, we found image in our cache, so we can just
// update image view and we're done
cell.customImageView.image = image
} else {
// if we got here, we have not yet downloaded the image, so let's
// request the image and then reload the cell
cell.customImageView.image = nil // make sure to reset the image view
URLSession.shared.dataTask(with: imageURL) { data, _, error in
guard let data = data, error == nil else {
print(error ?? "Unknown error")
return
}
if let image = UIImage(data: data) {
self.imageCache[imageURL] = image
DispatchQueue.main.async {
// NB: This assumes that rows cannot be inserted while this asynchronous
// request is underway. If that is not a valid assumption, you will need to
// go back to your model and determine what `IndexPath` now represents
// this row in the table.
tableView.reloadRows(at: [indexPath], with: .middle)
}
}
}.resume()
}
return cell
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let imageURL = objects[indexPath.row].imageURL
if let image = imageCache[imageURL] {
let size = image.size
return view.bounds.size.width * size.height / size.width
} else {
return 0
}
}
}
Where a simple image cache (which is not relevant to your question, but I include for the sake of completeness) is as follows:
class ImageCache {
private let cache = NSCache<NSURL, UIImage>()
private var observer: NSObjectProtocol!
init () {
observer = NotificationCenter.default.addObserver(forName: .UIApplicationDidReceiveMemoryWarning, object: nil, queue: nil) { [weak self] _ in
self?.cache.removeAllObjects()
}
}
deinit {
NotificationCenter.default.removeObserver(observer)
}
subscript(key: URL) -> UIImage? {
get {
return cache.object(forKey: key as NSURL)
}
set (newValue) {
if let image = newValue {
cache.setObject(image, forKey: key as NSURL)
} else {
cache.removeObject(forKey: key as NSURL)
}
}
}
}

Related

How to get different image from assets and assign it to image view in different table view cells

I am trying to add an image to my tableview cell by using an NFC reader session. So, my problem here is that every first reader session, I am getting the correct image in image view, but when I try the reader session the second time, I am stuck with two same last assigned image on both cells of my table view. I know it because of tableView.dequeueReusableCell method, but I am not sure which method to use to get correct image incorrect cells.
I have also attached a screenshot to make more clear of what I mean.
In the screenshot is should see an image of a water bottle from my assets, but instead, I am getting the last assigned image to every cell
Here is the code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
cell.nfcModel = arrData[indexPath.row]
// IMG CELL
cell.img.image = UIImage(named: name)
return cell
}
Not an expert in NFC readers.
1.Create an array of products to store product data from NFC render.
2.in tableView func cellForRowAt you can render the images from
favoriteMovies using displayMovieImage function.
Sidenote:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var favoriteMovies: [Movie] = []
override func viewWillAppear(_ animated: Bool) {
mainTableView.reloadData()
super.viewWillAppear(animated)
if favoriteMovies.count == 0 {
favoriteMovies.append(Movie(id: "tt0372784", title: "Batman Begins", year: "2005", imageUrl: "https://images-na.ssl-images-amazon.com/images/M/MV5BNTM3OTc0MzM2OV5BMl5BanBnXkFtZTYwNzUwMTI3._V1_SX300.jpg"))
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let moviecell = tableView.dequeueReusableCell(withIdentifier: "customcell", for: indexPath) as! CustomTableViewCell
let idx: Int = indexPath.row
moviecell.tag = idx
//title
moviecell.movieTitle?.text = favoriteMovies[idx].title
//year
moviecell.movieYear?.text = favoriteMovies[idx].year
// image
displayMovieImage(idx, moviecell: moviecell)
return moviecell
}
func displayMovieImage(_ row: Int, moviecell: CustomTableViewCell) {
let url: String = (URL(string: favoriteMovies[row].imageUrl)?.absoluteString)!
URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { (data, response, error) -> Void in
if error != nil {
print(error!)
return
}
DispatchQueue.main.async(execute: {
let image = UIImage(data: data!)
moviecell.movieImageView?.image = image
})
}).resume()
}

Reload TableView After Deleting, Adding, or Modifying Firestore Document and Paginating Results

I am retrieving documents from Firebase Firestore and displaying them in a table view. From the table view I want to be able to delete and add items. I also modify documents from the item detail view. I'll focus on my issues deleting items for this question though. I'm getting paginated results with my query by using the last snapshot to only get the next set of items. I'm also using a listener to get realtime updates for when items are modified. The issue with deleting is how to I handle it correctly? What I currently have deletes items just fine but then doubles the remaining rows in the table view.
var items = [Item]()
var itemQuery: Query?
var lastSnapshot: QueryDocumentSnapshot?
func getItems() {
if lastSnapshot == nil {
itemQuery = Firestore.firestore().collection("items").whereField("collection", isEqualTo: self.collection!.id).order(by: "name").limit(to: 25)
} else {
itemQuery = itemQuery?.start(afterDocument: lastSnapshot!)
}
itemQuery!.addSnapshotListener( { (snapshot, error) in
guard let snapshot = snapshot else {
return
}
if snapshot.documents.last != nil {
self.lastSnapshot = snapshot.documents.last
} else {
return
}
if let error = error {
print(error.localizedDescription)
} else {
for document in snapshot.documents {
let docName = document["name"] as? String
let docId = document.documentID
let docImages = document["images"] as? [String]
let docCollection = document["collection"] as? String
let docInfo = document["info"] as? String
let docQuantity = document["quantity"] as? Int
let item = Item(id: docId, name: docName!, collection: docCollection!, info: docInfo!, images: docImages!, quantity: docQuantity!)
self.items.append(item)
}
if self.items.count >= 25 {
self.addFooter()
}
self.tableView.reloadData()
}
})
}
func deleteItem(at indexPath: IndexPath) {
let itemToDelete = items[indexPath.row]
// Delete images from storage
for url in itemToDelete.images {
let store = Storage.storage()
let storageRef = store.reference(forURL: url)
storageRef.delete { error in
if let error = error {
print(error.localizedDescription)
} else {
print("Image file deleted successfully")
}
}
}
Firestore.firestore().collection("items").document(itemToDelete.id).delete() { error in
if let error = error {
print(error.localizedDescription)
} else {
print("Item deleted")
}
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("numberOfRows(): \(items.count)")
return items.count
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath) as! ItemCell
let item = items[indexPath.row]
cell.itemNameLabel.text = item.name
if item.images.count > 0 {
let thumbnailUrl = item.images[0]
cell.itemImageView.sd_setImage(with: URL(string: thumbnailUrl), placeholderImage: UIImage(named: "photo"), completed: { (image, error, cacheType, imageUrl) in
cell.itemImageView.roundCornersForAspectFit(radius: 10)
})
} else {
cell.itemImageView.image = UIImage(named: "photo")
}
return cell
}
// Override to support conditional editing of the table view.
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
// Override to support editing the table view.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
print("Items before delete: \(items.count)")
deleteItem(at: indexPath)
// items.removeAll()
// tableView.reloadData()
items.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
print("Items after delete: \(items.count)")
}
}
You can use Property Observer to handle your tableView.reloadData()
var items = [Item]() {
didSet {
tableView.reloadData()
}
}
what it does above is whenever variable items is modified, it will trigger didSet {} block of code.
Hope is will answer your question.

Limit the amount of cells shown in tableView, load more cells when scroll to last cell

I'm trying to set up a table view that only shows a specific amount of cells. Once that cell has been shown, the user can keep scrolling to show more cells. As of right now I'm retrieving all the JSON data to be shown in viewDidLoad and storing them in an array. Just for example purposes I'm trying to only show 2 cells at first, one the user scrolls to bottom of screen the next cell will appear. This is my code so far:
class DrinkViewController: UIViewController {
#IBOutlet weak var drinkTableView: UITableView!
private let networkManager = NetworkManager.sharedManager
fileprivate var totalDrinksArray: [CocktailModel] = []
fileprivate var drinkImage: UIImage?
fileprivate let DRINK_CELL_REUSE_IDENTIFIER = "drinkCell"
fileprivate let DRINK_SEGUE = "detailDrinkSegue"
var drinksPerPage = 2
var loadingData = false
override func viewDidLoad() {
super.viewDidLoad()
drinkTableView.delegate = self
drinkTableView.dataSource = self
networkManager.getJSONData(function: urlFunction.search, catagory: urlCatagory.cocktail, listCatagory: nil, drinkType: "margarita", isList: false, completion: { data in
self.parseJSONData(data)
})
}
}
extension DrinkViewController {
//MARK: JSON parser
fileprivate func parseJSONData(_ jsonData: Data?){
if let data = jsonData {
do {
let jsonDictionary = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? [String : AnyObject]//Parses data into a dictionary
// print(jsonDictionary!)
if let drinkDictionary = jsonDictionary!["drinks"] as? [[String: Any]] {
for drink in drinkDictionary {
let drinkName = drink["strDrink"] as? String ?? ""
let catagory = drink["strCategory"] as? String
let drinkTypeIBA = drink["strIBA"] as? String
let alcoholicType = drink["strAlcoholic"] as? String
let glassType = drink["strGlass"] as? String
let drinkInstructions = drink["strInstructions"] as? String
let drinkThumbnailUrl = drink["strDrinkThumb"] as? String
let cocktailDrink = CocktailModel(drinkName: drinkName, catagory: catagory, drinkTypeIBA: drinkTypeIBA, alcoholicType: alcoholicType, glassType: glassType, drinkInstructions: drinkInstructions, drinkThumbnailUrl: drinkThumbnailUrl)
self.totalDrinksArray.append(cocktailDrink)
}
}
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
}
}
DispatchQueue.main.async {
self.drinkTableView.reloadData()
}
}
//MARK: Image Downloader
func updateImage (imageUrl: String, onSucceed: #escaping () -> Void, onFailure: #escaping (_ error:NSError)-> Void){
//named imageData because this is the data to be used to get image, can be named anything
networkManager.downloadImage(imageUrl: imageUrl, onSucceed: { (imageData) in
if let image = UIImage(data: imageData) {
self.drinkImage = image
}
onSucceed()//must call completion handler
}) { (error) in
onFailure(error)
}
}
}
//MARK: Tableview Delegates
extension DrinkViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//return numberOfRows
return drinksPerPage
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = drinkTableView.dequeueReusableCell(withIdentifier: DRINK_CELL_REUSE_IDENTIFIER) as! DrinkCell
//get image from separate url
if let image = totalDrinksArray[indexPath.row].drinkThumbnailUrl{//index out of range error here
updateImage(imageUrl: image, onSucceed: {
if let currentImage = self.drinkImage{
DispatchQueue.main.async {
cell.drinkImage.image = currentImage
}
}
}, onFailure: { (error) in
print(error)
})
}
cell.drinkLabel.text = totalDrinksArray[indexPath.row].drinkName
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let image = totalDrinksArray[indexPath.row].drinkThumbnailUrl{
updateImage(imageUrl: image, onSucceed: {
}, onFailure: { (error) in
print(error)
})
}
performSegue(withIdentifier: DRINK_SEGUE, sender: indexPath.row)
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastElement = drinksPerPage
if indexPath.row == lastElement {
self.drinkTableView.reloadData()
}
}
}
I saw this post: tableview-loading-more-cell-when-scroll-to-bottom and implemented the willDisplay function but am getting an "index out of range" error.
Can you tell me why you are doing this if you are getting all results at once then you don't have to limit your display since it is automatically managed by tableview. In tableview all the cells are reused so there will be no memory problem. UITableViewCell will be created when it will be shown.
So no need to limit the cell count.
I dont now what you are doing in your code but:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastElement = drinksPerPage // no need to write this line
if indexPath.row == lastElement { // if block will never be executed since indexPath.row is never equal to drinksPerPage.
// As indexPath starts from zero, So its value will never be 2.
self.drinkTableView.reloadData()
}
}
Your app may be crashing because may be you are getting only one item from server.
If you seriously want to load more then you can try this code:
Declare numberOfItem which should be equal to drinksPerPage
var numberOfItem = drinksPerPage
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//return numberOfRows
return numberOfItem
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == numberOfItem - 1 {
if self.totalDrinksArray.count > numberOfItem {
let result = self.totalDrinksArray.count - numberOfItem
if result > drinksPerPage {
numberOfItem = numberOfItem + drinksPerPage
}
else {
numberOfItem = result
}
self.drinkTableView.reloadData()
}
}
}

Using Almofireimage to set image in a Custom Tableview Cell

I'm using Alamofireimage to set an image based on a remote url on a UIImageView in my Custom UITableViewCell however the results are (1) the images aren't set until you scroll and (2) even though I'm using StackViews for my autolayout the sizes of the images displayed in the app vary wildly. Any idea what I'm doing wrong?
import UIKit
import Alamofire
import AlamofireImage
class AppsTableViewController: UITableViewController {
let session = URLSession.shared
var rekos: [Reko]? {
didSet {
DispatchQueue.main.async {
self.tableView?.reloadData()
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
RekoManager().downloadAndConvertRekos(rekoType: .apps) { (result) in
print("Reko Count = \(result.count)")
self.rekos = result
}
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let count = rekos?.count else {return 0}
return count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "AppCell", for: indexPath) as! AppsTableViewCell
if let reko = rekos?[indexPath.row] {
cell.titleLabel.text = reko.title
cell.descriptionLabel.text = reko.description
if let imageUrlString = reko.imageUrlString {
if let imageURL = URL(string: imageUrlString) {
//TODO IMPLEMENT SPINNER OVER IMAGE
cell.appImageView.af_setImage(withURL: imageURL, placeholderImage: UIImage(named: "placeholder"), filter: nil, progress: nil, progressQueue: DispatchQueue.main, imageTransition: .noTransition, runImageTransitionIfCached: false, completion: { (result) in
cell.setNeedsLayout()
cell.layoutIfNeeded()
})
}
}
}
return cell
}
}

Using UIImageView+AFNetworking, images doesn't appear until scrolling

I'm using UIImageView+AFNetworking to load images in table cell, but
images downloaded from server aren't shown in cell until scroll down and up again. I thought that I needed to reload data in tableView, but I don't know where to put said reload code.
Here's the code:
#IBOutlet weak var refresherTool: UIRefreshControl!
override func viewDidLoad() {
super.viewDidLoad()
let _ = dataRefresher.sharedInstance.refreshVideos(fromUrl: "https://api.vid.me/videos/featured", withLimit: "100", withOffset: "0", completion: {
featuredVideos in dataRefresher.sharedInstance.featuretVideos = featuredVideos
DispatchQueue.main.async {
self.tableView.reloadData()
}
})
}
#IBAction func refreshBtn(_ sender: UIRefreshControl) {
let _ = dataRefresher.sharedInstance.refreshVideos(fromUrl: "https://api.vid.me/videos/featured", withLimit: "100", withOffset: "0", completion: {
featuredVideos in dataRefresher.sharedInstance.featuretVideos = featuredVideos
self.refresherTool.endRefreshing()
DispatchQueue.main.async{
self.tableView.reloadData()
}
})
refresherTool.endRefreshing()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if dataRefresher.sharedInstance.featuretVideos == nil {
return 0
}
return 100
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "featuredCell", for: indexPath)
// if dataRefresher.sharedInstance.featuretVideos == nil { return cell}
let newImage = UIImageView()
newImage.setImageWithUrl(URL.init(string: dataRefresher.sharedInstance.getVideosThumbnailUrls(fromVideos: dataRefresher.sharedInstance.featuretVideos)[indexPath.row])!, placeHolderImage: #imageLiteral(resourceName: "first"))
// newImage.setImageWithUrl(URL.init(string: urls[indexPath.row])!)
cell.imageView?.image = newImage.image
cell.textLabel?.text = dataRefresher.sharedInstance.getVideosTitels(fromVideos: dataRefresher.sharedInstance.featuretVideos)[indexPath.row]
print(indexPath.row)
print(newImage.image!)
return cell
}
The setImageWithUrl method is asynchronous, meaning that if the referenced image is not in the local cache yet, it will only be downloaded some time in the future.
However, your code tries to get the image out right away:
let newImage = UIImageView()
newImage.setImageWithUrl(..., placeHolderImage: ...)
cell.imageView?.image = newImage.image <-- synch call, image is nil here at first!
Fortunately, there's no need to reload the table view or do any other arcane tricks; just stop doing unnecessary things.
Remove the temporary UIImageView and load the image into the cell's UIImageView directly.
The above code becomes:
cell.imageView?.setImageWithUrl(..., placeHolderImage: ...)

Resources