Loading N views of UICollectionView and reload at the end - ios

[EDIT after replies: solution at the end of the question]
I have a UICollectionView where I initially load 4 views. I would like that when the user scrolls until the last one (4th view) and try to scroll again, I update the data of my model, reload the UICollectionView and moves back to the 1st view so that the user can again scroll 3 times till the 4th view, etc.
The issue is that when I load 4 views initially, the user can't scroll after he reached the last view since there is nothing after. So I created an empty cell as my 5th view and now I load 5 views in my initial UICollectionView. But this 5th view is never fully displayed and I use "willDisplay" method to reload the data and force the move back to the first view. This is not elegant but I couldn't find another way to do it.
This method is working fine but here is the problem: it seems that when willDisplay is called, I indeed reload the data and moves back to the first view but only then the scroll that was supposed to take me to the 5th view is taken into account which means that after I moved back to the first view, the app shows me actually the second view (it did move back to the first view but then the scroll that I initiated to go from 4th to 5th view is applied, moving me to the 2nd view).
I hope this is clear...
So here is the code I am using:
override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// if this is the last view
if((indexPath.row == self.dataModel.getNumberOfData()) && self.dataModel.getNumberOfData() > 0)
{
// I update the model here
self.dataModel.resetData()
// Then I reload the UICollectionView
self.collectionView.reloadData()
// Then I move back to the first view
self.collectionView.selectItem(at: IndexPath(item: 0, section: 0), animated: false, scrollPosition: .centeredHorizontally)
}
}
Of course if you have another solution, more elegant, to avoid that 5th view, I am happy to consider it. My data is not static so when I need to reload the new data, I query a database to retrieve the new information so I don't know about the new data when I run the app.
Thanks.
[EDIT with a solution using scrollViewDidScroll. I show for example how to detect the end (horizontally scrolling and delete the first view...
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
// if reached the end of the UICollectionView...
if (scrollView.contentOffset.x == scrollView.contentSize.width - scrollView.frame.size.width) {
print("Reached the end...")
self.dataModel.removeNFirst(n: 1) // remove first item from model
// delete the first view of the UICollectionView
self.collectionView.performBatchUpdates({
self.collectionView.deleteItems(at: [IndexPath(item: 0, section: 0)])
}) { (finished) in
self.collectionView.reloadData()
}
}
}

Related

What are the differences between remembersLastFocusedIndexPath and indexPathForPreferredFocusedView(in:) methods?

I have made a TvOS app which has 2 collection views they will be scrollable horizontally.
Now both of them have items which open corresponding cell's detail page.
That new page is pushed on navigation stack, when I come back I want my focus to remain on same cell of the collection view which opened the new page.
So, I found and tried two methods.
Method 1:
Setting collectionView.remembersLastFocusedIndexPath = true for both collection views
Method 2:
Using indexPathForPreferredFocusedView(in:) in delegate of both collection views.
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// SOME WORK
focusedCellIndexPath = indexPath
}
override func indexPathForPreferredFocusedView(in collectionView: UICollectionView) -> IndexPath? {
return focusedCellIndexPath
}
Now when I used Method 1:
Say I have 10 cells in both collection view.
I was on cell 2 in first collection view, then I press down and scroll till the end on the second collection view and I press up then I go back to second cell of first collection view.
But when I used Mewthod 2:
When I press up then I don't go on second cell of first collection view but go on the cell just above currently focused cell of second collection view.
I want to know why they both behaved differently, looking at official documentation of method used in method2 in its discussion section it looks both things should behave similarly, but that is not the case here.

UITableView ScrollToRow function animating weirdly

