Pulling down data into a collectionView only when user is scrolling? - ios

have a collectionView that potentially loads a ton of cells depending on the data. Currently my architecture is viewDidLoad (get first 25 from dataSource), then in willDisplayCell (get rest of datasource in sets of 25 until I hit a rate limit). The problem is that it appears that its pulling down all the data at once. I would like to not pull down more data for the cells unless the user is scrolling (i.e. unless those cells are necessary to be displayed). How can I do this?
I know that I need to work with the cell and indexPath inside willDisplayCell but I can't quite piece it together?
override func viewDidLoad() {
super.viewDidLoad()
getFirstPinsAndImages()
}
override func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
if maxExceeded == false {
getRestOfPinsAndImages()
}
}

What I needed to do was limit the data call to only when the indexPath.row of willDisplayCell (i.e. the row that is about to be displayed) was greater than the number of items in section (cells loaded) less some arbitrary number (to give a buffer for loading time):
override func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
if indexPath.row > (self.collectionView(collectionView, numberOfItemsInSection: 0) - 10) {
if maxExceeded == false {
getRestOfPinsAndImages()
}
}
}

Related

Keep UICollectionView position after a reload using dynamic cell's height

I have a vertical UICollectionView with flow layout and I'm trying to implement an infinite scroll behaviour. When the view comes near to the end, the api request 10 more registers on the server side and then I update the collection data. But when I do that my table don't keeps on the same cell I was before the reload. How can I keep the position after a reload?
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.row == viewModel.numberOfItemsInSection - 3 {
self.viewModel.fetchNextRepositoriesPage() { shouldUpdateData in
self.repositoriesListView.repositoriesCollectionView.reloadData()
self.repositoriesListView.layoutIfNeeded()
}
}
}
private func reloadCollectionData() {
let offset = collectionView.contentOffset
collectionView.reloadData()
view.layoutIfNeeded()
collectionView.contentOffset = offset
}
EDIT:
I noticed that this problem only occurs when I use estimadedItemSize on my collectionView. When the itemSize is a constant the problem does not occur.

Loading data into UICollectionView bunch by bunch

I am building an chat app and I have a problem with the collection view. In my app I just want to do something like whatsapp, in starting I load the first 50 messages and when we scroll down then want to fetch other messages bunch by bunch. But when I doing this and call the collection view reloadData method then it scroll to the top message. And I don't want they scrolling. Can you help me. Please..
I just simply want to make the collectionView similar to whatsapp's chat view.. i.e. load more messages when user scroll to the older messages.
What you are asking for is just feeding (inserting) cell into collection view without reloading it.
Check this appledoc, on inserting item.
determine the new indexPaths and then feed it to collection view using following
yourCollectionView.insertItems(at indexPaths: newIndexPaths)
Detect when you reach at the end and fetch more data
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath)
{
let lastSectionIndex = collectionView.numberOfSections - 1
let lastRowIndex = collectionView.numberOfItems(inSection: lastSectionIndex) - 1
if indexPath.section == lastSectionIndex && indexPath.row == lastRowIndex
{
//fetch more items
//add items to data array
//Reload collectionview
}
}
Try pagination concept
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.row + 1 == dataArray.count && dataArray.count < completeData.count {
fetchMoreData()
}
.....
}
func fetchMoreData() {
//Fetch and add more data to your dataArray.
//Reload collectionView
}

Identify when the UITableView loads the visible set of rows in iOS

I am looking for the event that gets fired when the tableview loads the rows to fit the screen. I know tableview loads only the number of rows that fit the screen. I want to execute a set of code when the rows that fit the screen are loaded.
Any pointers on how to determine this?
I think you'd need to implement UITableViewDelegate and override the WillDisplay(UITableView, UITableViewCell, NSIndexPath) method. Be sure to set the delegate of your tableview to the class that implements UITableViewDelegate.
Unfortunately WillDisplay is called per cell, not per row.
I am not good at c#, so please translate this from Swift. I've added another solution down below which is, sort of a manual calculation of visible rows in tableview.
The Recommended Solution:
var isFirstTime:Bool = true
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let indexPaths = tableView.indexPathsForVisibleRows {
if indexPath == indexPaths.last {
if isFirstTime {
self.visibleCellsLoaded()
}
isFirstTime = false
}
}
}
The isFirstTime (Very important) flag will restrict your specific "set of code" (self.visibleCellsLoaded()) to execute only once. You can remove it if you want it to be executed every time you scroll - which apparently negates the purpose of your question.
Another solution that also works:
Here we manually calculate and get what tableView.indexPathsForVisibleRows returns us (Result is mostly similar to the previous method)
fileprivate func getLastVisibleIndexPath() -> IndexPath {
//This samples the first indexPath only, so this works only with rows that have static height, not with UITableViewAutomaticDimension.
let firstIndexPath = IndexPath.init(row: 0, section: 0)
let tableViewHeight = self.tableView.bounds.height
let rowHeight = tableView.rectForRow(at: firstIndexPath).size.height
let numberOfVisibleRows = tableViewHeight / rowHeight
return IndexPath.init(row: Int(numberOfVisibleRows - 1), section: 0)
}
var isFirstTime:Bool = true
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath == getLastVisibleIndexPath() {
if isFirstTime {
self.visibleCellsLoaded()
}
isFirstTime = false
}
}

