UITableView load more when scrolling to bottom like Facebook application - ios

I am developing an application that uses SQLite. I want to show a list of users (UITableView) using a paginating mechanism. Could any one please tell me how to load more data in my list when the user scrolls to the end of the list (like on home page on Facebook application)?

You can do that by adding a check on where you're at in the cellForRowAtIndexPath: method. This method is easy to understand and to implement :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Classic start method
static NSString *cellIdentifier = #"MyCell";
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell)
{
cell = [[MyCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MainMenuCellIdentifier];
}
MyData *data = [self.dataArray objectAtIndex:indexPath.row];
// Do your cell customisation
// cell.titleLabel.text = data.title;
BOOL lastItemReached = [data isEqual:[[self.dataArray] lastObject]];
if (!lastItemReached && indexPath.row == [self.dataArray count] - 1)
{
[self launchReload];
}
}
EDIT : added a check on last item to prevent recursion calls. You'll have to implement the method defining whether the last item has been reached or not.
EDIT2 : explained lastItemReached

Swift
Method 1: Did scroll to bottom
Here is the Swift version of Pedro Romão's answer. When the user stops scrolling it checks if it has reached the bottom.
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
// UITableView only moves in one direction, y axis
let currentOffset = scrollView.contentOffset.y
let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height
// Change 10.0 to adjust the distance from bottom
if maximumOffset - currentOffset <= 10.0 {
self.loadMore()
}
}
Method 2: Reached last row
And here is the Swift version of shinyuX's answer. It checks if the user has reached the last row.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// set up cell
// ...
// Check if the last row number is the same as the last current data element
if indexPath.row == self.dataArray.count - 1 {
self.loadMore()
}
}
Example of a loadMore() method
I set up these three class variables for fetching batches of data.
// number of items to be fetched each time (i.e., database LIMIT)
let itemsPerBatch = 50
// Where to start fetching items (database OFFSET)
var offset = 0
// a flag for when all database items have already been loaded
var reachedEndOfItems = false
This is the function to load more items from the database into the table view.
func loadMore() {
// don't bother doing another db query if already have everything
guard !self.reachedEndOfItems else {
return
}
// query the db on a background thread
DispatchQueue.global(qos: .background).async {
// determine the range of data items to fetch
var thisBatchOfItems: [MyObjects]?
let start = self.offset
let end = self.offset + self.itemsPerBatch
// query the database
do {
// SQLite.swift wrapper
thisBatchOfItems = try MyDataHelper.findRange(start..<end)
} catch _ {
print("query failed")
}
// update UITableView with new batch of items on main thread after query finishes
DispatchQueue.main.async {
if let newItems = thisBatchOfItems {
// append the new items to the data source for the table view
self.myObjectArray.appendContentsOf(newItems)
// reload the table view
self.tableView.reloadData()
// check if this was the last of the data
if newItems.count < self.itemsPerBatch {
self.reachedEndOfItems = true
print("reached end of data. Batch count: \(newItems.count)")
}
// reset the offset for the next data query
self.offset += self.itemsPerBatch
}
}
}
}

Better to use willDisplayCell method to check if which cell will be loaded.
Once we get the current indexPath.row is last we can load more cells.
This will load more cells on scrolling down.
- (void)tableView:(UITableView *)tableView
willDisplayCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath
{
// check if indexPath.row is last row
// Perform operation to load new Cell's.
}

