UITableView Scrolls To Undesired Index or Position - ios

Basically the task is to load new API response as the user scrolls to down in UITableView without stopping user interaction and maintaining the current position (live feeds like Facebook and Instagram).
What I want to do is that parsing and then viewing Rest API's response in UITableView with custom cells.
The issue is that when the user scrolls to bottom the API call is made and table view scrolls to some other position where the user is.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return array.count;
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "xxxxx", for:indexPath) as! xxxxxxxx
return cell;
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if scrollView == tableView {
if ((scrollView.contentOffset.y + scrollView.frame.size.height) >= scrollView.contentSize.height) {
print("Scroll Ended")
fetchDataFromApi()
}
}
}
func fetchDataFromApi() {
let jsonUrlString = "xxxxxxx"
guard let url = URL(string: jsonUrlString) else {
return
}
print(url)
URLSession.shared.dataTask(with: url) {(data,response,err) in
guard let data = data else {
return
}
do {
let apiResponseData = try JSONDecoder().decode(ApiResponse.self, from: data)
if apiResponseData.httpStatusCode == HttpStatusCode.OK {
if let apiResponse = apiResponseData.data{
self.array.append(contentsOf: apiResponse)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
else {
print("API Response Error : \(apiResponseData.httpStatusCode) \(apiResponseData.errorMessage)")
}
}catch let jsonErr {
print("Serialization Error \(jsonErr)")
}
}.resume()
}}

Try with this
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "xxxxx", for:indexPath) as! xxxxxxxx
let lastRowIndex = tableView.numberOfRows(inSection: tableView.numberOfSections-1)
if (indexPath.row == lastRowIndex - 1) {
//print("last row selected")
fetchDataFromApi()
}
return cell;
}
If you are using this so no need to call this method
scrollViewDidEndDragging

try insertCellAtIndexPath replace for tableview.reloadData() in
if let apiResponse = apiResponseData.data{
self.array.append(contentsOf: apiResponse)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
......

There are several ways to implement this. The most favourable as far as I see it is to implement a paginated API and call the API once the UITableView's last item. You may use:
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let botEdge = scrollView.contentOffset.y + scrollView.frame.size.height;
if (botEdge >= scrollView.contentSize.height)
{
// Call API and reload TableView on completion handler.
}
}
Call the tableView reload on the completion handler of the API method.
Or you can do it in a slightly more awkward way:
public var indexPathsForVisibleRows: [NSIndexPath]? { get }
Get the array of index paths of the currently visible cells and scroll to the index after you reload the UITableView using:
tableView.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.None, animated: false)

It was because of dynamic cell sizes I was setting in table view

Related

Infinite scrolling UITableView with numbers