The ScrollToRow is not working as I want.
The function I wrote seems to be working fine, but I want it to scroll down to last row only when its added like it happens in chat. When you get a new message, your tableview scrolls to the bottom to read new message.
However, my tableview seems to reload every time before performing scrollToLastRow function (what I mean is every time before scrolling to bottom... it first goes back to top first cell and starts scrolling to bottom from there Every time when a new cell is created).
My code is simple for the scroll:
func scrollToLastRow()
{
let indexPath = IndexPath(row: messages.count - 1, section: 0)
self.chat.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
To check my code:
here's a link to my repository
ChatApplication
the ViewController's used for chat is ---> chatVC
the ViewController used to make chat is -> chatCell
I have tried anything I could get all over I could find.
Tried dispatch (without it the scroll function wasn't working at all)
Used the function on many different places (including viewDidAppear)
Used dispatch.asyncAfter for delay.....
but nothing worked.
#Fahad, you are reloading the tableview before the scrollToLast function is getting called. Please check and please verify at your end.
Note please update your GitHub repository url in your question properly as it not clickable.
Use this function for scrolling to the bottom.
func scrollToBottom() {
let bottomOffset = CGPoint(x: 0, y: contentSize.height - bounds.size.height + contentInset.bottom)
setContentOffset(bottomOffset, animated: true)
}
use tableview.scrollToBottom()
Use tableView.insertRows(at: [IndexPath], with: UITableView.RowAnimation) for adding object to end of the tableView.
From the problem you say and looking in to your code, the animation issues are due to reloading the entire tableView after a new item is added to dataSource array,
What you have to do is to use Call tableView.insertRows(at: [IndexPath], with: UITableView.RowAnimation) with the correct indexPath on your tableView and the tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) will be invoked.

Why does Collection View load center carousel with this code?

I've been trying to figure out how to make my collection view of 3 cells load with the 2nd cell, and I finally figured it out after looking through StackOverFlow. However, the code that I came across is a bit confusing to me. Would anyone be able to explain why this code below works in making my collection view cell (that covers the whole screen) start with the 2nd of 3 cells? (this is the effect I wanted to achieve all along, but I want to learn more about why this code works exactly.
In this block of code, there's a bool variable and an if statement, why are they needed? When I took out the boolean variable and if statement, the collection view was unable to scroll.
How does this block of code work exactly?
Thank you.
var onceOnly = false
internal override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if !onceOnly {
let indexToScrollTo = IndexPath(item: 1, section: 0)
self.collectionView.scrollToItem(at: indexToScrollTo, at: .left, animated: false)
onceOnly = true
}
}
1- This scrollToItem in Docs
let indexToScrollTo = IndexPath(item: 1, section: 0)
self.collectionView.scrollToItem(at: indexToScrollTo, at: .left, animated: false)
makes the collectionView scroll to the second item in the list
2-
When I took out the boolean variable and if statement, the collection view was unable to scroll
Because willDisplay is called for every cell display , so when for example you scroll to 3rd cell willdisplay is called and causes the collectionView to go to the second cell , so it makes it stuck in the second item all the time ( and this seems like no scroll but the scroll happens and you won't notice that as it happens instantly ) , so the boolean var is needed to make that scroll action happens once which is the scroll to the specified index

UITableView dynamic cell height scroll to top after calling reloadData

I have dynamic cell height in my tableView
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 100
I want to do same as whatsapp app (pagination), i want to load 25 by 25 row so when user scroll at top, i want to load new data but i want to keep same position of scroll, here is what i did
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if isMore && scrollView.contentOffset.y == 0 {
loadMore() // function that add new data in my model
self.tableView.reloadData()
}
}
The problem that i have is after i reach top of my tableview it call reloadData but the tableView scroll to UP
I tester some other solution like this:
var cellHeights: [IndexPath : CGFloat] = [:]
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
guard let height = cellHeights[indexPath] else { return 70.0 }
return height
}
But still not working
If you are reloading the table to add more data , then after reload call below function to retain the scroll position.
tableview.scrollToRow(at: indexPath, at: .middle, animated: true)
here indexPath is the indexPath of the last cell before reload.
In tableView:cellForRowAt when you detect indexPath is getting close to the last element (when indexPath.row is getting close to yourArray.count), query for more items. When you have them, add them to your data source (the array, or whatever it is, where you have all the objects represented in the table) and reload the table view. The table view should display the new items and the user can continue scrolling without doing anything special.
You shouldn't wait until tableView:cellForRowAt is called for the last cell, as the user will have to wait if you need to call a server to fetch more items. Whatsapp's scroll is fast because they download the items well before you scroll to the last elements (some time ago it did stop at the end, but now they are probably fetching new items way earlier).
Keep in mind that 25 items might be too few, it the network is slow the user might scroll through the items you have before more items to display and will have to wait, which is not a good user experience. Simulate a slow network connection and see if 25 items are enough to scroll through while your app contacts the server (depends on network speed, request and response sizes, how fast the server answers your request, etc.).
I faced the same problem in a messaging App. Here's how I solved it:
Assigned an unique messageId to every message in datasource array.
In cellForRow:, I save that messageId of very first row in a variable called messageIdFirst.
if(indexPath.row == 0){
messageIdFirst = message.id!
}
In loadMore() method, I check which current indexpath row have the same messageId in now updated datasource array.
if(message.id == messageIdFirst){
previousTopIndexPath = i
}
,where i being that index in datasource array which have the same messageId.
Now, I scroll to that previousTopIndexPath like this:
tableview.scrollToRow(at: previousTopIndexPath, at: .middle, animated: true)
Note 1: As this was not working efficiently in UITableView, I end up using UICollectionView with the same solution.
Note 2: While this does work smoothly, I know there must exist a cleaner way to do this.