Details
Swift 5.1, Xcode 11.2.1
Solution
Worked with UIScrollView / UICollectionView / UITableView
import UIKit
class LoadMoreActivityIndicator {
private let spacingFromLastCell: CGFloat
private let spacingFromLastCellWhenLoadMoreActionStart: CGFloat
private weak var activityIndicatorView: UIActivityIndicatorView?
private weak var scrollView: UIScrollView?
private var defaultY: CGFloat {
guard let height = scrollView?.contentSize.height else { return 0.0 }
return height + spacingFromLastCell
}
deinit { activityIndicatorView?.removeFromSuperview() }
init (scrollView: UIScrollView, spacingFromLastCell: CGFloat, spacingFromLastCellWhenLoadMoreActionStart: CGFloat) {
self.scrollView = scrollView
self.spacingFromLastCell = spacingFromLastCell
self.spacingFromLastCellWhenLoadMoreActionStart = spacingFromLastCellWhenLoadMoreActionStart
let size:CGFloat = 40
let frame = CGRect(x: (scrollView.frame.width-size)/2, y: scrollView.contentSize.height + spacingFromLastCell, width: size, height: size)
let activityIndicatorView = UIActivityIndicatorView(frame: frame)
if #available(iOS 13.0, *)
{
activityIndicatorView.color = .label
}
else
{
activityIndicatorView.color = .black
}
activityIndicatorView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
activityIndicatorView.hidesWhenStopped = true
scrollView.addSubview(activityIndicatorView)
self.activityIndicatorView = activityIndicatorView
}
private var isHidden: Bool {
guard let scrollView = scrollView else { return true }
return scrollView.contentSize.height < scrollView.frame.size.height
}
func start(closure: (() -> Void)?) {
guard let scrollView = scrollView, let activityIndicatorView = activityIndicatorView else { return }
let offsetY = scrollView.contentOffset.y
activityIndicatorView.isHidden = isHidden
if !isHidden && offsetY >= 0 {
let contentDelta = scrollView.contentSize.height - scrollView.frame.size.height
let offsetDelta = offsetY - contentDelta
let newY = defaultY-offsetDelta
if newY < scrollView.frame.height {
activityIndicatorView.frame.origin.y = newY
} else {
if activityIndicatorView.frame.origin.y != defaultY {
activityIndicatorView.frame.origin.y = defaultY
}
}
if !activityIndicatorView.isAnimating {
if offsetY > contentDelta && offsetDelta >= spacingFromLastCellWhenLoadMoreActionStart && !activityIndicatorView.isAnimating {
activityIndicatorView.startAnimating()
closure?()
}
}
if scrollView.isDecelerating {
if activityIndicatorView.isAnimating && scrollView.contentInset.bottom == 0 {
UIView.animate(withDuration: 0.3) { [weak self] in
if let bottom = self?.spacingFromLastCellWhenLoadMoreActionStart {
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottom, right: 0)
}
}
}
}
}
}
func stop(completion: (() -> Void)? = nil) {
guard let scrollView = scrollView , let activityIndicatorView = activityIndicatorView else { return }
let contentDelta = scrollView.contentSize.height - scrollView.frame.size.height
let offsetDelta = scrollView.contentOffset.y - contentDelta
if offsetDelta >= 0 {
UIView.animate(withDuration: 0.3, animations: {
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}) { _ in completion?() }
} else {
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
completion?()
}
activityIndicatorView.stopAnimating()
}
}
Usage
init
activityIndicator = LoadMoreActivityIndicator(scrollView: tableView, spacingFromLastCell: 10, spacingFromLastCellWhenLoadMoreActionStart: 60)
handling
extension ViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
activityIndicator.start {
DispatchQueue.global(qos: .utility).async {
sleep(3)
DispatchQueue.main.async { [weak self] in
self?.activityIndicator.stop()
}
}
}
}
}
Full Sample
Do not forget to paste the solution code.
import UIKit
class ViewController: UIViewController {
fileprivate var activityIndicator: LoadMoreActivityIndicator!
override func viewDidLoad() {
super.viewDidLoad()
let tableView = UITableView(frame: view.frame)
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
tableView.dataSource = self
tableView.delegate = self
tableView.tableFooterView = UIView()
activityIndicator = LoadMoreActivityIndicator(scrollView: tableView, spacingFromLastCell: 10, spacingFromLastCellWhenLoadMoreActionStart: 60)
}
}
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "\(indexPath)"
return cell
}
}
extension ViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
activityIndicator.start {
DispatchQueue.global(qos: .utility).async {
for i in 0..<3 {
print("!!!!!!!!! \(i)")
sleep(1)
}
DispatchQueue.main.async { [weak self] in
self?.activityIndicator.stop()
}
}
}
}
}
Result

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger lastSectionIndex = [tableView numberOfSections] - 1;
NSInteger lastRowIndex = [tableView numberOfRowsInSection:lastSectionIndex] - 1;
if ((indexPath.section == lastSectionIndex) && (indexPath.row == lastRowIndex)) {
// This is the last cell
[self loadMore];
}
}
If you are using Core Data and NSFetchedResultsController, then loadMore could look like the following:
// Load more
- (void)loadMore {
[self.fetchedResultsController.fetchRequest setFetchLimit:newFetchLimit];
[NSFetchedResultsController deleteCacheWithName:#"cache name"];
NSError *error;
if (![self.fetchedResultsController performFetch:&error]) {
// Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
[self.tableView reloadData];
}

Details
Swift 5.1, Xcode 11.3.1
Solution
Generic UITableView Extension For Loadmore.
add this UITableView + Extension in your new file
extension UITableView {
func indicatorView() -> UIActivityIndicatorView{
var activityIndicatorView = UIActivityIndicatorView()
if self.tableFooterView == nil {
let indicatorFrame = CGRect(x: 0, y: 0, width: self.bounds.width, height: 80)
activityIndicatorView = UIActivityIndicatorView(frame: indicatorFrame)
activityIndicatorView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
if #available(iOS 13.0, *) {
activityIndicatorView.style = .large
} else {
// Fallback on earlier versions
activityIndicatorView.style = .whiteLarge
}
activityIndicatorView.color = .systemPink
activityIndicatorView.hidesWhenStopped = true
self.tableFooterView = activityIndicatorView
return activityIndicatorView
}
else {
return activityIndicatorView
}
}
func addLoading(_ indexPath:IndexPath, closure: #escaping (() -> Void)){
indicatorView().startAnimating()
if let lastVisibleIndexPath = self.indexPathsForVisibleRows?.last {
if indexPath == lastVisibleIndexPath && indexPath.row == self.numberOfRows(inSection: 0) - 1 {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
closure()
}
}
}
}
func stopLoading() {
if self.tableFooterView != nil {
self.indicatorView().stopAnimating()
self.tableFooterView = nil
}
else {
self.tableFooterView = nil
}
}
}
Now, just add following line of code in UITableViewDelegate Method willDisplay Cell in your ViewController and make sure tableView.delegate = self
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// need to pass your indexpath then it showing your indicator at bottom
tableView.addLoading(indexPath) {
// add your code here
// append Your array and reload your tableview
tableView.stopLoading() // stop your indicator
}
}
Result
That's it.. Hope this helpful. Thank You

I have implemented one solution that i found in stackoverflow, and it works fine, but i think the shinyuX's solution it's very easy to implement and works fine for my propose.
If someone wants a different solution can use this one below.
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
// UITableView only moves in one direction, y axis
CGFloat currentOffset = scrollView.contentOffset.y;
CGFloat maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height;
//NSInteger result = maximumOffset - currentOffset;
// Change 10.0 to adjust the distance from bottom
if (maximumOffset - currentOffset <= 10.0) {
[self loadOneMorePage];
//[self methodThatAddsDataAndReloadsTableView];
}
}

Use limit and offset in your queries and fill your tableview with that content. When the user scrolls down, load the next offset.
Implement the tableView:willDisplayCell:forRowAtIndexPath: method in your UITableViewDelegate and check to see if it's the last row

Below link will provide sample code. #Swift3
User need to pull up last table view cell, at least hight of 2 cell to fetch more data from server.
You will found Process cell also to show loading process as in last cell.
Its in Swift3
https://github.com/yogendrabagoriya/YBTableViewPullData