I need to create infinite table with numbers, that is, when scrolling, new cells with numbers should be created.
I create APICaller with counter, pagination, arrays and while loop.
Also I create UITableView with func scrollViewDidScroll(_ scrollView: UIScrollView) which append new values in a table.
My ViewController with UITableView
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIScrollViewDelegate {
private let apiCaller = APICaller()
private let tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.register(UITableViewCell.self,
forCellReuseIdentifier: "cell")
return tableView
}()
private var data = [Int]()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.dataSource = self
tableView.delegate = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = view.bounds
apiCaller.fetchData(pagination: false, completion: { [weak self] result in
switch result {
case.success(let data):
self?.data.append(contentsOf: data)
DispatchQueue.main.async {
self?.tableView.reloadData()
}
case.failure(_):
break
}
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = String(describing: data[indexPath.row])
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let position = scrollView.contentOffset.y
if position > (tableView.contentSize.height-100-scrollView.frame.size.height) {
guard !apiCaller.isPaginating else { return }
apiCaller.fetchData(pagination: true) { [weak self] result in
switch result {
case .success(let moreData):
self?.data.append(contentsOf: moreData)
DispatchQueue.main.async {
self?.tableView.reloadData()
}
case .failure(_):
break
}
}
}
}
}
In this case of APICaller I have only 100 cells, which corresponds to the loop constraint (but if i remove break from the while loop, nothing appears)
class APICaller {
private var counter = 0
var isPaginating = false
func fetchData(pagination: Bool = false, completion: #escaping (Result<[Int], Error>) -> Void) {
if pagination {
isPaginating = true
}
DispatchQueue.global().asyncAfter(deadline: .now() + (pagination ? 3 : 2), execute: {
var newData: [Int] = [0]
var originalData: [Int] = [0]
while true {
self.counter += 1
originalData.append(self.counter)
if self.counter == 100 {
break
}
}
completion(.success(pagination ? newData : originalData))
if pagination {
self.isPaginating = false
}
})
}
}
So, how can i get a table with infinite numbers?
The basic idea is right. When you scroll to a point where more rows are needed, fetch them. But doing this in the UIScrollViewDelegate is an expensive place to do that. I.e., that method is called for every pixel of movement and will result in many redundant calls.
Personally, I would advise moving this logic to the appropriate table view methods. For example, at a bare minimum, you might do it in the UITableViewDataSource method (i.e., if you are handling a row more than n rows from the end of your data set, fetch more data). E.g.,
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count + 1 // NB: one extra for the final “busy” cell
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (indexPath.row + 50) >= data.count { // scrolled within 50 rows of end
fetch()
}
if indexPath.row >= data.count { // if at last row, show spinner
return tableView.dequeueReusableCell(withIdentifier: "busy", for: indexPath)
}
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = String(describing: data[indexPath.row])
return cell
}
}
A few things to note there:
I'm reporting one more row than I have thus far. This is for my “busy” cell (a cell with a spinning UIActivityIndicatorView). That way, if the user ever scrolls faster than the network response can handle, we at least show the user a spinner to let them know that we are fetching more data.
Thus, the cellForRowAt checks the row, and shows the “busy cell” if necessary.
And in this case, when I'm within 50 items of the end, I will initiate a fetch of the next batch of data.
Even better than the above, I would also marry the UITableViewDataSource implementation with a UITableViewDataSourcePrefetching. Thus, set the tableView.prefetchDataSource and then implement prefetchRowsAt:
extension ViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
guard
let maxIndexPath = indexPaths.max(by: { $0.row < $1.row }), // get the last row, and
maxIndexPath.row >= data.count // see if it exceeds what we have already fetched
else { return }
fetch(pagination: true)
}
}
By the way, a few notes on fetch:
func fetch(pagination: Bool = false) {
apiCaller.fetchData(pagination: pagination) { [weak self] result in
guard
let self = self,
case .success(let values) = result
else { return }
let oldCount = self.data.count
self.data.append(contentsOf: values)
let indexPaths = (oldCount ..< self.data.count).map { IndexPath(row: $0, section: 0) }
self.tableView.insertRows(at: indexPaths, with: .automatic)
}
}
Note, I would advise not “reloading” the whole table view, but rather just “inserting” the appropriate rows. I've also moved the “am I busy” logic into the APICaller, where it belongs:
class APICaller {
private var counter = 0
private var isInProgress = false
func fetchData(pagination: Bool = false, completion: #escaping (Result<[Int], Error>) -> Void) {
guard !isInProgress else {
completion(.failure(APIError.busy))
return
}
isInProgress = true
DispatchQueue.main.asyncAfter(deadline: .now() + (pagination ? 3 : 2)) { [weak self] in
guard let self = self else { return }
let oldCounter = self.counter
self.counter += 100
self.isInProgress = false
let values = Array(oldCounter ..< self.counter)
completion(.success(values))
}
}
}
extension APICaller {
enum APIError: Error {
case busy
}
}
I not only simplified the APICaller, but also made it thread-safe (by moving all state mutation and callbacks on the the main queue). If you start some asynchronous task on a background queue, dispatch the updates and callback to the main queue. But do not mutate objects from background threads (or if you must, add some synchronization logic).

How to optimize loading from Firestore to Tableview

My View Controller has a Tableview with 2 segments. Depending on which Segment is selected, the Tableview displays a different set of data.
#IBAction func didChangeSegment(_ sender: UISegmentedControl) {
if sender.selectedSegmentIndex == 0 {
model.getRecipes(starredTrue: false)
}
else if sender.selectedSegmentIndex == 1 {
model.getRecipes(userAdded: true)
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return recipe.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MealPlanCell", for: indexPath) as! MealPlanCell
let recipeInTable = recipe[indexPath.row]
cell.displayRecipe(recipe: recipeInTable, indexPathRow: indexPath.row)
return cell
}
And this is how model.getRecipes() gets data from Firestore before returning it to the Tableview:
let recipeQuery = db.collection("recipes")
let docRef = recipeQuery.document(documentId)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.recipe = try document.data(as: Recipe.self)
let recipeFromFirestore = Recipe(
id: documentId,
title: self.recipe!.title ?? "")
self.recipes.append(recipeFromFirestore)
}
catch {
}
}
DispatchQueue.main.async {
self.delegateRecipes?.recipesRetrieved(recipes: self.recipes)
}
}
}
The issue I'm having is that the Tableview takes a very long time to display data. It appears this is because it has to wait for the model to finish loading all the data from Firestore every time I select one of the segments.
How can I optimize this process? Is it possible to have the TableView load/display cell by cell, instead of needing to wait for all data to be loaded?
Any guidance is much appreciated!