didSelectItemAtIndexPath not called with multiple collection views on same screen

I have 2 collection views on the same screen, and I have the data source and delegate implemented for both in the same view controller. However, the delegate methods such as didSelectItemAtIndexPath is only called for one.
Other info:
Both collection views have a cell with an image.
AllowSelection and UserInteractionEnabled is set to true in both collection views
User interaction enabled on the images
Delegate and data source are set on the storyboard from both collection views to the view controller
Collection views are displayed properly
Both collection views have the same setup, yet only one delegate works. Would you have any clues what could be setting this up?
The collection views are inside a view that has a scroll view. Could this be somehow related?
EDIT 2:
Project with the same problem: https://github.com/iagomr/ProblemWithAutoLayout
EDIT 1:
Somehow this has to do with autolayout constraints, because if I pin the bottom collection view to the bottom of the screen instead of the bottom of other collection view, it starts working.
This is all due to the fact that I need to build a tall screen, and added everything into a view inside a scroll view 1000 points tall.
Code:
//MARK: - CollectionView Delegate
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
println("Called")
}
//MARK: - CollectionView DataSource
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if collectionView == thisCV {
return 1
} else if collectionView == thisOtherCV{
return 1
}
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
if collectionView == "thisCV" {
if let thisCell = collectionView.dequeueReusableCellWithReuseIdentifier("thisCell", forIndexPath: indexPath) as? thisCollectionViewCell {
thisCell.image = image
return thisCell
}
} else if collectionView == "thisOtherCV"{
if let thisOtherCell = collectionView.dequeueReusableCellWithReuseIdentifier("thisOtherCell", forIndexPath: indexPath) as? OtherCollectionViewCell {
thisOtherCell.image = image
return thisOtherCell
}
}
return UICollectionViewCell()
}
I can confirm that didSelectItem is not getting called. If constant for top-bottom constraint between two collection views is changed from 501 to 0 it is working.
This problem is most likely related to the fact that you have two scroll views (collection views) inside of another scroll view. Overall, I would say, that you should modify your UI. I would suggest two ways of fixing it
Using single collection view
Use just one collection view with different sections for different content. Also, do NOT embed it in the scroll view - collection view already has a scroll view so you should be able to scroll easily. You can also dequeue different class of cells for different sections so you should be able to do everything which you want to do now.
If you want a starting point, here is a good tutorial which should help you with that.
Using scroll view
If you want to setup your UI in Interface Builder remove both collection views and simply add all of your UI inside of scroll view. Place UIButton in places where you want clicking to produce action.
You can even assign the same action to each button and then differentiate which one was triggered by assigning custom tags to each of them.

Resources