One more option to use (Swift 3 and iOS 10+):
class DocumentEventsTableViewController: UITableViewController, UITableViewDataSourcePrefetching {
var currentPage: Int = 1
let pageSize: Int = 10 // num of items in one page
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.prefetchDataSource = self
}
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
let upcomingRows = indexPaths.map { $0.row }
if let maxIndex = upcomingRows.max() {
let nextPage: Int = Int(ceil(Double(maxIndex) / Double(pageSize))) + 1
if nextPage > currentPage {
// Your function, which attempts to load respective page from the local database
loadLocalData(page: nextPage)
// Your function, which makes a network request to fetch the respective page of data from the network
startLoadingDataFromNetwork(page: nextPage)
currentPage = nextPage
}
}
}
}
For rather small pages (~ 10 items) you might want to manually add data for pages 1 and 2 because nextPage might be somewhere about 1-2 until the table has a few items to be scrolled well. But it will work great for all next pages.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (news.count == 0) {
return 0;
} else {
return news.count + 1 ;
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
#try {
uint position = (uint) (indexPath.row);
NSUInteger row = [indexPath row];
NSUInteger count = [news count];
//show Load More
if (row == count) {
UITableViewCell *cell = nil;
static NSString *LoadMoreId = #"LoadMore";
cell = [tableView dequeueReusableCellWithIdentifier:LoadMoreId];
if (cell == nil) {
cell = [[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:LoadMoreId];
}
if (!hasMoreLoad) {
cell.hidden = true;
} else {
cell.textLabel.text = #"Load more items...";
cell.textLabel.textColor = [UIColor blueColor];
cell.textLabel.font = [UIFont boldSystemFontOfSize:14];
NSLog(#"Load more");
if (!isMoreLoaded) {
isMoreLoaded = true;
[self performSelector:#selector(loadMoreNews) withObject:nil afterDelay:0.1];
}
}
return cell;
} else {
NewsRow *cell = nil;
NewsObject *newsObject = news[position];
static NSString *CellIdentifier = #"NewsRow";
cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
// Load the top-level objects from the custom cell XIB.
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:CellIdentifier owner:self options:nil];
// Grab a pointer to the first object (presumably the custom cell, as that's all the XIB should contain).
cell = topLevelObjects[0];
// Configure the cell...
}
cell.title.text = newsObject.title;
return cell;
}
}
#catch (NSException *exception) {
NSLog(#"Exception occurred: %#, %#", exception, [exception userInfo]);
}
return nil;
}
very good explanation on this post.
http://useyourloaf.com/blog/2010/10/02/dynamically-loading-new-rows-into-a-table.html
simple you have to add last row and hide it and when table row hit last row than show the row and load more items.

you should check ios UITableViewDataSourcePrefetching.
class ViewController: UIViewController {
#IBOutlet weak var mytableview: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
mytableview.prefetchDataSource = self
}
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
print("prefetchdRowsAtIndexpath \(indexPaths)")
}
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
print("cancelPrefetchingForRowsAtIndexpath \(indexPaths)")
}
}
https://developer.apple.com/documentation/uikit/uitableviewdatasourceprefetching
https://andreygordeev.com/2017/02/20/uitableview-prefetching/

For Xcode 10.1, Swift 4.2
This video seems like a great tutorial!
Starter/Complete project: https://github.com/RobCanton/Swift-Infinite-Scrolling-Example
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var tableView:UITableView!
var fetchingMore = false
var items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
initTableView()
}
func initTableView() {
tableView = UITableView(frame: view.bounds, style: .plain)
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "tableCell")
tableView.delegate = self
tableView.dataSource = self
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
let layoutGuide = view.safeAreaLayoutGuide
tableView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor).isActive = true
tableView.topAnchor.constraint(equalTo: layoutGuide.topAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor).isActive = true
tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath)
cell.textLabel?.text = "Item \(items[indexPath.row])"
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
if offsetY > contentHeight - scrollView.frame.height * 4 {
if !fetchingMore {
beginBatchFetch()
}
}
}
func beginBatchFetch() {
fetchingMore = true
print("Call API here..")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.50, execute: {
print("Consider this as API response.")
let newItems = (self.items.count...self.items.count + 12).map { index in index }
self.items.append(contentsOf: newItems)
self.fetchingMore = false
self.tableView.reloadData()
})
}
}

for loading from an API, It works for me, Xcode10 , swift 4.2 :
1- create New Swift file and do like this:
//
// apiTVCController.swift
// ApiTestingTableView
//
// Created by Hooma7n on 4/7/19.
// Copyright © 2019 Hooma7n. All rights reserved.
//
import Foundation
import Alamofire
class apiget {
var tableData : [Datum] = []
var loadin : [Datum] = []
var testfortotal : Int?
func getfromapi(completionHandler : ((_ isSucess : Bool) -> Void)?) {
let url = "https://reqres.in/api/users?page=1"
Alamofire.request(url, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: nil)
.responseJSON(completionHandler : { response in
switch response.result {
case .success(let data):
guard let jsonData = try? JSONSerialization.data(withJSONObject: data, options: JSONSerialization.WritingOptions.prettyPrinted) else {return}
let decoder = JSONDecoder()
guard let result = try? decoder.decode(Welcome.self, from: jsonData) else {return}
self.tableData = result.data ?? []
self.testfortotal = result.total ?? 0
completionHandler?(true)
// print(result)
case .failure(let error):
print(error)
}
})
}
var pagecounter : Int = 2
func loadmore(completionHandler : ((_ isSucess : Bool) -> Void)?){
let url = "https://reqres.in/api/users?page=\(pagecounter)"
Alamofire.request(url, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: nil)
.responseJSON(completionHandler : { response in
switch response.result {
case .success(let data):
guard let jsonData = try? JSONSerialization.data(withJSONObject: data, options: JSONSerialization.WritingOptions.prettyPrinted) else {return}
let decoder = JSONDecoder()
guard let myresult = try? decoder.decode(Welcome.self, from: jsonData) else {return}
self.loadin = myresult.data ?? []
self.tableData.append(contentsOf: myresult.data ?? [])
completionHandler?(true)
print(self.pagecounter)
self.pagecounter += 1
// print(myresult)
case .failure(let error):
print(error)
}
})
}
}
extension apiget {
struct Welcome: Codable {
let page, perPage, total, totalPages: Int?
var data: [Datum]?
enum CodingKeys: String, CodingKey {
case page
case perPage = "per_page"
case total
case totalPages = "total_pages"
case data
}
}
struct Datum: Codable {
let id: Int?
let firstName, lastName: String?
let avatar: String?
enum CodingKeys: String, CodingKey {
case id
case firstName = "first_name"
case lastName = "last_name"
case avatar
}
}
}
2- in Your ViewController file (tableView Controller) :
//
// apiTVC.swift
// ApiTestingTableView
//
// Created by Hooma7n on 4/7/19.
// Copyright © 2019 Hooma7n. All rights reserved.
//
import UIKit
import Alamofire
class apiTVC: UITableViewController {
var datamodel = apiget()
override func viewDidLoad() {
super.viewDidLoad()
datamodel.getfromapi(completionHandler: {finish in
if finish {self.tableView.reloadData()
}
})
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return datamodel.tableData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! apiTableViewCell
cell.firstNameLabel.text = datamodel.tableData[indexPath.row].firstName
cell.lastNameLabel.text = datamodel.tableData[indexPath.row].lastName
cell.dateLabel.text = "\(datamodel.tableData[indexPath.row].id ?? 0)"
cell.profileImageView.loadImage(fromURL: datamodel.tableData[indexPath.row].avatar ?? "")
return cell
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastElement = datamodel.tableData.count - 1
let total = datamodel.testfortotal ?? 12
if indexPath.row == lastElement && datamodel.tableData.count < total{
datamodel.loadmore(completionHandler: {finish in
if finish {
self.tableView.reloadData()
}})
}
}
}
if using tableView in Your viewController set delegate,datasource self in viewDidLoad.