How to remove row in UITableViewController presented as popover

I have a custom UITableViewController that I present as a popover in my app. In some of the cells there is a delete button (trash can) to remove that item. Everything works as it should except that I the UI is not update when pressing the delete button. That is, the data is cleared and I call self.tableView.reloadData(), but the cell remains visible in the UI. (Pressing the delete button again makes the app crash in my C++ code because of an assert). I have no storyboard or xib as I do not need it. I only want this to be in code.
What am I missing? It might be something simple, but I cannot fathom why. I have tried:
Separate data source implementation.
Calling reloadData() both sync and async.
Setting delegate to self.
Various other hacks.
Here is the UITableViewController implementation:
import Foundation
class IngredientInfoPopoverViewController : UITableViewController
{
var slViewController: ShoppingListViewController?;
var ingredientName: String = "Ingrediens";
#IBOutlet var uniqueIngredients: [Ingredient] = []; // Unique per *recipe* so that we can list all the recipes for the ingredients
var clickedCellIndexPath: IndexPath? = nil;
enum SECTIONS : Int
{
case HEADER = 0;
case RECIPE = 1;
}
static let ROW_HEIGHT = 44;
override func viewDidLoad()
{
super.viewDidLoad();
tableView.register(UINib(nibName: "OpenIngredientInfoCell", bundle: nil), forCellReuseIdentifier: "OpenIngredientInfoCell");
tableView.register(UINib(nibName: "OpenRecipeCell", bundle: nil), forCellReuseIdentifier: "OpenRecipeCell");
tableView.separatorStyle = .singleLine;
tableView.bounces = false; // "Static" table view
updateSize();
}
func updateSize()
{
let totalCount = min(uniqueIngredients.count + 1, 6); // + 1: header row. min: Allow max 5 recipes in list (enables scrolling)
self.preferredContentSize = CGSize(width: 300, height: totalCount * IngredientInfoPopoverViewController.ROW_HEIGHT);
}
func setup(slvc: ShoppingListViewController?, ingredients: [Ingredient], clickedCellIndexPath: IndexPath)
{
self.slViewController = slvc;
self.clickedCellIndexPath = clickedCellIndexPath;
if (ingredients.count > 0)
{
let first = ingredients[0];
for i in ingredients
{
assert(i.getId() == first.getId());
}
ingredientName = first.getName();
var uniqueRecipeNames: Set<String> = [];
for i in ingredients
{
uniqueRecipeNames.insert(i.getRecipeName());
}
let sorted = uniqueRecipeNames.sorted();
uniqueIngredients.removeAll();
for s in sorted
{
for i in ingredients
{
if (i.getRecipeName() == s)
{
uniqueIngredients.append(i);
break;
}
}
}
}
}
override func numberOfSections(in tableView: UITableView) -> Int
{
return 2;
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
switch section
{
case SECTIONS.HEADER.rawValue:
return 1;
case SECTIONS.RECIPE.rawValue:
return uniqueIngredients.count;
default:
assert(false);
return 0;
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
switch (indexPath.section)
{
case SECTIONS.HEADER.rawValue:
assert(indexPath.row == 0);
if (uniqueIngredients.count > 0)
{
let ingredient = uniqueIngredients[0]; // All are the same ingredient
self.dismiss(animated: true, completion: nil);
slViewController?.onIngredientInfoButtonClicked(ingredient);
}
break;
case SECTIONS.RECIPE.rawValue:
if (indexPath.row < uniqueIngredients.count)
{
let ingredient = uniqueIngredients[indexPath.row];
self.dismiss(animated: true, completion: nil);
slViewController?.onRecipeInfoButtonClicked(ingredient);
}
break;
default:
break;
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = UITableViewCell();
switch (indexPath.section)
{
case SECTIONS.HEADER.rawValue:
if (uniqueIngredients.count > 0)
{
let ingredient = uniqueIngredients[0];
let cell = tableView.dequeueReusableCell(withIdentifier: "OpenIngredientInfoCell", for: indexPath) as! OpenIngredientInfoCell;
cell.setup(ingredient);
}
break;
case SECTIONS.RECIPE.rawValue:
if (indexPath.row < uniqueIngredients.count)
{
cell.selectionStyle = .none; // Without this the cell contents become gray and disappear when long pressing! FML
let ingredient = uniqueIngredients[indexPath.row];
let cell = tableView.dequeueReusableCell(withIdentifier: "OpenRecipeCell", for: indexPath) as! OpenRecipeCell;
cell.setup(self, ingredient, clickedCellIndexPath);
}
break;
default:
break;
}
return cell;
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
return CGFloat(IngredientInfoPopoverViewController.ROW_HEIGHT);
}
func ingredientRemoved(_ ingredient: Ingredient)
{
for i in 0..<uniqueIngredients.count
{
if (uniqueIngredients[i].getRecipeId() == ingredient.getRecipeId())
{
uniqueIngredients.remove(at: i);
// let indexPath = IndexPath(row: i, section: SECTIONS.RECIPE.rawValue);
// self.tableView.deleteRows(at: [indexPath], with: .fade);
DispatchQueue.main.async {
self.tableView.reloadData();
}
break;
}
}
if (uniqueIngredients.count == 0)
{
self.dismiss(animated: true, completion: nil);
}
else
{
DispatchQueue.main.async {
self.tableView.reloadData();
}
}
}
}
Here is how I present the IngredientInfoPopoverViewController:
#objc func ingredientInfoClicked(_ sender: UITapGestureRecognizer)
{
let tapLocation = sender.location(in: self.tableView)
let indexPath = self.tableView.indexPathForRow(at: tapLocation)!
let ingredients = CppInterface.shoppingList.getIngredients(UInt(indexPath.section), position: UInt(indexPath.row));
let controller = IngredientInfoPopoverViewController();
controller.setup(slvc: self, ingredients: ingredients!, clickedCellIndexPath: indexPath);
controller.modalPresentationStyle = .popover;
controller.popoverPresentationController!.delegate = self;
self.present(controller, animated: true, completion: {
self.tableView.reloadData();
});
}
Here is how the view controller looks when presented. If I click the trash can on one of the items, the data is cleared, but the cell is not removed from the UI, which is what I am trying to achieve.
I'm actually surprised your tableView shows any data at all. Because you declare cell as a let in cellForRowAt when you do let cell = UITableViewCell();, that makes it immutable, and the first cell (outside of the switch) is the one that should technically get returned. Hence why no data should be displaying. And probably also the reason why your tableView is not updating correctly.
Anyway, you should only declare cell when you're dequeueing it, and you should as much as possible, avoid force-unwrapping of a variable.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == SECTIONS.HEADER.rawValue, let cell = tableView.dequeueReusableCell(withIdentifier: "OpenIngredientInfoCell", for: indexPath) as? OpenIngredientInfoCell {
// not sure this check is necessary, but I'm adding it because it was in your original code
guard uniqueIngredients.count > 0 else { return UITableViewCell() }
let ingredient = uniqueIngredients[0]
cell.setup(ingredient)
return cell
} else if indexPath.section == SECTIONS.RECIPE.rawValue, let cell = tableView.dequeueReusableCell(withIdentifier: "OpenRecipeCell", for: indexPath) as? OpenRecipeCell {
// it shouldn't be possible for the indexPath to ever be greater than the dataSource items count, but I'll keep the check
guard indexPath.row < uniqueIngredients.count else { return UITableViewCell() }
cell.selectionStyle = .none
let ingredient = uniqueIngredients[indexPath.row]
cell.setup(self, ingredient, clickedCellIndexPath)
return cell
}
return UITableViewCell()
}
I've removed the semi-colons as they're not necessary in Swift.
For specifying the table cells' reuse identifiers, using the class names would probably be better. So you would use "\(OpenRecipeCell.self)" instead of "OpenRecipeCell"
If you are using the defaulted way of editing a UITableView (either swiping or entering edit mode), then here's my delegate code that works fine:
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let movedStep = appState.recipe.steps[sourceIndexPath.row]
appState.recipe.steps.remove(at: sourceIndexPath.row)
appState.recipe.steps.insert(movedStep, at: destinationIndexPath.row)
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
appState.recipe.steps.remove(at: indexPath.row)
tblSteps.deleteRows(at: [indexPath], with: .automatic)
}
}
Notes:
I manually place this table view in edit mode through a UIBarButtonItem, and a cell can both be moved or deleted.
My data source is in my model, at appState.recipe.steps. The structure doesn't matter, just handling the array.
I set a Notification anytime this array is changed that triggers a reloadData() in this table view.
I don't see either of these delegate methods listed, so I'm posting this answer. If by chance it doesn't help you, I'll gladly delete this.

Is this the proper way to use heightForRowAt?

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)
}
}
}
}

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()
}
}
}

Resources