Swift - How to do specific action on last row of a UITableView when the last row isn't visible?

I am trying to achieve a custom action on the last row of my UITableView.
I found a nice extension in order to know if I'm on the last row :
extension UITableView {
func isLast(for indexPath: IndexPath) -> Bool {
let indexOfLastSection = numberOfSections > 0 ? numberOfSections - 1 : 0
let indexOfLastRowInLastSection = numberOfRows(inSection: indexOfLastSection) - 1
return indexPath.section == indexOfLastSection && indexPath.row == indexOfLastRowInLastSection
}
}
The only issue I'm facing is that if my tableView has many rows, with scrollable content, the last row isn't visible.
Thus, in my cellForRow I am doing this :
if tableView.isLast(for: indexPath) {
print("Last Row - Do Specific action")
} else {
}
It is working, except that imagine my screen can display 6 rows, but the total number of rows of my tableView is 15.
It will apply my specific action on the row at indexPath 5 and 15.
I am guessing it is because each time cellForRow is called, it is checking for the last row, and it apply on the last visible row also.
How can I achieve a custom action ONLY on the last row even if it is not visible yet?
EDIT: After more investigation, the issue comes from UITableView not able to prefetch the cell, and cellLoaded and cellVisible is the same.
I can't copy paste the code as I don't have xcode right now.
But if you want to perform action before cell gets displayed then do it in
func tableView(_ tableView: UITableView,
willDisplay cell: UITableViewCell,
forRowAt indexPath: IndexPath)
And to check for the last row:
if indexPath.row == dataSourceArray.count - 1 {
//Perform action
}
dataSourceArray is the array from which you are fetching the data to show it in the cell.
Basically your code should be something like this
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == dataSourceArray.count - 1 {
//Perform action
}
}
You may go through apple docs to know more about tableview delegate methods
You can try with tableview delegate method:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
try this code in cell for row method
if indexPath.row == yourarray.count - 1{
// Put your code
}

Display cells of tableview gradually using autolayout?

I have a tableview inside my UIViewController to display comments. The height of this tableview depends on the number and the size of comments. The cells are dynamic.
I use autolayout, so my tableview has a height constraint. I set this constraint programmatically :
override func viewDidLayoutSubviews() {
self.heightCommentsTableView.constant = self.commentsTableView.contentSize.height + 50
}
It works if I display all the comments at once.
BUT, I would like display the comments 5 per 5, using this method : load more for UITableView in swift because of performance issue to display my view
(all my comments are loaded before pushing the view)
I noticed when I set my constraint, it calls cellForRowAtIndexPath for all the comments, not only the first 5.
I don't know how to do.
EDIT
var allCommentsArray: NSMutableArray = []
var elements: NSMutableArray = []
var range = 5
var currentPage = 0
var nextpage = 0
override func viewDidLoad() {
super.viewDidLoad()
allCommentsArray = NSMutableArray(array: comments)
elements.addObjectsFromArray(allCommentsArray.subarrayWithRange(NSMakeRange(0, range)))
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return comments.count
}
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
self.nextpage = self.elements.count - 5
if indexPath.row == nextpage {
self.currentPage++
self.nextpage = self.elements.count - 5
self.elements.addObjectsFromArray(self.allCommentsArray.subarrayWithRange(NSMakeRange(self.currentPage, self.range)))
tableView.reloadData()
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CommentCustomTableViewCell", forIndexPath: indexPath) as! CommentCustomTableViewCell
self.configureCell(cell, atIndexPath: indexPath)
return cell
}
The table view will call tableView(_:cellForRowAtIndexPath:) for every visible cell it is about to create, the amount of it will determine based on how many sections and how many cells in each section. You let the table vie know these amounts by implementing the tableView(_:numberOfRowsInSection:) and numberOfSectionsInTableView(_:) methods of UITableViewDataSource. So, if you want to control how many cells could possibly be created and visible at a given time, you'd have to manage that state in your data source by adding and removing according to whatever logic you desire. In the answer that you linked, you can see that he is called elements.addObjectsFromArray to progressively add more elements in batches.

Resources