Just wanna share this approach:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
NSLog(#"%#", [[YourTableView indexPathsForVisibleRows] lastObject]);
[self estimatedTotalData];
}
- (void)estimatedTotalData
{
long currentRow = ((NSIndexPath *)[[YourTableView indexPathsForVisibleRows] lastObject]).row;
long estimateDataCount = 25;
while (currentRow > estimateDataCount)
{
estimateDataCount+=25;
}
dataLimit = estimateDataCount;
if (dataLimit == currentRow+1)
{
dataLimit+=25;
}
NSLog(#"dataLimit :%ld", dataLimit);
[self requestForData];
// this answers the question..
//
if(YourDataSource.count-1 == currentRow)
{
NSLog(#"LAST ROW"); //loadMore data
}
}
NSLog(...); output would be something like:
<NSIndexPath: 0xc0000000002e0016> {length = 2, path = 0 - 92}
dataLimit :100
<NSIndexPath: 0xc000000000298016> {length = 2, path = 0 - 83}
dataLimit :100
<NSIndexPath: 0xc000000000278016> {length = 2, path = 0 - 79}
dataLimit :100
<NSIndexPath: 0xc000000000238016> {length = 2, path = 0 - 71}
dataLimit :75
<NSIndexPath: 0xc0000000001d8016> {length = 2, path = 0 - 59}
dataLimit :75
<NSIndexPath: 0xc0000000001c0016> {length = 2, path = 0 - 56}
dataLimit :75
<NSIndexPath: 0xc000000000138016> {length = 2, path = 0 - 39}
dataLimit :50
<NSIndexPath: 0xc000000000120016> {length = 2, path = 0 - 36}
dataLimit :50
<NSIndexPath: 0xc000000000008016> {length = 2, path = 0 - 1}
dataLimit :25
<NSIndexPath: 0xc000000000008016> {length = 2, path = 0 - 1}
dataLimit :25
This is good for displaying data stored locally.
Initially I declare the dataLimit to 25, that means uitableview will have 0-24 (initially).
If the user scrolled to the bottom and the last cell is visible dataLimit will be added with 25...
Note: This is more like a UITableView data paging, :)

-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger sectionsAmount = [tableView numberOfSections];
NSInteger rowsAmount = [tableView numberOfRowsInSection:[indexPath section]];
if ([indexPath section] == sectionsAmount - 1 && [indexPath row] == rowsAmount - 1) {
//get last row
if (!isSearchActive && !isFilterSearchActive) {
if (totalRecords % 8 == 0) {
int64_t delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
[yourTableView beginUpdates];
[yourTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
[yourTableView endUpdates];
});
}
}
}
}

The best way to solve this problem is to add cell at the bottom of your table, and this cell will hold indicator.
In swift you need to add this:
Create new cell of type cellLoading this will hold the indicator. Look at the code below
Look at the num of rows and add 1 to it (This is for loading cell).
you need to check in the rawAtIndex if idexPath.row == yourArray.count then return Loading cell.
look at code below:
import UIKit
class LoadingCell: UITableViewCell {
#IBOutlet weak var indicator: UIActivityIndicatorView!
}
For table view :
numOfRows:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return yourArray.count + 1
}
cellForRawAt indexPath:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == users.count {
// need to change
let loading = Bundle.main.loadNibNamed("LoadingCell", owner: LoadingCell.self , options: nil)?.first as! LoadingCell
return loading
}
let yourCell = tableView.dequeueReusableCell(withIdentifier: "cellCustomizing", for: indexPath) as! UITableViewCell
return yourCell
}
If you notice that my loading cell is created from a nib file. This videos will explain what I did.

let threshold = 100.0 // threshold from bottom of tableView
var isLoadingMore = false // flag
func scrollViewDidScroll(scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.y
let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height;
if !isLoadingMore && (maximumOffset - contentOffset <= threshold) {
// Get more data - API call
self.isLoadingMore = true
// Update UI
dispatch_async(dispatch_get_main_queue()) {
tableView.reloadData()
self.isLoadingMore = false
}
}
}

it is sample code.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:ShowComplainCell = tableView.dequeueReusableCell(withIdentifier: "cell")! as! ShowComplainCell
let item = self.dataArray[indexPath.row] as! ComplainListItem;
let indexPathArray = NSArray(array: tableView.indexPathsForVisibleRows!)
let vIndexPath = indexPathArray.lastObject as! NSIndexPath
let lastItemReached = item.isEqual(self.dataArray.lastObject);
if (lastItemReached && vIndexPath.row == (self.dataArray.count - 1))
{
self.loadData()
}
return cell
}
indexPathArray: is visible rows.
vIndexPath:is visible last indexpath
load data
func loadData(){
if(isReloadTable){
let HUD = MBProgressHUD.showAdded(to: self.view, animated: true)
let manager :AFHTTPSessionManager = AFHTTPSessionManager()
var param = NSDictionary()
param = [
"category":cat_id,
"smart_user_id": USERDEF.value(forKey: "user_id") as! String,
"page":page,
"phone":phone! as String
] as [String : Any] as NSDictionary
print("param1 = \(param)")
manager.get("lists.php?", parameters: param, progress: nil, success: { (task:URLSessionDataTask, responseObject: Any) in
let adsArray = dic["results"] as! NSArray;
for item in adsArray {
let item = ComplainListItem(dictionary: item as! NSDictionary )
self.dataArray.add(item)
}
self.view.addSubview(self.cityTableView)
self.cityTableView.reloadData()
if(adsArray.count==10){
self.cityTableView.reloadData()
self.isReloadTable = true
self.page+=1
}else if(adsArray.count<10){
self.cityTableView.reloadData()
self.isReloadTable = false
}
HUD.hide(animated:true)
}) { (operation,error) -> Void in
print("error = \(error)")
HUD.hide(animated:true)
}
}
}
check your dataArray count which is myadsarray check to equal your data limit. then if dataArray count equal next page is called if not equal which is less then 10, all data is showed or finished.

Related

Gif when used as pagination loader, sometimes not displayed in image view

I am using collection view with pagination to show more data. At end of collection view I show an image view with gif file until data loads and then hide the image view. But sometimes gif doesn't load in the image view.
Please help and thanks in advance !!
I have tried changing libraries - Kingfisher, SDWebImage, ImageIO, UIImage+Gif etc. but it didn't help.
Also tried running on main thread.
class HorizontalScrollDealCollectionView: UICollectionView {
private var indicator:UIImageView!
private var offset:CGFloat = 0
private let loaderGif = UIImage.gif(name: "831")
override func awakeFromNib() {
super.awakeFromNib()
let frame = CGRect(x: 20, y: self.frame.height/2-15, width: 30, height: 30)
indicator = UIImageView(frame: frame)
indicator.image = loaderGif
indicator.backgroundColor = .blue
self.addSubview(indicator)
indicator.isHidden = true
}
override func layoutSubviews() {
super.layoutSubviews()
self.adjustIndicator()
}
func loadGifAnimatedIndicator() {
indicator.isHidden = true
indicator.image = loaderGif
}
private func adjustIndicator() {
indicator.frame.origin.x = self.contentSize.width + (offset/2)
let view = self.visibleCells.first
if let scrollViewAdjuster = view as? InfiniteScrollOffsetAdjuster {
indicator.frame.origin.y = scrollViewAdjuster.refralFrameForProgressView.height / 2 - 15
}
else {
indicator.frame.origin.y = self.contentSize.height / 2 - 15;
}
}
func showIndicator() {
if indicator.isHidden == false { return }
indicator.isHidden = false
indicator.image = loaderGif
indicator.startAnimating()
UIView.animate(withDuration: 0.2) {
self.contentInset.right = self.offset + 40
}
}
func hideIndicator() {
if indicator.isHidden { return }
self.indicator.isHidden = true
}
func resetContentInset(animate:Bool = true) {
UIView.setAnimationsEnabled(animate)
UIView.animate(withDuration: 0.1, animations: {
self.contentInset.right = 0
}) { (success) in
if !animate {
UIView.setAnimationsEnabled(true)
}
}
}
}
In ViewController
//Table View Delegate
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard let homeTableViewCell = cell as? HorizontalScrollHomeTableViewCell else {
return
}
homeTableViewCell.collectionView.loadGifAnimatedIndicator()
homeTableViewCell.collectionView.reloadData()
}
//Collection View Delegate
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let horizontalCollView = collectionView as! HorizontalScrollDealCollectionView
let dataIndex = horizontalCollView.tableViewCellIndex!
let data = items[dataIndex.row]
if (indexPath.item == data.products.count - 1) && data.moreAvailable {
horizontalCollView.showIndicator()
data.loadMore(completion: {
(success) in
horizontalCollView.hideIndicator()
if success {horizontalCollView.reloadData()}
})
}
}
Screenshot 1
****Screenshot 2**
Changing
private let loaderGif = UIImage.gif(name: "831")
to
private var loaderGif: UIImage {
return UIImage.gif(name: "831")
}
solves the issue but tableview scrolling becomes laggy/jittering.
Also did this asynchronously using DispatchQueue but didn't helped. Still scrolling hangs.
func loadGifAnimatedIndicator() {
indicator.isHidden = true
DispatchQueue.main.async {
self.indicator.image = self.loaderGif
}
}

Pagination code being triggered upon launch instead of when user reaches bottom of page

I'm trying to implement an infinite scroll/pagination feature to a table view. I use scrollViewDidScroll to measure when the user reaches the bottom of the page, which then triggers a function to fetch the next batch of data. However I think the measurements are off because my fetchMoreEvents function is being triggered upon the launch of the app.
This is the pagination code (scrollViewDidScroll and fetchMoreEvents):
func fetchMoreEvents() {
fetchingMore = true
var page = 1
page += 1
let seatGeekApiUrl = URL(string: "https://api.seatgeek.com/2/events?venue.state=NY&page=\(page)&client_id=MTM5OTE0OTd8MTU0MjU2NTQ4MC4z")!
fetchData(url: seatGeekApiUrl) { (result: FetchResult<Welcome>) -> (Void) in
switch result {
case .success(let object): self.eventData.append(contentsOf: object.events)
print("\neventData: \n\n\(self.eventData)")
case .failure(let error):
print("\nError decoding JSON: \n\n\(error)")
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
print("\nFetching next batch of events: (Page \(page))\n")
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
if offsetY > contentHeight - scrollView.frame.height {
if !fetchingMore {
fetchMoreEvents()
}
}
}
Once fetchMoreEvents is triggered, I have it append my eventData array with the next page of results and reload the table view. My print statement confirms that it fetches page 2 of the data, but like I said that happens immediately instead of when I scroll down the page. Also, it never gets triggered again.
Is this an issue with the measurements in scrollViewDidScroll, or am I going wrong somewhere else?
These are the table view methods if they're applicable here:
override func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return eventData.count
} else if section == 1 && fetchingMore {
return 1
}
return 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "eventsCell", for: indexPath) as! EventsTableViewCell
let event = eventData[indexPath.row]
// Labels
cell.eventNameLabel.text = event.title
cell.eventVenueLabel.text = event.venue.nameV2
cell.eventAddressLabel.text = event.venue.address
cell.eventTimeLabel.text = dateFormatter.string(from: event.dateTimeLocal)
// Image
if let urlString = event.performers[0].image, let imageURL = URL(string: urlString) {
ImageService.getImage(url: imageURL) { (image) in
cell.eventImageView.image = image
}
}
else {
cell.eventImageView.image = UIImage(named: "noImageFound")
}
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "loadingCell", for: indexPath) as! LoadingCell
cell.spinner.startAnimating()
return cell
}
}
Declare an Bool as property
var isAllRowSeeked: Bool = false
Put this logic in your scrollViewDidScroll
let height = scrollView.frame.size.height
let contentYoffset = scrollView.contentOffset.y
let distanceFromBottom = scrollView.contentSize.height - contentYoffset
if distanceFromBottom < height,
self.isAllRowSeeked == true {
// you've reached the end, you are now ready to load more data
self.isAllRowSeeked = false
}
Now in cellForRowAtIndexPath
if fetchingMore == true,
isLastSectionRow(indexPath: indexPath) {// it's the last row of this section
state.isAllRowSeeked = true
return paginatorUI?.getPaginatedLoadMoreCell()
} else {
return nil
}
Add the following method
public func isLastSectionRow(indexPath: IndexPath) -> Bool {
let lastSection = tableview.dataSource?.numberOfSections?(in: tableview)
?? 1
let lastRow = tableview.dataSource?.tableView(tableview,
numberOfRowsInSection: indexPath.section)
?? 0
return lastSection == (indexPath.section+1) && lastRow == (indexPath.row+1)
}
Actually this logic is borrowed from one of my pod, which you can use. Complete code for this pagination can be found here

how check table view cell focus and player set pause

my project
I have several videos in the table in a row.
The problem is that they are played synchronously.
On screen 1video and 2video play synchronously
How do i can track focus cell on table view and cell.player?.play(). And other cells cell.player?.pause()
my code:
class MyViewController
//don't work
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? ListViewCell {
cell.player?.pause()
}
}
//don't call
func tableView(_ tableView: UITableView, didUpdateFocusIn context: UITableViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
print("table view cell didUpdateFocusIn")
}
class MyTableviewCell
override func prepareForReuse() {
super.prepareForReuse()
player?.pause()
}
I'm making some changes to your project. Check this
https://yadi.sk/d/bQ-eIyMz3VF28V
Decide which video to play or pause when scrolling:
func handleScroll() {
if let indexPathsForVisibleRows = myTableView.indexPathsForVisibleRows, indexPathsForVisibleRows.count > 0 {
var focusCell: ListViewCell?
for indexPath in indexPathsForVisibleRows {
if let cell = myTableView.cellForRow(at: indexPath) as? ListViewCell {
if focusCell == nil {
let rect = myTableView.rectForRow(at: indexPath)
if myTableView.bounds.contains(rect) {
cell.player?.play()
focusCell = cell
} else {
cell.player?.pause()
}
} else {
cell.player?.pause()
}
}
}
}
}
Hope this will help you.
Use this in Your ViewController
extension ViewController {
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(UITableView.contentOffset) {
if let playIndexPath = currentPlayIndexPath {
if let cell = tblInstaFeed.cellForRow(at: playIndexPath) {
if player.displayView.isFullScreen { return }
let visibleCells = tblInstaFeed.visibleCells
if visibleCells.contains(cell) {
cell.contentView.addSubview(player.displayView)
player.displayView.snp.remakeConstraints{
$0.edges.equalTo(cell)
}
self.player.play()
} else {
player.displayView.removeFromSuperview()
self.player.pause()
}
}
}
}
}
}
And Call that Like this:
var tableViewContext = 0
func addTableViewObservers() {
let options = NSKeyValueObservingOptions([.new, .initial])
tblInstaFeed?.addObserver(self, forKeyPath: #keyPath(UITableView.contentOffset), options: options, context: &tableViewContext)
}
And Call addTableViewObservers function in viewDidLoad
Hope this will help.
You can use indexPathForRow(at: CGpoint)
Here is Extension to tableView
import Foundation
import UIKit
extension UITableView {
// center point of content size
var centerPoint : CGPoint {
get {
return CGPoint(x: self.center.x + self.contentOffset.x, y: self.center.y + self.contentOffset.y);
}
}
// center indexPath
var centerCellIndexPath: IndexPath? {
if let centerIndexPath: IndexPath = self.indexPathForRow(at: self.centerPoint) {
return centerIndexPath
}
return nil
}
// visible or not
func checkWhichVideoToEnableAtIndexPath() -> IndexPath? {
guard let middleIndexPath = self.centerCellIndexPath else {return nil}
guard let visibleIndexPaths = self.indexPathsForVisibleRows else {return nil}
if visibleIndexPaths.contains(middleIndexPath) {
return middleIndexPath
}
return nil
}
}
Then Use at func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
if let visibleIndex = tableView.checkWhichVideoToEnableAtIndexPath() ,
let cellMiddle = tableView.cellForRow(at: visibleIndex) as? ListViewCell
{
cellMiddle.player?.play()
}
else
{
cell.player?.pause()
}
I have implemented similar functionality like facebook video player.
-- > Auto play video
--> if video pause by user don't auto play it
--> Pause video as soon as it is removed from the screen
This is tested and working in every scenarios
You need to track video status in your datasource array. And I suggest you to create datasource array with class not with struct as it we need reference
var videos:[VideoAlbum] = []
var lastContentOffset:CGFloat = 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// print("scrollViewDidScroll")
let visibleCell = self.collView.indexPathsForVisibleItems
if visibleCell.count > 0 {
for indexPath in visibleCell {
if videos[indexPath.row].isPlaying || videos[indexPath.row].pausedByUser {
continue
}
if let cell = self.collView.cellForItem(at: indexPath) as? CustomCell,//cell.video?.mediaType == MediaType.video,
let rect = self.collView.layoutAttributesForItem(at: indexPath)?.center {
//If cell's center is top of bottom of view OR cell's center is on Bottom of top of cell then play
let cellCenterToView = self.collView.convert(rect, to: self.view)
let cellFrameToView = self.collView.convert((self.collView.layoutAttributesForItem(at: indexPath)?.frame)!, to: self.view)
// scrolling up
if scrollView.contentOffset.y > lastContentOffset {
if cellCenterToView.y < self.view.frame.height && (cellFrameToView.height - abs(cellFrameToView.origin.y)) > self.view.frame.size.height / 2 {
self.pauseVideo(notFor: indexPath)
self.playVideoForCell(cell: cell,at: indexPath)
} else {
self.pauseVideoFor(indexPath: indexPath)
print("ELSE on SCROLL UP")
}
} else {
if cellCenterToView.y > 0 && (cellFrameToView.height - abs(cellFrameToView.origin.y)) > self.view.frame.size.height / 2 {
print(self.view.frame.intersection(cellFrameToView).size.height)
self.pauseVideo(notFor: indexPath)
self.playVideoForCell(cell: cell,at: indexPath)
} else {
self.pauseVideoFor(indexPath: indexPath)
print("ELSE on SCROLL DOwn \((self.view.frame.intersection(cellFrameToView).size.height + 64))")
}
}
}
}
}
lastContentOffset = scrollView.contentOffset.y
}
And Here is function for playing and pausing video
//--------------------------------------------------------------------------------
private func pauseVideo(notFor autoPlayIndexPath:IndexPath) {
let visibleCell = self.collView.indexPathsForVisibleItems
if visibleCell.count > 0 {
for indexPath in visibleCell {
if videos[indexPath.row].isPlaying && indexPath.row != autoPlayIndexPath.row {
guard let cellToHide = self.collView.cellForItem(at: indexPath) as? CustomCell/*,cellToHide.video?.mediaType == MediaType.video */ else {continue}
cellToHide.player?.pause()
cellToHide.player?.removeTimeObserver(cellToHide.video.timeObserver)
cellToHide.video.currentTime = cellToHide.player?.currentTime() ?? kCMTimeZero
cellToHide.video.isPlaying = false
NotificationCenter.default.removeObserver(cellToHide.player?.currentItem, name: Notification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
// if cellToHide.video.timeObserver != nil {
// cellToHide.player?.removeTimeObserver(cellToHide.video.timeObserver)
// }
}
}
}
}
//--------------------------------------------------------------------------------
private func pauseVideoFor(indexPath:IndexPath) {
if videos[indexPath.row].isPlaying {
guard let cellToHide = self.collView.cellForItem(at: indexPath) as? CustomCell/*,cellToHide.video?.mediaType == MediaType.video */ else {return}
cellToHide.player?.pause()
cellToHide.player?.removeTimeObserver(cellToHide.video.timeObserver)
cellToHide.video.currentTime = cellToHide.player?.currentTime() ?? kCMTimeZero
cellToHide.video.isPlaying = false
NotificationCenter.default.removeObserver(cellToHide.player?.currentItem, name: Notification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
// if cellToHide.video.timeObserver != nil {
// cellToHide.player?.removeTimeObserver(cellToHide.video.timeObserver)
// }
}
}
And here is model that is loaded in collection view
class VideoAlbum:Codable {
let id, image,video: String?
let mediaType: JSONNull?
let type, deleted, createdOn: String?
let modifiedOn: JSONNull?
var isPlaying:Bool = false
var currentTime:CMTime = kCMTimeZero
var timeObserver:Any? = nil
var pausedByUser:Bool = false
var hidePlayingControls = false
enum CodingKeys: String, CodingKey {
case id, image, video
case mediaType = "media_type"
case type, deleted
case createdOn = "created_on"
case modifiedOn = "modified_on"
}
}
Hope it is helpful to you

UITableView Height is not correct dynamically some time using Swift and without AutoLayout

Really I have one issue about set UITableView Height Dynamically correct. I am not using AutoLayout. First time when table Loaded, It contains lots of space top and bottom of the cell and some time images are overlapping with other cells. I am unable to calculate the right height of cell due to images (image can be large in height )Below is code.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeight
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:Templategt4 = tableView.dequeueReusableCell(withIdentifier: "Templategt4Identify", for: indexPath) as! Templategt4
cell.delegate = self;
cell.configure( self.storyData[0], row: indexPath, screenSize: screenSize,
gt4tableview: self.storyTableView);
//self.secondWebViewLable.scrollView.contentSize.height
cellHeight = cell.firstWebViewLable.frame.height + cell.firstImage.frame.height + cell.secondWebViewLable.frame.height;
//cellHeight = cellHeight + 200
print("cellForRowAt cellHeight", cellHeight);
return cell
}
I am using Custom Cell. The cell contains to UiWebView, UIImageView ( Height can be too much long ). Below is the code of cell.
class Templategt4: UITableViewCell, UIWebViewDelegate{
#IBOutlet weak var firstWebViewLable: UIWebView!
#IBOutlet weak var firstImage: UIImageView!
#IBOutlet weak var secondWebViewLable: UIWebView!
#IBOutlet weak var playiconimage: UIImageView!
let padding = 24;
var contentHeights1 : [CGFloat] = [0.0]
var contentHeights2 : [CGFloat] = [0.0]
var imageHeights : [CGFloat] = [0.0]
var gt4tableview: UITableView? = nil
var indexpath: IndexPath? = nil
var delegate:ImageClickedForStoryDetails?
class MyTapGestureRecognizer: UITapGestureRecognizer {
var youTubeCode: String?
}
func configure(_ data: StoryDetailsRequest, row: IndexPath, screenSize: CGRect, gt4tableview: UITableView) {
self.gt4tableview = gt4tableview;
self.indexpath = row;
let callData = data.content[(row as NSIndexPath).row];
let url = Constants.TEMP_IMAGE_API_URL + callData.image;
// this is first webview data.
self.firstWebViewLable.frame.size.height = 1
self.firstWebViewLable.frame.size = firstWebViewLable.sizeThatFits(.zero)
let htmlString1:String! = callData.text1
self.firstWebViewLable.delegate = self;
self.firstWebViewLable.loadHTMLString(htmlString1, baseURL: nil)
// this is second webview data.
self.secondWebViewLable.frame.size.height = 1
self.secondWebViewLable.frame.size = secondWebViewLable.sizeThatFits(.zero)
let htmlString2:String! = callData.text2
self.secondWebViewLable.loadHTMLString(htmlString2, baseURL: nil)
self.secondWebViewLable.delegate = self;
if( !callData.image.isEmpty ) {
let range = url.range(of: "?", options: .backwards)?.lowerBound
var u:URL!
if(url.contains("?")) {
u = URL(string: url.substring(to: range!))
} else {
u = URL(string: url)
}
self.firstImage.image = nil
self.firstImage.kf.setImage(with: u, placeholder: nil,
options: nil, progressBlock: nil, completionHandler: { image, error, cacheType, imageURL in
if( image != nil) {
let imageW = (image?.size.width)!;
let imageheight = (image?.size.height)!;
self.firstImage.image = image;
self.firstImage.contentMode = UIViewContentMode.scaleAspectFit
self.firstImage.clipsToBounds = false
let ratio = self.firstImage.frame.size.width/imageW;
let scaledHeight = imageheight * ratio;
if(scaledHeight < imageheight)
{
//update height of your imageView frame with scaledHeight
self.firstImage.frame = CGRect(x: 0,
y: Int(self.firstWebViewLable.frame.origin.y + self.firstWebViewLable.frame.height),
width: Int(screenSize.width)-self.padding,
height: Int(scaledHeight) )
} else {
self.firstImage.frame = CGRect(x: 0,
y: Int(self.firstWebViewLable.frame.origin.y + self.firstWebViewLable.frame.height),
width: Int(screenSize.width)-self.padding,
height: Int(imageheight) )
}
} else {
self.firstImage.image = UIImage(named: "defaultimg")
}
self.secondWebViewLable.frame = CGRect(x: 0,
y: Int(self.firstImage.frame.origin.y + self.firstImage.frame.height),
width: Int(screenSize.width)-self.padding,
height: Int(self.secondWebViewLable.frame.height) )
if (self.imageHeights[0] == 0.0)
{
self.imageHeights[0] = self.firstImage.frame.height
UIView.performWithoutAnimation {
let ipath = IndexPath(item: (self.indexpath?.row)!, section: (self.indexpath?.section)!)
self.gt4tableview?.reloadRows(at: [ipath], with: UITableViewRowAnimation.none)
}
}
})
}
}
func webViewDidStartLoad(_ webView: UIWebView)
{
//myActivityIndicator.startAnimating()
}
func webViewDidFinishLoad(_ webView: UIWebView)
{
if(webView == self.firstWebViewLable) {
if (contentHeights1[0] != 0.0)
{
// we already know height, no need to reload cell
return
}
contentHeights1[0] = self.firstWebViewLable.scrollView.contentSize.height
UIView.performWithoutAnimation {
let ipath = IndexPath(item: (self.indexpath?.row)!, section: (self.indexpath?.section)!)
self.gt4tableview?.reloadRows(at: [ipath], with: UITableViewRowAnimation.none)
}
} else if(webView == self.secondWebViewLable) {
if (contentHeights2[0] != 0.0)
{
// we already know height, no need to reload cell
return
}
contentHeights2[0] = self.secondWebViewLable.scrollView.contentSize.height
UIView.performWithoutAnimation {
let ipath = IndexPath(item: (self.indexpath?.row)!, section: (self.indexpath?.section)!)
self.gt4tableview?.reloadRows(at: [ipath], with:UITableViewRowAnimation.none)
}
}
}
You should not calculate the cell height in cellForRow method.
and instead of heightForRowAt: use this method
TableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat
and in your viewDidLoad call this.
tableView.rowHeight = UITableViewAutomaticDimension

Cell height not being calculated properly because of Completion Handler

I have a cell which has an image in it that is retrieved from Amazon S3. The height of this image will affect the height of the cell but unfortunately the image is retrieved in a completion handler after the cell height is determined. If I scroll down and up the cell reloads properly.
Before scrolling (incorrect cell height):
After scrolling (correct cell height):
Cell configuration (some unnecessary stuff redacted for readability):
func setCompletionCell(completion: CompletionPublic, contentType: String, classType: String){
self.completionPub = completion
self.selectionStyle = UITableViewCellSelectionStyle.None //Disables clicking
//Setting image or video
if (contentType == "image"){
nwa.fetchSignedUrl("image/accepted/" + completion.mediaId + ".png") { (result, err) in
self.nwa.fetchImage(result) { (image, err) in
if image != nil{
let screenSize: CGRect = UIScreen.mainScreen().bounds
var multiplyNum = screenSize.width / image.size.width
//if image height is going to be more than 60% of the screen, resize width and height to ensure that it isn't greater than 60% while keeping the aspect ratio correct
if ((image.size.height*multiplyNum) > (screenSize.height*0.6)){
multiplyNum = screenSize.height*0.6 / image.size.height
self.imageViewWidthConstraint.constant = (multiplyNum*image.size.width)
self.imageViewHeightConstraint.constant = screenSize.height*0.6
}
else{
self.imageViewWidthConstraint.constant = screenSize.width
self.imageViewHeightConstraint.constant = (multiplyNum*image.size.height)
}
self.imgView.image = image
}
else{
//no image returned
}
}
}
}
else if (contentType == "video"){
ytplayer.loadWithVideoId(completion.mediaId)
}
}
TableView delegate methods:
func callNWT(tableView: UITableView, completionHandler: () -> ()) {
switch trendingToggle {
case 0:
nwt.getTrendingBounties(0) { (bountyArr, err) in
//#TODO: change pos
if bountyArr == nil {
self.bountyArr = []
}
else {
self.bountyArr = bountyArr as [BountyPublic]
}
if self.bountyArr.count == 0 {
completionHandler()
}
self.reloadTableViewContent(tableView)
}
case 1:
nwt.getTrendingCompletions(0) { (compArr, err) in
if compArr == nil {
self.compArr = []
}
else {
self.compArr = compArr as [CompletionPublic]
}
if self.compArr.count == 0 {
completionHandler()
}
self.reloadTableViewContent(tableView)
}
case 2:
nwt.getTrendingPeople(0) { (peopleArr, err) in
if peopleArr == nil {
self.peopleArr = []
}
else {
self.peopleArr = peopleArr as [Person]
}
if self.peopleArr.count == 0 {
completionHandler()
}
self.reloadTableViewContent(tableView)
}
default:
break
}
}
func configureTableView(tableView: UITableView){
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 500.0
tableView.allowsSelection = false; //disables selection highlighting of cells
tableView.tableFooterView = UIView()
tableView.dataSource = self
}
func reloadTableViewContent(tableView: UITableView) {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
tableView.reloadData()
print("reloading table view content")
tableView.scrollRectToVisible(CGRectMake(0, 0, 1, 1), animated: false)
})
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if trendingToggle == 0 {
return bountyArr.count
}
else if trendingToggle == 1 {
return compArr.count
}
else {
return peopleArr.count
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if trendingToggle == 0 {
return bountyCellAtIndexPath(tableView, indexPath: indexPath)
}
else if trendingToggle == 1 {
return completedCellAtIndexPath(tableView, indexPath:indexPath)
}
else {
return personCellAtIndexPath(tableView, indexPath: indexPath)
}
}
func completedCellAtIndexPath(tableView: UITableView, indexPath:NSIndexPath) -> CompletedCell{
var cell: CompletedCell
if compArr[indexPath.row].contentType == "image" {
cell = tableView.dequeueReusableCellWithIdentifier(completedImgCellIdentifier) as! CompletedCell
let comp = compArr[indexPath.row]
cell.setCompletionCell(comp, contentType: "image", classType: "trending")
}
else { //video
cell = tableView.dequeueReusableCellWithIdentifier(completedVidCellIdentifier) as! CompletedCell
let comp = compArr[indexPath.row]
cell.setCompletionCell(comp, contentType: "video", classType: "trending")
}
return cell
}
How do I ensure the cell height is correctly calculated the first time? Is there a way I can delay the code executing until the image is retrieved? Or is that not a good idea?
Reload the tableview in completion handler.
tableView.reloadData()
In the heightForRowAtIndex method for the datasource/delegate protocols recalculate the height using the image and return the appropriate value for each individual cell.
When you call reloadData() all the datasource/delegate methods are called again, so return the correct height will allow you to resize the cell as needed.

Resources